Arquitetura do Agente IA¶
O agente é um ReAct (Reasoning + Acting) construído sobre LangGraph com LangChain. Ele decide dinamicamente quando chamar OTIMUS, quando transferir para humano, e quando só conversar.
Stack¶
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.checkpoint.memory import MemorySaver
import psycopg
LLM¶
# MEDCENTER
llm = ChatOpenAI(model="gpt-4.1", api_key=OPENAI_API_KEY, temperature=0.7)
# CLINFETO
llm = ChatOpenAI(model="gpt-4.1-mini", api_key=OPENAI_API_KEY, temperature=0.7)
Escolha de modelo
gpt-4.1para prompts muito longos e/ou clínicas com muitas regras sutis (ex: Medcenter, 660+ linhas de prompt com preços, telefones, regras de convênio).gpt-4.1-minipara fluxos mais limpos e bem delimitados (ex: Clinfeto, triagem de exames).temperature=0.7nos dois — ok. Subir para1.0só se o agente parecer robótico; descer para0.3se estiver inventando dados.
Checkpointer (memória por sessão)¶
# agent.py (ambos os projetos)
try:
_conn = psycopg.connect(DATABASE_URL, autocommit=True)
checkpointer = PostgresSaver(conn=_conn)
checkpointer.setup()
logger.info("PostgresSaver conectado com sucesso")
except Exception as e:
logger.warning("Falha ao conectar PostgresSaver, usando MemorySaver: %s", e)
checkpointer = MemorySaver()
PostgresSaverpersiste o histórico de cadathread_idem tabelascheckpoint*. Cria-se ao primeiro.setup().thread_id=session_iddo WTS Chat. Cada atendimento WhatsApp é uma thread independente.- Fallback
MemorySaversó para dev local sem DB — em produção tem que ter PostgresSaver, senão reiniciar o container perde todo histórico.
Conexão única e autocommit=True
PostgresSaver precisa de conexão em autocommit. Criar uma conexão
no módulo (global), não por request. Reconexão automática está
embutida.
Construção do agente¶
TOOLS = [transferir_atendimento, consultar_horarios, agendar_consulta]
def build_agent():
return create_react_agent(
model=llm,
tools=TOOLS,
checkpointer=checkpointer,
prompt=SYSTEM_PROMPT,
)
agent_executor = build_agent()
create_react_agent retorna um Runnable compatível com
.invoke({"messages": [...]}, config={...}).
Invocação¶
# agent.py (MEDCENTER), simplificado
def run_agent(message: str, session_id: str, phone_number: str, contact_name: str) -> str:
# context vars (usadas nas tools)
_current_session_id.set(session_id)
_current_phone_number.set(phone_number)
_current_contact_name.set(contact_name)
now = _now_br()
full_message = (
f"{message}\n\n"
f"DATA DE HOJE: {now.strftime('%A')}, {now.strftime('%d-%m-%Y')}, as {now.strftime('%H:%M')}"
)
config = {"configurable": {"thread_id": session_id}}
result = agent_executor.invoke(
{"messages": [HumanMessage(content=full_message)]},
config=config,
)
# Telemetria de tokens
for m in result["messages"]:
if m.type == "ai" and getattr(m, "usage_metadata", None):
log_token_usage(
session_id=session_id,
provider="openai",
model=llm.model_name,
prompt_tokens=m.usage_metadata.get("input_tokens", 0),
completion_tokens=m.usage_metadata.get("output_tokens", 0),
)
ai_messages = [m for m in result["messages"] if m.type == "ai" and m.content]
return ai_messages[-1].content if ai_messages else "Desculpe, tive um problema. Pode repetir?"
Injeção de data/hora na mensagem¶
O LLM não tem clock interno. Injete data/hora atual em cada chamada:
Isso evita que o agente ofereça "amanhã" quando hoje é quinta e "amanhã" seria sexta mas ele acha que é terça.
Fluxo ReAct internamente¶
flowchart TD
IN[HumanMessage + SYSTEM_PROMPT + histórico] --> L[LLM raciocina]
L -->|sem tool| OUT[AIMessage texto]
L -->|tool call| T[Executa tool]
T -->|ToolMessage| L
L -->|loop até gerar texto final| OUT
Cada iteração conta como uma chamada gpt-4.1. Logar tokens agregado
(somando todas as mensagens ai do result["messages"]).
Tratamento de erro¶
try:
result = agent_executor.invoke(...)
except Exception as e:
log_error("agent_error", str(e), session_id, phone_number)
raise # deixa o workflow capturar e enviar fallback
O workflow ao capturar, envia mensagem de erro genérica:
E chama transfer_session(session_id) para escalar.