Pular para conteúdo

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.1 para 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-mini para fluxos mais limpos e bem delimitados (ex: Clinfeto, triagem de exames).
  • temperature=0.7 nos dois — ok. Subir para 1.0 só se o agente parecer robótico; descer para 0.3 se 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()
  • PostgresSaver persiste o histórico de cada thread_id em tabelas checkpoint*. Cria-se ao primeiro .setup().
  • thread_id = session_id do WTS Chat. Cada atendimento WhatsApp é uma thread independente.
  • Fallback MemorySaver só 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:

<mensagem original>

DATA DE HOJE: Monday, 14-04-2026, as 18:30

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:

Desculpe, tive um problema técnico. Um atendente já foi acionado.

E chama transfer_session(session_id) para escalar.