Convenções de Código e Padrões
Estas são as convenções aplicadas em todos os repositórios da Triadeflow. Claude Code deve seguir todas estas regras automaticamente, sem precisar ser lembrado.
Estrutura geral
Linguagens
- Python 3.11+ para todo backend, agentes, scripts
- TypeScript para frontends (admin panel, ops panel, dashboards de cliente)
- YAML para configurações de tenant
- Markdown para documentação
Naming
- Arquivos Python:
snake_case.py - Classes:
PascalCase - Funções e variáveis:
snake_case - Constantes:
UPPER_SNAKE_CASE - Pastas:
kebab-case(ex:agent-template,triadeflow-core) - Branches Git:
feat/nome-descritivo,fix/...,refactor/...,docs/...
Idioma
- Código, comentários, docstrings: inglês
- Documentação (
.md), prompts de agente, mensagens ao cliente: português brasileiro - Logs: inglês para operacionais, português para mensagens visíveis ao cliente final
- Commit messages: inglês, padrão Conventional Commits
Tipagem
Tipagem é obrigatória. Não há “Python dinâmico” nesta base.
# CORRETOdef process_message(tenant_id: str, message: IncomingMessage) -> AgentResponse: ...
# ERRADO - sem tiposdef process_message(tenant_id, message): ...Pydantic v2 para schemas
Todo dado que cruza fronteira (HTTP, fila, config, LLM input/output) é Pydantic.
from pydantic import BaseModel, Field
class TenantConfig(BaseModel): tenant_id: str = Field(..., min_length=3, max_length=64) agent_type: Literal["sdr", "vendas", "suporte", "agendamento"] modelo: ModelConfig messaging: MessagingConfig rag: RAGConfigType aliases para clareza
# Em types.py do coreTenantId = strConversationId = strAgentType = Literal["sdr", "vendas", "suporte", "agendamento", "recuperacao", "cobranca", "pos-venda"]Estrutura de pastas (padrão de cada agente)
agent-{tipo}/├── pyproject.toml├── Dockerfile├── docker-compose.yml├── Makefile├── .env.example├── README.md├── CLAUDE.md # aponta para triadeflow-docs├── src/│ ├── main.py # FastAPI entrypoint│ ├── agent.py # LangGraph workflow│ ├── state.py # Pydantic state│ ├── settings.py # Settings via env│ ├── prompts/│ │ ├── system.md # template Jinja2│ │ └── fragments/ # tom, regras, lgpd, handoff│ ├── tools/│ │ ├── _registry.py # registro de tools│ │ ├── consultar_kb.py│ │ ├── handoff_humano.py│ │ └── ...│ ├── flows/ # LangGraph nodes│ │ ├── classify.py│ │ ├── qualify.py│ │ └── handoff.py│ ├── handlers/│ │ └── webhook.py│ └── utils/├── tenants/│ ├── _defaults.yaml # padrão para todos os tenants│ └── {tenant_id}/│ ├── config.yaml│ └── kb/ # base de conhecimento (vetorizada)├── eval/│ ├── datasets/│ │ └── golden.json│ └── runners/├── scripts/│ ├── create_tenant.py│ ├── ingest_kb.py│ └── deploy.sh├── tests/└── deploy/ └── hetzner.ymlConvenções por arquivo
main.py
Entry point FastAPI. Apenas:
- Cria app
- Configura middlewares (Langfuse, CORS, error handler)
- Registra rotas (
/webhook,/health,/admin) - Nunca tem lógica de negócio
agent.py
Define o LangGraph workflow. Apenas:
- Constrói o grafo
- Adiciona nodes (cada um em arquivo separado em
flows/) - Define edges e conditional_edges
- Compila o grafo
state.py
AgentState Pydantic. Imutável-ish (use .copy(update={...})).
class AgentState(BaseModel): tenant_id: str conversation_id: str messages: list[Message] current_step: str extracted_data: dict[str, Any] metadata: dict[str, Any]settings.py
Settings via Pydantic Settings + env vars. Nunca leia os.environ direto.
from pydantic_settings import BaseSettings
class Settings(BaseSettings): anthropic_api_key: SecretStr redis_url: str qdrant_url: str langfuse_public_key: SecretStr langfuse_secret_key: SecretStr
class Config: env_file = ".env"Tools
Cada tool em arquivo próprio, decorada com @tool (LangChain) e @trace_tool (Langfuse):
from langchain_core.tools import toolfrom triadeflow_core.observability import trace_tool
@tool@trace_tooldef agendar_reuniao( tenant_id: str, lead_id: str, data_hora: str, duracao_min: int = 30,) -> AgendamentoResultado: """ Agenda uma reunião no calendário do tenant.
Args: tenant_id: ID do tenant lead_id: ID do lead no CRM data_hora: ISO 8601 com timezone (ex: "2026-05-15T14:00:00-03:00") duracao_min: Duração em minutos
Returns: AgendamentoResultado com sucesso, link e ID do evento """ # implementaçãoPrincípios de tool:
- Uma responsabilidade
- Idempotente quando possível
- Tipada (input e output)
- Logada (Langfuse via decorator)
- Tenant-aware (sempre recebe
tenant_id) - Erro tratado (retorna erro estruturado, nunca raise para o agente)
Async first
Toda I/O é async. Nunca requests.get() — sempre httpx.AsyncClient.
import httpx
async def fetch_lead(crm_url: str, lead_id: str) -> Lead: async with httpx.AsyncClient(timeout=10.0) as client: resp = await client.get(f"{crm_url}/leads/{lead_id}") resp.raise_for_status() return Lead.model_validate(resp.json())Exceção: scripts CLI utilitários podem ser síncronos.
Tratamento de erro
Hierarquia de exceções
# Em triadeflow_core/exceptions.pyclass TriadeflowError(Exception): """Base de todas exceções da plataforma"""
class ProviderError(TriadeflowError): """Falha em provider de mensageria"""
class CRMError(TriadeflowError): """Falha em integração com CRM"""
class TenantConfigError(TriadeflowError): """Configuração de tenant inválida ou ausente"""
class ToolExecutionError(TriadeflowError): """Falha na execução de uma tool"""Retry com backoff
Para integrações externas, sempre use retry com tenacity:
from tenacity import retry, stop_after_attempt, wait_exponential
@retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type(httpx.HTTPError),)async def call_external_api(...): ...Circuit breaker para integrações instáveis
Use circuitbreaker para CRMs e gateways. Após N falhas, parar de tentar por X minutos.
Logging
structlog em todos os módulos. Nunca print().
import structloglog = structlog.get_logger()
log.info("message_received", tenant_id=tid, conversation_id=cid)log.warning("rag_low_score", tenant_id=tid, score=0.45, query=query[:50])log.error("crm_failure", tenant_id=tid, error=str(e), exc_info=True)Sempre inclua tenant_id e conversation_id quando disponíveis.
Configuração de tenant
Cada tenant tem tenants/{tenant_id}/config.yaml que segue exatamente o schema Pydantic TenantConfig. Validar na inicialização e não em runtime.
tenant_id: usa-salusagent_type: sdr
agente: nome: "Assistente USA Salus" empresa: "USA Salus" contexto_negocio: | Empresa de saúde corporativa que oferece...
modelo: principal: claude-sonnet-4-20250514 fallback: claude-haiku-4-5-20251001 estruturadas: claude-haiku-4-5-20251001 # para tarefas simples temperatura: 0.7 max_tokens: 1024
messaging: provider: evolution instance_name: usa-salus-prod webhook_secret: ${USA_SALUS_WEBHOOK_SECRET}
crm: type: kommo base_url: https://usa.kommo.com api_token: ${USA_SALUS_KOMMO_TOKEN} pipeline_id: 12345
rag: collection: usa-salus top_k: 5 rerank: false
memory: redis_prefix: usa-salus short_term_size: 10 summarize_after: 10
# ... outras seçõesVariáveis de ambiente dentro de YAML são resolvidas pelo env_resolver do core na hora de carregar.
Prompt engineering
System prompts são arquivos .md com Jinja2:
{# prompts/system.md #}
# Identidade
Você é {{ agente.nome }}, assistente virtual da {{ agente.empresa }}.
# Sua empresa
{{ agente.contexto_negocio }}
# Seu objetivo
{{ agente.objetivo_principal }}
# Como você atua
{% include "fragments/tom.md" %}
# Suas ferramentas
{% include "fragments/tools.md" %}
# Regras inegociáveis
{% include "fragments/regras.md" %}{% include "fragments/lgpd.md" %}
# Handoff humano
{% include "fragments/handoff.md" %}Fragments reutilizáveis ficam em prompts/fragments/ e são compartilhados entre tenants do mesmo tipo de agente.
Prompt caching da Anthropic: marcar a parte fixa do system prompt como cache_control: ephemeral quando o prompt for > 1024 tokens.
Modelo selection (model router)
Use o model_router do core para escolher modelo dinamicamente baseado no tipo de tarefa:
from triadeflow_core.llm import model_router
# Conversa principalresponse = await model_router.invoke( messages=msgs, task_type="conversation", # → Sonnet tenant_id=tid,)
# Sub-tarefa estruturadaclassification = await model_router.invoke( messages=msgs, task_type="classification", # → Haiku tenant_id=tid,)Tipos de tarefa: classification, extraction, simple_response, conversation, creative, analysis.
Testes
Estrutura
tests/├── unit/ # módulos isolados├── integration/ # com Redis, Postgres reais├── e2e/ # webhook completo└── eval/ # contra dataset de qualidadePytest com markers
@pytest.mark.unitdef test_classify_intent(): ...
@pytest.mark.integrationasync def test_redis_memory(): ...Eval automatizado
Cada agente tem eval/datasets/golden.json com 50-100 casos rotulados. Rodar antes de promover qualquer mudança de prompt ou modelo.
Observabilidade
Toda chamada LLM é traceada
from triadeflow_core.observability import langfuse_handler
response = await llm.ainvoke( messages, config={ "callbacks": [langfuse_handler], "metadata": { "tenant_id": tid, "agent_type": "sdr", "conversation_id": cid, } })Tags semânticas obrigatórias
Toda chamada LLM no Langfuse precisa de:
tenant_id— quem é o cliente finalagent_type— qual tipo de agente (sdr, vendas, etc)conversation_id— para correlacionar mensagensflow_step— em qual passo do grafo está (classify, qualify, etc)
Annotation queue
Traces com score baixo do LLM-as-judge entram automaticamente na annotation queue. Revisar semanalmente como parte da rotina operacional.
Custos
Antes de mexer em modelo, simule
Não troque modelo de produção sem antes rodar a simulação de custo. Ver .claude/workflows/otimizar-custo.md.
Tática híbrida é padrão
Configure cada agente para usar Haiku em sub-tarefas estruturadas (classificação, extração, confirmação) e Sonnet apenas na conversa principal. Economia típica: 30-50%.
Prompt caching
System prompts longos (> 1024 tokens) devem usar prompt caching da Anthropic. Cache hit reduz custo da parte cacheada em 90%.
Segurança
Secrets
- Nunca comite secrets. Use
.env(gitignored) e${VAR}no YAML - Em produção, use Hetzner Vault ou Doppler
- API keys têm escopo mínimo necessário
LGPD
Todo agente tem fragmento prompts/fragments/lgpd.md que define:
- Consentimento explícito antes de coletar PII
- Direito ao esquecimento implementado
- Logs de mensagens com PII têm retenção limitada (90 dias máximo)
- Em saúde, tutela explícita do art. 11 LGPD
Webhooks
- Sempre validar HMAC signature
- Whitelist de IPs quando possível
- Rate limiting por IP
Git e PRs
Conventional Commits
feat: adiciona tool agendar_reuniao no agent-sdrfix: corrige timezone na conversão de Unix timestamprefactor(core): extrai model_router para módulo separadodocs(agents/sdr): atualiza spec com BANT customizadoperf(rag): reduz top_k padrão de 8 para 5test(eval): adiciona 20 casos golden para agent-suportePRs
- Título no padrão de commit
- Descrição com: o que muda, por quê, como testou
- Screenshots se afetar UI
- Checklist de impacto: arquitetura, custo, breaking change, docs
Branches
main— produção, protegidafeat/...,fix/...— desenvolvimentorelease/v1.2.0— preparação de release
Erros antipattern (não fazer)
from langchain import *
Sempre import explícito.
time.sleep() em código async
Use await asyncio.sleep().
Configuração hardcoded por cliente
Tudo de cliente vai em YAML, nunca em código.
Múltiplas LLM calls quando uma resolve
Otimize para latência e custo. Combine quando possível.
Catch genérico de Exception
# ERRADOtry: ... except Exception: pass
# CORRETOtry: ...except (httpx.TimeoutException, httpx.HTTPStatusError) as e: log.warning("crm_timeout", error=str(e)) raise CRMError(...)Strings de SQL/comando construídas com f-string
Sempre use parameterização ou ORMs.
Mock de LLM em testes via patch global
Use o mock_llm fixture do core que tem dataset estruturado.
Convenção de logs operacionais
Padrão para mensagens visíveis ao cliente final (em português):
- Saudação: tom natural, sem “Olá! Como posso ajudar?” automático
- Confirmação: “Pronto, registrei aqui” — natural, não robotizado
- Erro recuperável: “Tive um problema aqui, pode repetir o que você queria?”
- Erro grave: “Vou chamar um humano pra te atender” + tool de handoff
- Despedida: tom natural, sem clichês
Versão: 1.0 Aplica-se a: todos os repositórios da Triadeflow Última atualização: 2026-05-03