Forgebase core libraries (utils, framework e cliente LLM)
Project description
Forgebase
Forgebase reúne três blocos principais usados neste monorepo:
- forge_utils – logging estruturado e utilidades de paths/configuração.
- forgebase – framework MVC-C enxuto baseado em Pydantic v2 (modelos, commands, controllers, views e persistência).
- llm_client – cliente agnóstico para a OpenAI Responses API com suporte a streaming, tool calling e replays offline.
A biblioteca está disponível em PyPI e também no TestPyPI para validação prévia.
⚡ Quick Start — System Instructions (v0.2.11+)
Agora o Forgebase expõe o parâmetro instructions (mensagem de sistema da OpenAI
Responses API). Não confunda com o prompt: as instructions definem o comportamento do
modelo e acompanham as chamadas (incluindo follow‑ups de tool calling).
Exemplos diretos:
from forgebase import LLMOpenAIClient, LLMClientFactory
# Cliente direto
client = LLMOpenAIClient(api_key="sk-...", model="gpt-4o")
resp = client.send_prompt(
"Explique idempotência.",
instructions="Você é um revisor sênior. Responda em uma frase."
)
# Provider via Factory (recomendado)
provider = LLMClientFactory.create("openai", api_key="sk-...")
out = provider.send_message(
"Liste 3 boas práticas de logging.",
instructions="Responda de forma objetiva, em tópicos."
)
# Streaming com instructions
for delta in provider.send_stream(
"Resuma o papel do backoff exponencial.",
instructions="Resuma em até 2 frases."
):
print(delta, end="")
Detalhes completos e observações em “Usando system instructions (Responses API)”, abaixo.
Instalação rápida
pip install forgebase
Crie um ambiente virtual limpo antes de instalar (python -m venv .venv && source .venv/bin/activate).
⭐ Novo na v0.2.2: Streaming + Tools (Responses API)
Esta versão foca em robustez do streaming com tool calling sobre a OpenAI Responses API.
- Headers de streaming: adicionamos
Accept: text/event-streame melhoramos timeouts (SSE sem read timeout) no cliente HTTP. - Normalização de tools: agora aceitamos tanto o formato top‑level da Responses API
(
{\"type\":\"function\",\"name\":...}) quanto o formato “aninhado” comum no Chat Completions ({\"type\":\"function\",\"function\": {\"name\":..., \"parameters\":...}}). O cliente normaliza para o contrato oficial da Responses API antes de enviar. - Sem fallback para Chat Completions: não utilizamos Chat Completions; todo o fluxo permanece na Responses API.
- Header opcional de beta: é possível habilitar
OpenAI-Beta: responses=v1viaOPENAI_BETA_RESPONSES=1caso seu ambiente exija este gate.
Documentação relacionada: docs/api/openai_responses.md, docs/api/openai_responses_tool_calling.md e docs/api/streaming_tools_guide.md.
Impacto: corrige erros intermitentes 400 observados durante streaming + tools e melhora compatibilidade com formatos de tools enviados por clientes legados.
⭐ Novo na v0.2.3: Robustez de Streaming + Follow‑up de Tools
Esta versão refina o fluxo de streaming e o follow‑up de ferramentas:
- Tratamento de erros em SSE:
- Em 4xx, o cliente drena o corpo e lança
APIResponseErrorcom a mensagem do provider (elimina o erro "without read()" e evita conexões em mau estado).
- Em 4xx, o cliente drena o corpo e lança
- Follow‑ups de tool calling (Responses API):
- O follow‑up inclui o descritor bruto
tool_use(além defunction_call) antes do respectivo*_output, alinhando com variantes da Responses API em cenários multi‑turn.
- O follow‑up inclui o descritor bruto
- Testes e documentação:
- Novos testes de streaming e follow‑up.
- Guia:
docs/api/streaming_tools_guide.md.
Compatibilidade: sem mudanças de interface — sua integração atual continua funcionando.
🧪 Desenvolvimento (v0.2.6+)
Esta seção resume como depurar e validar cenários de streaming + tools no ForgeBase.
-
Defaults a partir da v0.2.5/0.2.6
- Follow‑up em streaming:
outputs-onlyhabilitado (não reenvia descriptors brutos) tool_choiceno follow‑up:nonenameincluído nos outputs
- Follow‑up em streaming:
-
Flags de compatibilidade (override)
FORGEBASE_FOLLOWUP_OUTPUTS_ONLY=0|1FORGEBASE_FOLLOWUP_TOOL_CHOICE=none|auto|requiredFORGEBASE_OUTPUT_NAME=0|1FORGEBASE_OUTPUT_KIND=function_result|tool_result
-
Diagnóstico (sem vazar conteúdo)
FORGEBASE_DEBUG_FOLLOWUP=1emite em nível INFO a linha:[follow-up] prev=... outputs=N types=[...] call_ids=[...] unique=N- Recomendações:
- Habilitar
DEBUGno logger durante testes locais - Usar hooks do provider para imprimir o payload do follow‑up quando necessário
- Habilitar
-
Reconciliação de call_id (v0.2.8+)
- IDs
call_*coletados dos eventos de streaming e, se preciso, viainput_items - Substituem IDs alternativos ao montar os outputs no follow‑up
- Buffer de outputs particionado por
previous_response_id
- IDs
-
Testes
- Rodar toda a suíte:
pytest -q - Casos relevantes:
tests/test_streaming_error_handling.pytests/test_streaming_tool_use_followup.pytests/test_followup_flags_overrides.py
- O guia
docs/api/streaming_tools_guide.mdreforça cenários, flags e troubleshooting
- Rodar toda a suíte:
⭐ Novo na v0.2.1: LLMClientFactory
A partir da v0.2.1, use LLMClientFactory para criar providers LLM de forma desacoplada:
from forgebase import LLMClientFactory, Tool
# Opção 1: Criação manual
provider = LLMClientFactory.create("openai", api_key="sk-...", timeout=60)
# Opção 2: Auto-configuração via variável de ambiente (OPENAI_API_KEY)
provider = LLMClientFactory.create_from_env("openai")
# Usar o provider
response = provider.send_message("Hello, world!")
print(response)
# Trocar provider transparentemente (quando disponível)
# provider = LLMClientFactory.create("llama") # Mesma interface!
Por que usar a factory?
- ✅ Desacoplamento: Código não depende de implementação específica (OpenAI, Llama, etc.)
- ✅ Extensibilidade: Trocar provider mudando apenas 1 string
- ✅ Testabilidade: Mock de interface
ILLMClientem vez de classe concreta - ✅ Preparado para o futuro: Suporte a múltiplos providers sem breaking changes
⚠️ Deprecação:
OpenAIProviderainda funciona na v0.2.1 (backward compatible), mas recomendamos migrar paraLLMClientFactory. Veja exemplos abaixo.
Pacote por pacote
forge_utils
forge_utils.log_service.LogServiceconfigura logging com console/arquivo rotativo e filtros.forge_utils.log_service.loggeré a instância global pronta para uso.forge_utils.pathsoferece helpers (build_app_paths,ensure_dirs) para organizar arquivos de configuração, histórico e cache.
forgebase
Reexporta o framework MVC-C básico. Os pontos de entrada mais usados são:
CustomBaseModel/BaseModelData: modelos Pydantic com suporte a dirty tracking e observers.CustomCommandBase+guard_errors: encapsulam regras de negócio, padronizando o tratamento de exceções (CommandException).CustomBaseController/CustomBaseView: composição MVC-C mínima.PersistenceFactory+JsonPersistence: persistência compatível com Pydantic v2.
Todos estes nomes estão disponíveis diretamente com from forgebase import ....
llm_client
O cliente LLM também é reexportado por forgebase para facilitar o consumo:
API Pública (v0.2.1+):
LLMClientFactory: factory para criar providers sem conhecer implementação (✅ use este!)ILLMClient: interface Protocol para type hints e extensibilidadeTool: modelo Pydantic que representa o schema JSON das ferramentasAPIResponseError/ConfigurationError: exceções específicas do clienteContentPart,OutputMessage,ResponseResult,TextFormat,TextOutputConfig: modelos retornados pelo Responses API
Legacy (deprecated - mantido para compatibilidade):
OpenAIProvider: ⚠️ usarLLMClientFactory.create("openai")em vez dissoLLMOpenAIClient: ⚠️ interno - não usar diretamente
Multi-provider ready: A arquitetura está preparada para múltiplos providers (Llama, Anthropic, OpenRouter)
sem breaking changes no código cliente. Use LLMClientFactory para garantir compatibilidade futura.
Guia rápido de uso
Core MVC-C
from forgebase import CustomBaseModel, CustomCommandBase, JsonPersistence, guard_errors
class User(CustomBaseModel):
id: int
name: str
class CreateUserCommand(CustomCommandBase):
@guard_errors
def execute(self, payload: dict) -> User:
model = User(**payload)
# ... lógica de negócio ...
return model
storage = JsonPersistence("users.json")
Cliente LLM – configuração e chamadas
Crie um arquivo .env na raiz do projeto (ou exporte no shell) com:
OPENAI_API_KEY=sk-...
Todos os exemplos abaixo carregam essa chave automaticamente via python-dotenv.
Chamada síncrona
import os
from dotenv import load_dotenv
from forgebase import APIResponseError, ConfigurationError, LLMOpenAIClient
load_dotenv()
client = LLMOpenAIClient(api_key=os.environ["OPENAI_API_KEY"], model="gpt-4o-mini")
try:
# instructions: mensagem de sistema (não confundir com o prompt)
response = client.send_prompt(
"Por que o céu é azul?",
instructions="Você é um assistente técnico. Responda de forma objetiva."
)
answer = "\n".join(
part.text.strip()
for item in response.output
for part in getattr(item, "content", [item])
if getattr(part, "text", None)
)
print(answer)
except (APIResponseError, ConfigurationError) as exc:
print(f"Falha na chamada: {exc}")
Usando system instructions (Responses API)
"Instructions" são a mensagem de sistema da Responses API. Elas definem o comportamento do modelo, e não devem ser confundidas com o prompt do usuário. No Forgebase, você pode passá-las explicitamente nas chamadas do cliente e do provider. Se omitidas, a API do provider usará as instruções padrão do servidor.
Exemplos:
from forgebase import LLMOpenAIClient, OpenAIProvider, LLMClientFactory
# 1) Cliente direto
client = LLMOpenAIClient(api_key=os.environ["OPENAI_API_KEY"], model="gpt-4o")
resp = client.send_prompt(
"Liste 3 boas práticas para logs.",
instructions=(
"Você é um revisor sênior. Use linguagem clara, sem jargões desnecessários."
),
)
# 2) Provider (recomendado via Factory)
provider = LLMClientFactory.create_from_env("openai")
out = provider.send_message(
"Explique o conceito de idempotência.",
instructions="Padrão: respostas curtas, com um exemplo prático."
)
# 3) Streaming com instructions
for chunk in provider.send_stream(
"Resuma o papel do backoff exponencial.",
instructions="Resuma em até 2 frases."
):
print(chunk, end="")
Observações:
- Instructions são propagadas em múltiplas rodadas durante tool calling (follow‑ups).
- Instructions não substituem o prompt; são complementares e tratadas como “system message”.
- O campo é enviado como
ResponseRequest.instructionspara a Responses API.
Migrando do uso direto da OpenAI API
Antes utilizávamos o SDK oficial diretamente. Agora a camada LLMOpenAIClient traz timeout,
hooks e tool calling prontos, além de facilitar a troca de provider.
# Legado: uso direto do SDK da OpenAI
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
resp = client.responses.create(model="gpt-4o-mini", input="Por que o céu é azul?")
print(resp.output[0].content[0].text)
# Atual: via LLMOpenAIClient encapsulado
from forgebase import LLMOpenAIClient
client = LLMOpenAIClient(api_key=os.environ["OPENAI_API_KEY"], model="gpt-4o-mini")
resp = client.send_prompt("Por que o céu é azul?")
print(resp.output[0].content[0].text)
Configurando timeout (⭐ novo na v0.2.0)
Por padrão, as chamadas HTTP usam timeout de 120 segundos. Você pode customizar:
# Timeout de 45 segundos
client = LLMOpenAIClient(
api_key=os.environ["OPENAI_API_KEY"],
timeout=45 # segundos
)
# Ou via OpenAIProvider
from forgebase import OpenAIProvider
provider = OpenAIProvider(timeout=45)
provider.set_api_key(os.environ["OPENAI_API_KEY"])
result = provider.send_message("Hello")
Importante: O timeout se aplica ao tempo total incluindo retries. Com timeout=45 e max_tries=4, o sistema não ultrapassará 45 segundos mesmo com múltiplas tentativas.
Chamada com streaming
import os
from dotenv import load_dotenv
from forgebase import LLMOpenAIClient
load_dotenv()
client = LLMOpenAIClient(api_key=os.environ["OPENAI_API_KEY"], model="gpt-4o-mini")
stream = client.send_prompt("Conte uma história curta sobre um robô e uma criança.", streamed=True)
for delta in stream:
print(delta, end="", flush=True)
print()
Chamada multimodal (imagem + áudio)
import os
from dotenv import load_dotenv
from forgebase import LLMOpenAIClient
load_dotenv()
client = LLMOpenAIClient(api_key=os.environ["OPENAI_API_KEY"], model="gpt-4o")
response = client.send_prompt(
"Descreva a imagem e comente o áudio anexado.",
images=["https://upload.wikimedia.org/wikipedia/commons/9/99/Colorful_sunset.jpg"],
audio={"base64": "ZGF0YQ==", "mime_type": "audio/wav"},
)
print(response)
Listar modelos disponíveis
import os
from dotenv import load_dotenv
from forgebase import LLMOpenAIClient
load_dotenv()
client = LLMOpenAIClient(api_key=os.environ["OPENAI_API_KEY"])
models = client.list_models()
print(models["data"][0])
Tool calling síncrono (v0.2.1+)
import os
from dotenv import load_dotenv
from forgebase import LLMClientFactory, Tool
load_dotenv()
# ✅ Novo: usar factory
provider = LLMClientFactory.create_from_env("openai")
# 1) Descreva a ferramenta com JSON Schema compatível com a Responses API
tool = Tool(
type="function",
name="say_hello",
parameters={"type": "object", "properties": {"who": {"type": "string"}}, "required": ["who"]},
)
# 2) Conte ao provider quais ferramentas estão disponíveis
provider.configure_tools([tool], tool_choice="required")
# 3) Registre o handler Python que será chamado quando o modelo disparar a ferramenta
provider.register_tool("say_hello", lambda args: f"Olá, {args['who']}!")
# 4) Use send_message normalmente: o provider executa as tools e devolve o texto final
print(provider.send_message("Cumprimente Forgebase."))
📜 Código antigo (deprecated - clique para expandir)
# ⚠️ Deprecated: usando OpenAIProvider diretamente
from forgebase import OpenAIProvider, Tool
provider = OpenAIProvider()
provider.set_api_key(os.environ["OPENAI_API_KEY"])
# ... resto do código igual
Tool calling com orquestração automática
Providers criados pela factory encapsulam todos os ciclos de tool calling da Responses API.
Basta definir o schema das ferramentas, registrar os handlers e chamar send_message:
from forgebase import LLMClientFactory, Tool
provider = LLMClientFactory.create("openai", api_key=os.environ["OPENAI_API_KEY"], timeout=60)
weather_tool = Tool(
type="function",
name="get_weather",
description="Retorna a temperatura atual para uma cidade brasileira.",
parameters={
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
)
provider.configure_tools([weather_tool], tool_choice="required")
def get_weather(args: dict[str, str]) -> str:
city = args["city"]
return f"{city}, 30 graus."
provider.register_tool("get_weather", get_weather)
print(provider.send_message("Qual a temperatura de hoje em São Paulo?"))
Internamente o provider:
- Executa múltiplas rodadas de tool calling até chegar no texto final — o mesmo fluxo usado nos testes
test_demo_tool_call_api_forcedetest_openai_provider_hooks_with_tools. - Gerencia
tool_choice, IDs de chamadas e payloadsfunction_call_output/custom_tool_call_output, montando oinput_overridecorreto para cada rodada, como coberto emtests/test_openai_tool_calling.py. - Expõe hooks (
before_tool_call,after_tool_call,tool_error, etc.) que você pode monitorar para métricas e logs. - Funciona tanto com chamadas síncronas (
send_message) quanto streaming (send_stream) sem alterar o código dos handlers.
Passo a passo resumido
- Configure o provider com
set_api_key. - Descreva as ferramentas com
forgebase.Toolemconfigure_tools. - Registre cada handler com
register_tool. - Dispare
send_message(ousend_stream) — o provider executa as ferramentas e devolve apenas o texto final. - Opcional: adicione hooks (
register_hook) para inspecionarbefore_tool_call,after_tool_callou erros.
Para validar fluxos sem acessar a API real, use LLMOpenAIClient com uma resposta fake
(response=<objeto compatível>) ou rode python -m llm_client.demo_tool_call_api que inclui
um modo offline utilizando as mesmas rotinas de orquestração (vide tests/test_demo_tool_call_api.py).
Tool calling com streaming
import os
from dotenv import load_dotenv
from forgebase import OpenAIProvider, Tool
load_dotenv()
provider = OpenAIProvider()
provider.set_api_key(os.environ["OPENAI_API_KEY"])
tool = Tool(
type="function",
name="summarize_numbers",
parameters={"type": "object", "properties": {"nums": {"type": "array", "items": {"type": "number"}}}},
)
provider.configure_tools([tool], tool_choice="auto")
provider.register_tool("summarize_numbers", lambda args: sum(args.get("nums", [])))
for chunk in provider.send_stream("Considere os números 2, 4, 6 e mostre a soma."):
print(chunk, end="", flush=True)
print()
# Verifique tests/test_openai_tool_calling.py::test_streaming_tool_call_flow
# para um cenário completo de múltiplas rodadas no modo streaming.
Hooks de eventos
Tanto o LLMOpenAIClient quanto o OpenAIProvider expõem um sistema simples de
hooks para instrumentar o fluxo:
from forgebase import LLMOpenAIClient, OpenAIProvider
client = LLMOpenAIClient(api_key=os.environ["OPENAI_API_KEY"])
client.register_hook("before_request", lambda ctx: print("▶", ctx["prompt"]))
client.register_hook("after_response", lambda ctx: print("◀", ctx.get("response")))
provider = OpenAIProvider(client=client)
provider.register_hook("before_tool_call", lambda ctx: print("tool", ctx["tool"]))
provider.register_hook("after_tool_call", lambda ctx: print("tool result", ctx["result"]))
Eventos disponíveis:
before_request,after_response,on_error,on_cache_hit(cliente LLM)before_send,after_send,on_error,before_tool_call,after_tool_call,tool_error,cache_hit(provider)
Demo completo
O projeto inclui uma demo mais abrangente que cobre respostas diretas, streaming, (tool calling) e replays offline:
PYTHONPATH=shared/src:apps/llm_client/src python -m llm_client.example_full_usage
No Windows (PowerShell):
$env:PYTHONPATH = "shared/src;apps/llm_client/src"
python -m llm_client.example_full_usage
O arquivo apps/llm_client/src/llm_client/example_full_usage.py comenta cada etapa
(passos para configurar OPENAI_API_KEY, habilitar tool calling real com
DEMO_TOOL_CALLING=1, e como funciona o replay offline).
Configuração do ambiente
Opção 1 – Poetry (recomendada)
O pyproject.toml já descreve todos os pacotes via path. Basta executar na raiz:
poetry install
poetry run pytest -q
poetry run python -m llm_client.example_full_usage
O Poetry cria e gerencia o ambiente virtual automaticamente; não é necessário ajustar o
PYTHONPATH manualmente.
Opção 2 – pip + requirements
Se preferir pip, gere um virtualenv e instale as dependências de desenvolvimento com
o arquivo requirements-dev.txt gerado a partir do pyproject:
python -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
python -m pip install -r requirements-dev.txt
O arquivo pode ser sincronizado com o pyproject.toml executando python requirements-dev.py.
Depois disso, exporte PYTHONPATH=shared/src:framework/src:apps/llm_client/src:cli/src (ou
use python -m para os módulos) e rode pytest -q normalmente.
Desenvolvimento local
- Testes:
pytest -q(oupoetry run pytest -q). - Linters:
ruff check .emypy --config-file mypy.ini(compoetry runse estiver usando Poetry). - Build:
python -m buildgera wheel/sdist para publicar em TestPyPI/PyPI.
Onde continuar
docs/api/openai_responses.md: detalhes da Responses API e eventos de streaming.docs/api/openai_responses_tool_calling.md: guia de tool calling, payloads e replays.docs/api/streaming_tools_guide.md: guia extensivo de streaming + tools (SSE, timeouts, follow‑ups).docs/architecture/forgebase-architecture.md: visão completa da arquitetura e fluxos.docs/release-guide.md: processo recomendado de versionamento e publicação.docs/testing-strategy.md: abordagem de testes e boas práticas.docs/configuration.md: variáveis de ambiente e diretórios importantes.docs/providers/adding-new-provider.md: instruções para suportar novos LLMs.docs/cli/usage.md: comandos expostos pela CLI.docs/CONTRIBUTING.md: convenções de contribuição.docs/adr/README.md: decisões arquiteturais registradas.docs/tech-debts/TD-001-Robustez-Tool-Calling-Responses.md: backlog de melhorias planejadas.
Sinta-se à vontade para abrir issues ou PRs com sugestões e correções.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file forgebase-0.2.12.tar.gz.
File metadata
- Download URL: forgebase-0.2.12.tar.gz
- Upload date:
- Size: 44.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.8.2 CPython/3.12.3 Linux/6.6.87.2-microsoft-standard-WSL2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d8f58824d33fe7c7929e4c0f1ffed7a7258543fa252ad75c73a0a481b95d7f97
|
|
| MD5 |
27870a37c9a26bc9a77cc8ea108c7993
|
|
| BLAKE2b-256 |
6e76c215105a050c181cbc4b22ddbeafa17199432ac3ee0fe874b355a1ddb8e7
|
File details
Details for the file forgebase-0.2.12-py3-none-any.whl.
File metadata
- Download URL: forgebase-0.2.12-py3-none-any.whl
- Upload date:
- Size: 47.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.8.2 CPython/3.12.3 Linux/6.6.87.2-microsoft-standard-WSL2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e91e6fb0bdfc830cf1af0cba8ee3dde408af08bc0ec9824c8ea2a668c9ebb775
|
|
| MD5 |
f7a3f450d7acd92e4c40538f4a0333f8
|
|
| BLAKE2b-256 |
3a1a01737aa7f765a98cadccd7720546c053acea3865de33d8622cf287f5abad
|