Backend de agente LLM multi-provider para atendimento digital. Roteia requisições entre OpenAI, OpenRouter e Ollama; executa toolpacks Python; recupera contexto de uma base de conhecimento vetorial (RAG); transmite respostas via SSE; e expõe o conjunto completo de ferramentas por um servidor MCP opcional.
- Screenshots
- Funcionalidades
- Arquitetura
- Pré-requisitos
- Início Rápido
- Makefile
- Stack de Desenvolvimento
- Referência de Configuração
- Base de Conhecimento e RAG
- Modelo de Execução de Ferramentas
- API HTTP
- Integração MCP
- Observabilidade
- Exemplos Incluídos
- Testes
- Solução de Problemas
- Segurança
| Saudação e cardápio | Confirmação e pedido |
|---|---|
![]() |
![]() |
| Visão geral HTTP e Providers LLM | Ferramentas, RAG e Recursos |
|---|---|
![]() |
![]() |
| Categoria | Detalhe |
|---|---|
| Roteamento multi-provider | OpenAI, OpenRouter, Ollama: lista de prioridade, fixação de sessão, balanceamento de carga |
| Despacho por capacidade | Cada provider declara text, image, audio, embedder; requisições são roteadas para um provider capaz |
| Loop de tool-calling | Loop iterativo do agente com limite configurável de iterações; suporta function-calling nativo e fallback JSON |
| Toolpacks Python | Ferramentas de negócio definidas como manifest.json + main.py; carregadas no startup, sem recompilação Go |
| Pool persistente de workers Python | Subprocessos Python reutilizáveis (protocolo JSON-Lines) substituem a criação de processo por chamada |
| RAG com score de relevância | Recuperação híbrida (densa + esparsa) do Qdrant; cada trecho injetado exibe [relevance: X.XX]; reranking por cross-encoder opcional |
| Ingestão dinâmica de conhecimento | POST /knowledge/ingest aceita texto JSON ou upload de arquivo (.md, .txt, .csv); fragmenta, valida dimensão de embedding e indexa no Qdrant sem reiniciar |
| Streaming SSE | Streaming token a token com eventos tool_start/tool_end e frame final de resumo |
| Servidor MCP | Servidor Model Context Protocol opcional expondo o mesmo conjunto de toolpacks |
| Retry com backoff | Erros transitórios de provider (429, 503, rate-limit) são reprocessados com backoff exponencial |
| Monitoramento de saúde do provider | Contadores de erro por provider expostos em /health; probes /health/live e /health/ready para Kubernetes |
| Cache de resposta e embedding | Caches LRU em memória com TTL configurável |
| Cache de compressão de histórico | Histórico de conversa comprimido indexado por sessão + comprimento; evita contagem redundante de tokens |
| Deduplicação singleflight | Requisições concorrentes idênticas (mesma sessão + mesmo input) compartilham uma única chamada de inferência |
| OpenTelemetry | Traces (OTLP/gRPC), métricas Prometheus, logs estruturados |
| Entrada multimodal | Anexos de texto, imagem, áudio e documento (PDF) |
| CORS e rate limiting | Middleware CORS configurável; rate limiter por IP com token-bucket |
| Watch de configuração | Flag --watch-config recarrega o arquivo de configuração sem reiniciar o processo |
cmd/api/
main.go Bootstrap: config, logger, telemetria, HTTP, MCP, graceful shutdown
agent.go Registro de adapters LLM e construção do Agent
http.go Router Gin; middlewares (CORS, rate limit, request-ID, logging)
mcp.go Inicialização do servidor MCP com auth Bearer opcional
rag.go Inicialização do RAG engine (Qdrant + embedder)
ratelimit.go Rate limiter por IP (token-bucket)
telemetry.go Inicialização do OpenTelemetry (traces + métricas)
internal/
agent/
agent.go Struct Agent; singleflight; saúde do provider; cache de capacidades
run.go Loop de provider; retry com backoff exponencial; inferência
routing.go Seleção de provider; filtro de capacidade (cacheado); balanceamento
context_manager.go Compressão de histórico; cache de template (SHA-1); orçamento de tokens
message_builder.go Montagem de mensagens; RAG com score; compressão cacheada por sessão
knowledge_ingestor.go Ingestão dinâmica de texto no Qdrant; validação de dimensão de embedding
tool_caller.go Struct ToolCaller; loop Execute; métodos de configuração
tool_caller_native.go Passo de function-call nativo
tool_caller_fallback.go Passo de fallback JSON; recuperação de tool-call a partir de texto puro
tool_caller_exec.go Execução de tool; validação de argumentos; log de campos ausentes
tool_caller_format.go Prompts de schema; contexto de roteamento; limpeza de texto
constants.go Constantes nomeadas para todos os limites e atrasos padrão
document_identifier.go Classificação de tipo de documento via LLM
media.go / audio.go Processamento de anexos multimodais
config/ Leitura e normalização da configuração (YAML, TOML, JSON)
handlers/ Handlers HTTP; payload de saúde; ingestão de conhecimento
streaming/ Streamer SSE; eventos tool_start/tool_end durante execução
tools/ Descoberta de toolpacks, validação de manifestos, runtime Python
pool.go Pool persistente de workers Python (ProcessPool)
runtime_python.go Execução pool-first com fallback para subprocesso legado
knowledge/ Carregamento da base de conhecimento, chunking, indexação RAG
openai/ Adapter do provider OpenAI (compatível com API OpenAI)
openrouter/ Adapter do provider OpenRouter
ollama/ Adapter do provider Ollama
port/ Contratos de interface (ConfigProvider, LLMProviderConfig, …)
shared/ Helpers compartilhados entre providers
pkg/
telemetry/ Inicialização OpenTelemetry e métricas HTTP
oauth2tkm/ Gerenciador de token OAuth2
viper/ Wrapper Viper para leitura de configuração
examples/
tools/ Toolpacks de exemplo: caso de uso pizzaria
create_order/ manifest.json + main.py
finish/
get_menu/
get_order_status/
runtime.py Helpers de runtime compartilhados
_pool_worker.py Worker persistente (protocolo JSON-Lines)
knowledge/ Base de conhecimento de exemplo: pizzaria
kb_pizzaria/ Arquivos Markdown com cardápio, fluxo, pagamentos, etc.
knowledge.json Índice de chunks para o RAG
Cliente
│ POST /chat ou /chat/stream
▼
Handler HTTP (rate limit → CORS → request-ID → logging)
│ valida → constrói agent.Request
▼
Agent.Run (singleflight na chave sessão+input)
│ buildMessages: system prompt + histórico comprimido + contexto RAG
▼
Seleção de provider (filtro de capacidade → prioridade → balanceador)
│ loop de retry: attemptInference → retryWithBackoff(maxRetries=2, baseDelay=500ms)
▼
Inferência LLM (OpenAI / OpenRouter / Ollama)
│ tool-call nativo OU tool-call fallback JSON
▼
Execução de tool (loop ToolCaller.Execute, máx 6 iterações)
│ Go despacha para pool de workers Python → _pool_worker.py executa main.py
▼
Montagem da resposta → stream SSE / resposta JSON
│ recordProviderHealth
▼
Cliente
- Go
1.22+ - Python
3.xdisponível comopython3(ou definaTOOLPACKS_PYTHON_BIN) - Qdrant acessível se
rag.enabled = true - Ollama em execução se usado como provider ou embedder
- Um arquivo de configuração válido em
configs/(suporta YAML, TOML ou JSON; o formato é detectado automaticamente pela extensão do arquivo)
# 1. Copie o config de exemplo e preencha suas chaves / caminhos
cp configs/config.example.yml configs/config.yml
# 2. (Opcional) Suba a stack de desenvolvimento: Qdrant, Jaeger, Prometheus, Grafana
docker compose -f .devcontainer/docker-compose.dev.yml up -d
# 3. Execute o agente
go run ./cmd/api -config ./configs/config.yml
# 4. (Opcional) Habilite o watch automático do arquivo de configuração
go run ./cmd/api -config ./configs/config.yml --watch-configFlags disponíveis:
| Flag | Padrão | Descrição |
|---|---|---|
-config |
config.yaml |
Caminho para o arquivo de configuração (YAML, TOML ou JSON) |
--watch-config |
false |
Recarrega o config automaticamente ao detectar alterações no arquivo |
Variáveis de ambiente:
| Variável | Finalidade |
|---|---|
TOOLPACKS_PYTHON_BIN |
Sobrescreve o caminho do binário Python (padrão: python3) |
make build # Constrói a imagem Docker (usa IMAGE_NAME e VERSION)
make deploy # Publica e executa via scripts/sh/deploy.sh
make test # Executa a suíte de testes com detector de corrida
make swagger # Gera documentação Swagger
make profiler # Inicia o profiler de CPU/memóriaVariáveis Makefile:
| Variável | Padrão | Descrição |
|---|---|---|
IMAGE_NAME |
nexus_agent |
Nome da imagem Docker |
VERSION |
1.0.0 |
Tag de versão da imagem |
O docker-compose.dev.yml sobe:
| Serviço | URL padrão |
|---|---|
| Agent API | http://localhost:3000 |
| Exportador de métricas Prometheus | http://localhost:9464/metrics |
| UI Prometheus | http://localhost:9091 |
| UI Grafana | http://localhost:3001 |
| UI Jaeger | http://localhost:16686 |
| Qdrant | http://localhost:6333 |
O arquivo de configuração único fica em configs/ e é passado via flag -config. A aplicação suporta os formatos YAML (.yml/.yaml), TOML (.toml) e JSON (.json) o formato é detectado automaticamente pela extensão do arquivo. Todos os caminhos relativos dentro do arquivo são resolvidos a partir do diretório de trabalho no startup.
env: "dev" # dev | prod
log_level: "debug" # debug | info | warn | error[telemetry]
enabled = true
service_name = "nexus-agent"
service_version = "1.0.0"
environment = "dev"
otlp_endpoint = "localhost:4317" # endpoint OTLP/gRPC
otlp_insecure = true
metrics_host = "0.0.0.0"
metrics_port = 9464
metrics_path = "/metrics"[server]
port = 3000
read_timeout = "30s"
write_timeout = "30s" # deve ser >= request_timeout
idle_timeout = "60s"
request_timeout = "120s"Cada seção de provider compartilha um conjunto comum de campos:
[llm_providers.ollama]
base_url = "http://127.0.0.1:11434"
model = "qwen3:4b"
embedding_model = "mxbai-embed-large:335m"
system_prompt = "./prompts/system.txt"
schema_template = "./schemas/ollama_schema.json"
tool_prompt = "./prompts/tool.txt" # instrução de tool-call injetada no system prompt
tool_router_prompt = "./prompts/tool_router.txt" # usado ao rotear para a tool correta
document_identifier_prompt = "./prompts/doc_id.txt"
user_prompt_prefix = ""
native_tool_caller = true # usa a API de function-calling nativa do provider
temperature = 0.35
max_tokens = 768
num_ctx = 4096 # janela de contexto específica do Ollama
num_predict = 768 # limite de tokens específico do Ollama
top_k = 20
top_p = 0.9
repeat_penalty = 1.12
capabilities = ["text", "embedder"]
[llm_providers.openai]
api_key = "<OPENAI_API_KEY>"
base_url = "https://api.openai.com/v1"
model = "gpt-4o-mini"
temperature = 0.6
max_tokens = 1024
system_prompt = "./prompts/system.txt"
native_tool_caller = false
capabilities = ["text", "audio", "image"]
[llm_providers.openrouter]
api_key = "<OPENROUTER_API_KEY>"
base_url = "https://openrouter.ai/api/v1"
site_url = "https://seu-site.exemplo"
app_name = "nexus-agent"
model = "openai/gpt-4o-mini"
temperature = 0.6
max_tokens = 1024
system_prompt = "./prompts/system.txt"
native_tool_caller = false
capabilities = ["text", "audio", "image"]Capacidades:
| Valor | Significado |
|---|---|
text |
Geração de texto geral |
image |
Visão / análise de imagem |
audio |
Transcrição de fala / áudio |
embedder |
Geração de embeddings (obrigatório para RAG) |
[llm_routing]
priority = ["openrouter", "openai", "ollama"] # preferência decrescente
default_embedder = "ollama" # provider usado para embeddings do RAG
pinning_session = true # fixa cada sessão no primeiro provider que respondeu com sucesso
[llm_routing.load_balancing]
enabled = false
mode = "both" # request | session | both
timeout = "10s" # troca de provider após este tempo sem resposta
sessions = 1000 # máximo de sessões por providerRegras:
pinning_session = true: uma vez que uma sessão escolhe um provider, ela permanece nele.load_balancing.timeoutdeve ser menor queserver.request_timeoutquando o balanceamento está ativo.
[cache]
response_ttl_interval = "5m"
response_cleanup_interval = "1m"
embedding_ttl_interval = "24h"
embedding_cleanup_interval = "30m"
max_response = 1000[context]
max_context_tokens = 3000 # orçamento de tokens para o histórico comprimido injetado no prompt
max_history_turns = 4 # turnos de conversa mantidos antes da compressão
max_tool_iter = 6 # máximo de iterações de tool-calling por requisição[mcp]
addr = ":8090"
enabled = false
token = "" # se preenchido, habilita autenticação Bearer no servidor MCP
request_timeout = "60s"[tools]
toolpacks_dir = "./examples/tools" # diretório raiz dos toolpacks Python[rag]
enabled = true
top_k = 6
min_score = 0.35
knowledge_path = "./examples/knowledge/knowledge.json" # opcional: deixe vazio para iniciar sem conhecimento estático
retrieval_mode = "hybrid" # dense | sparse | hybrid
force_always = false # sempre injeta contexto RAG independente do score de relevância
embed_workers = 4
# Reranking por cross-encoder (opcional)
reranker_url = ""
reranker_path = "/v1/rerank"
reranker_api_key = ""
reranker_model = "BAAI/bge-reranker-v2-m3"
reranker_candidates = 14
reranker_timeout = "15s"
# Armazenamento vetorial Qdrant
qdrant_url = "http://127.0.0.1:6333"
qdrant_api_key = ""
qdrant_collection = "knowledge"
qdrant_reindex_on_boot = trueA base de conhecimento é um arquivo JSON apontado por rag.knowledge_path. Cada entrada descreve um chunk de conhecimento:
[
{
"id": "kb_000",
"category": "assistant_persona",
"files_path": "./knowledge/persona",
"role": "system"
},
{
"id": "kb_001",
"category": "rules",
"content": "Forneça previsões apenas com base nos dados retornados pela tool de clima.",
"role": "system"
},
{
"id": "kb_002",
"category": "faq",
"files_path": "./knowledge/faq",
"role": "user"
}
]Campos:
| Campo | Descrição |
|---|---|
id |
Identificador único |
category |
Rótulo do grupo lógico |
content |
Conteúdo de texto inline |
files |
Lista de caminhos de arquivo a carregar |
files_path |
Diretório; todos os arquivos suportados dentro são carregados |
role |
system ou user (ausente = user) |
Comportamento do role:
system: carregado uma vez no startup e anexado ao system prompt como contexto fixo.user: fragmentado, embedado e indexado no Qdrant; recuperado dinamicamente por requisição com base em similaridade semântica.
Extensões de arquivo aceitas para chunking: .md, .markdown, .txt, .csv
rag.knowledge_pathé opcional. Se estiver vazio ou ausente, o agente inicializa sem conhecimento estático e a coleção Qdrant é criada vazia. O endpointPOST /knowledge/ingestpode ser usado para popular o índice em runtime.
Textos longos são divididos em chunks de tamanho controlado. Quando um parágrafo é quebrado em múltiplos chunks de sentenças, a última sentença do chunk anterior é repetida no início do próximo (defSentenceOverlap = 1). Isso preserva continuidade semântica entre chunks adjacentes, melhorando a qualidade da recuperação RAG.
Quando rag.enabled = true e um store vetorial está configurado, o agente busca documentos via SimilaritySearch e prefixa cada trecho recuperado com seu score de similaridade:
[relevance: 0.87] Consultas de segunda a sexta das 08h às 18h.
[relevance: 0.74] Agendamentos podem ser feitos pelo app ou presencialmente.
Trechos com score abaixo de rag.min_score são descartados. Se nenhum trecho passar pelo filtro e rag.force_always = true, a busca é repetida sem threshold de score.
O endpoint POST /knowledge/ingest indexa conteúdo diretamente no Qdrant em runtime, sem necessidade de reiniciar o agente. Aceita dois formatos:
application/json: campotextcom o conteúdo em texto planomultipart/form-data: campofilecom upload de arquivo (.md,.txt,.markdown,.csv; limite 10 MB)
Em ambos os casos o conteúdo é fragmentado com a mesma lógica da carga estática (overlap de sentenças para texto, agrupamento de linhas para CSV). Antes da indexação, a dimensão do embedding é validada contra o tamanho esperado, evitando inconsistências na coleção Qdrant.
<toolpacks_dir>/
_pool_worker.py Processo worker persistente (gerenciado pelo Go)
runtime.py Helpers de runtime compartilhados (opcional)
<tool>/
manifest.json
main.py
.env.example Variáveis de ambiente esperadas pela tool
{
"name": "orders_checkout",
"description": "Registra um pedido confirmado",
"runtime": "python",
"entrypoint": "main.py",
"timeout": "5s",
"schema": {
"type": "object",
"properties": {
"nome": { "type": "string", "description": "Nome do cliente" },
"pedido": { "type": "string", "description": "Resumo do pedido confirmado" }
},
"required": ["nome", "pedido"],
"additionalProperties": false
}
}Em vez de criar um novo processo Python a cada chamada de tool, o Go mantém um pool de processos _pool_worker.py persistentes (tamanho padrão = min(NumCPU, 8)).
Protocolo (JSON delimitado por newline):
Go → stdin Python: {"id":"<nano>","script":"<caminho-abs>","cwd":"<dir>","input":"<json>"}
stdout Python → Go: {"id":"<nano>","ok":true,"output":"<json>"}
{"id":"<nano>","ok":false,"error":"<mensagem>"}
Cada worker serializa chamadas com um mutex. Se um worker morrer, é substituído de forma transparente antes da próxima requisição. Se o pool estiver indisponível, o agente faz fallback para o subprocesso legado por chamada.
Tool-calling nativo (native_tool_caller = true): a API de function-calling do provider é utilizada. Chamadas de tool estruturadas chegam em ContentChoice.ToolCalls.
Fallback JSON (native_tool_caller = false): o agente injeta um prompt de schema na mensagem de sistema. O modelo responde com um envelope JSON que o agente analisa e despacha:
{"tool_call": {"name": "get_weather_forecast", "arguments": {"location": "Belem/PA"}}}- Monta o prompt completo (system + histórico comprimido + contexto RAG + mensagem atual).
- Chama o LLM.
- Analisa a resposta em busca de uma tool call (nativo ou fallback JSON).
- Executa a tool via pool de workers.
- Anexa a mensagem do assistente + resultado da tool ao histórico de trabalho.
- Repete a partir do passo 2, até
context.max_tool_iter(padrão 6) iterações. - Retorna a resposta textual final.
Erros transitórios de providers: HTTP 429, 503, 502, mensagens contendo rate limit, overloaded ou too many requests acionam até 2 retentativas com backoff exponencial a partir de 500 ms (delay = 500ms × 2^tentativa).
| Toolpack | Descrição |
|---|---|
create_order |
Registra um novo pedido |
finish |
Sinaliza o encerramento da conversa |
get_menu |
Retorna o cardápio disponível |
get_order_status |
Consulta o status de um pedido existente |
Chat síncrono: aguarda a resposta completa do agente.
Requisição:
{
"history": [
{
"input": "{\"protocol\":\"site\",\"name\":\"Ana\"}",
"output": "Olá! Sou a Nina, sua assistente de atendimento."
}
],
"text": "Qual é a previsão do tempo para Belém/PA?",
"media": {
"type": "image",
"data": "<base64>",
"mime_type": "image/jpeg"
}
}history: obrigatório; ao menos um turno.textoumedia: ao menos um é obrigatório.media.type-image,audiooudocument.
Resposta:
{
"output": "Previsão para Belém/PA: alta chance de chuva hoje.",
"tool_calls": [
{
"name": "get_weather_forecast",
"input": "{\"location\":\"Belem/PA\"}",
"output": "{\"resolved_location\":{\"label\":\"Belem/PA\"},\"forecast\":{...}}",
"duration_ms": 210,
"success": true
}
],
"rag_used": false,
"cache_hit": false,
"latency_ms": 950,
"tokens_used": 420
}Headers de resposta:
| Header | Descrição |
|---|---|
X-RAG-Used |
true se contexto RAG foi injetado |
X-Cache-Hit |
true se a resposta veio do cache |
X-Latency-Ms |
Latência de ponta a ponta em milissegundos |
X-Request-ID |
UUID único gerado por requisição (útil para rastreamento em logs) |
Chat em streaming via Server-Sent Events (Content-Type: text/event-stream).
Cada frame SSE é um objeto JSON prefixado com data: :
Frame de token (emitido por token transmitido):
{"token": "Previsão", "done": false}Frame de início de tool (emitido antes de cada execução de tool):
{"type": "tool_start", "tool": "get_weather_forecast", "done": false}Frame de fim de tool (emitido após cada execução de tool):
{"type": "tool_end", "tool": "get_weather_forecast", "success": true, "done": false}Frame final:
{"token": "", "done": true, "tool_calls": [...], "latency_ms": 1234}Frame de erro:
{"error": "provider indisponível", "done": true, "tool_calls": [...]}Indexa conteúdo no Qdrant em runtime sem reiniciar o agente. Requer rag.enabled = true.
Aceita dois formatos detectados automaticamente pelo Content-Type:
{
"text": "Atendemos de segunda a sexta das 08h às 18h. Sábados das 09h às 13h.",
"category": "horarios",
"source": "site_institucional"
}| Campo | Obrigatório | Descrição |
|---|---|---|
text |
sim | Texto a ser fragmentado e indexado |
category |
não | Rótulo do grupo lógico (metadado no Qdrant) |
source |
não | Origem do documento (metadado no Qdrant) |
curl -X POST http://localhost:3000/knowledge/ingest \
-F "file=@base_conhecimento.md" \
-F "category=faq"| Campo | Obrigatório | Descrição |
|---|---|---|
file |
sim | Arquivo a ser fragmentado e indexado |
category |
não | Rótulo do grupo lógico (metadado no Qdrant) |
source |
não | Origem do documento; padrão: nome do arquivo |
Extensões aceitas: .md, .markdown, .txt, .csv limite de 10 MB por upload.
Resposta (200):
{
"chunks_indexed": 3,
"message": "3 chunk(s) indexed successfully"
}Erros:
| Código | Causa |
|---|---|
400 |
text / file ausente ou vazio; extensão de arquivo não suportada |
422 |
Dimensão do embedding não corresponde à coleção Qdrant existente |
500 |
Falha na indexação (Qdrant inacessível, embedder indisponível, etc.) |
Retorna um snapshot JSON do estado da aplicação:
{
"status": "ok",
"model": "openai/gpt-4o-mini",
"ollama_url": "http://127.0.0.1:11434",
"models": {
"ollama": "qwen3:4b",
"openai": "gpt-4o-mini",
"openrouter": "openai/gpt-4o-mini"
},
"llm_routing": {
"default": "openrouter",
"default_embedder": "ollama",
"priority": ["openrouter", "openai", "ollama"],
"capabilities": {"openrouter": ["text","audio","image"]},
"pinning_session": true,
"load_balancing": {"enabled": false, "mode": "both", "timeout": "5m0s", "sessions": 1000}
},
"uptime_seconds": 3600,
"cache_stats": {
"response": {"hits": 12, "misses": 38, "size": 50},
"embedding": {"hits": 100, "misses": 5, "size": 105}
},
"rag_chunks_loaded": 142,
"mcp_enabled": false,
"provider_health": {
"openrouter": {
"total_requests": 50,
"errors": 2,
"error_rate_pct": "4.0%",
"last_error": "rate limit exceeded",
"last_error_at": "2024-01-15T10:23:00Z"
}
}
}Probe de liveness confirma que o processo está em execução. Não avalia o estado dos providers. Retorna sempre 200 enquanto o processo estiver vivo.
{"status": "ok"}Probe de readiness indica se o agente está apto a servir requisições. Retorna 503 quando todos os providers configurados foram tentados e nenhum completou uma requisição com sucesso.
Resposta 200 (pronto):
{"status": "ok"}Resposta 503 (degradado):
{"status": "degraded", "reason": "all LLM providers are failing"}Configuração recomendada no Kubernetes:
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10Um provider com zero requisições é tratado como saudável (estado desconhecido). O
503só é emitido quando todos os providers foram efetivamente tentados e falharam em 100% das tentativas.
Retorna um snapshot JSON de métricas de runtime para troubleshooting operacional (contagens de requisição, histogramas de latência, uso de tools, taxas de cache hit, etc.).
Quando mcp.enabled = true, o agente inicia um servidor Model Context Protocol em mcp.addr (padrão :8090). O mesmo conjunto de toolpacks carregado no startup é exposto para qualquer cliente MCP compatível. Os schemas das tools são derivados diretamente dos arquivos manifest.json.
Se mcp.token estiver preenchido, o servidor exige autenticação Bearer no header Authorization. Requisições sem token ou com token incorreto são rejeitadas com 401.
Traces distribuídos são exportados via OTLP/gRPC para o endpoint configurado em telemetry.otlp_endpoint. Cada requisição HTTP, chamada LLM, execução de tool e recuperação RAG é instrumentada com spans.
Métricas Prometheus são expostas em telemetry.metrics_host:telemetry.metrics_port/telemetry.metrics_path.
Categorias de métricas:
- Contagem, latência e erros de requisições HTTP
- Contagem, latência e uso de tokens de inferência LLM
- Contagem, latência e erros de execução de tools (por nome de tool)
- Taxas de cache hit/miss (resposta e embedding)
- Contagens de fallback de provider e sessões ativas por provider
- Contagem e latência de recuperação RAG e ingestão de conhecimento
- Métricas de runtime Go (goroutines, heap)
Os dashboards Grafana pré-configurados ficam em .docker/grafana/dashboards/ e são provisionados automaticamente ao subir a stack de desenvolvimento.
Logs JSON estruturados no log_level configurado. Tracebacks Python dos subprocessos são automaticamente encaminhados para o stderr do processo Go pelo pool de workers.
O diretório examples/ contém um caso de uso completo: Pizzaria Bella Napoli que pode ser usado como ponto de partida para novos domínios.
examples/
tools/
create_order/ Registra pedidos (grava em arquivo local)
finish/ Encerra a conversa
get_menu/ Retorna cardápio com preços
get_order_status/ Consulta status de pedido por ID
runtime.py Helpers compartilhados entre tools
_pool_worker.py Worker persistente
knowledge/
kb_pizzaria/
01_cardapio.md
02_fluxo_pedido.md
03_pagamentos.md
04_informacoes_fixas.md
05_promocoes.md
knowledge.json Índice de chunks para o RAG
O diretório playground/ contém integrações prontas para testar o agente em plataformas de mensageria:
| Integração | Localização | Descrição |
|---|---|---|
| Telegram | playground/telegram/ |
Bot Telegram completo usando a biblioteca go-telegram/bot |
playground/whatsapp/ |
Integração via whatsmeow |
|
| Cenários de chat | playground/chat_scenarios/ |
Runner de cenários de conversa para testes automatizados |
# Executar todos os testes
go test ./...
# Executar com detector de corrida
make test
# Executar um pacote específico
go test ./internal/agent/...
go test ./internal/streaming/...
go test ./internal/handlers/...Tool calls não acontecem modelo descreve uma ação em vez de executar:
- Verifique
native_tool_callerpara o provider ativo. - Confirme que os caminhos de
tool_prompteschema_templateestão corretos e os arquivos existem. - Aumente
log_levelparadebuge inspecione o prompt completo enviado ao LLM.
RAG não injeta contexto:
- Confirme
rag.enabled = true. - Verifique a conectividade com o Qdrant (
qdrant_url). - Confirme que
llm_routing.default_embedderaponta para um provider que declara a capacidadeembedder. - Verifique
rag_chunks_loadedem/healthse for0, a indexação falhou no startup.
Requisições encerrando por timeout:
- Aumente
server.write_timeout(deve ser ≥server.request_timeout). - Se usar balanceamento de carga,
load_balancing.timeoutdeve ser menor queserver.request_timeout.
Tool Python falha ao iniciar:
- Confirme que
tools.toolpacks_direstá correto e contém_pool_worker.pyna raiz. - Valide cada
manifest.json(campos obrigatórios:name,description,runtime,entrypoint,schema). - Confirme que
python3(ouTOOLPACKS_PYTHON_BIN) está no PATH.
Tool Python falha durante a execução:
- O stderr do subprocesso Python é encaminhado para o log do processo Go.
- Confirme que a tool escreve JSON válido em
stdoute usastderrapenas para erros. - Verifique se as variáveis de ambiente exigidas pela tool estão definidas.
Respostas idênticas repetidas (cache hit inesperado):
- Verifique
cache_hitno corpo da resposta ou o headerX-Cache-Hit. - Ajuste
cache.response_ttl_intervalou varie o conteúdo do histórico para contornar o cache.
/health/ready retorna 503:
- Todos os providers configurados falharam em 100% das requisições desde o startup.
- Verifique
provider_healthem/healthpara identificar qual provider está com erro e qual é a mensagem (last_error). - Causas comuns: chave de API inválida, provider fora do ar, limite de taxa atingido sem recuperação.
- O probe volta a
200automaticamente assim que qualquer provider completar uma requisição com sucesso.
POST /knowledge/ingest retorna 422 (dimension mismatch):
- O modelo de embedding foi trocado após a criação da coleção Qdrant.
- Apague a coleção Qdrant e reinicie o agente (re-indexação ocorre no startup com
qdrant_reindex_on_boot = true).
POST /knowledge/ingest retorna 500:
- Verifique se
rag.enabled = truee o agente está conectado ao Qdrant. - Confirme que o provider embedder está disponível e declara a capacidade
embedder.
Alta taxa de erro em /health para um provider:
- O mapa
provider_healthmostraerror_rate_pctelast_errorpor provider. - A lógica de retry trata erros transitórios automaticamente (2 retentativas, backoff exponencial a partir de 500 ms).
- Erros persistentes indicam problema de configuração ou chave de API inválida.
- Use substituição de variáveis de ambiente ou um gerenciador de segredos em produção nunca commite o
config.ymlcom chaves reais. - O servidor MCP suporta autenticação Bearer via
mcp.token; habilite-a em ambientes expostos. - O pool de workers Python executa scripts do
toolpacks_dir; certifique-se de que esse diretório não seja gravável por usuários não confiáveis. - O middleware de rate limiting protege
/chate/chat/streamcontra abuso por IP.



