Pular para conteúdo

Contextvars — passar estado sem poluir tools

As tools do agente não devem receber session_id, phone, contact_name como parâmetros. O LLM não tem negócio decidir esses valores — eles vêm do webhook e são constantes durante o turno.

O padrão Python canônico para isso é contextvars.ContextVar.

Por que não passar como parâmetro

  1. Segurança: se o LLM passar telefone como argumento, pode confundir com o telefone de outro paciente citado no histórico.
  2. Limpeza do prompt: não poluir a docstring da tool com params técnicos.
  3. Confiabilidade: o telefone do WhatsApp é o telefone do paciente. Ponto. Nenhum LLM vai inventar melhor do que o webhook.

Setup

# agent.py — topo do módulo
import contextvars

_current_session_id: contextvars.ContextVar[str] = contextvars.ContextVar(
    "session_id", default=""
)
_current_phone_number: contextvars.ContextVar[str] = contextvars.ContextVar(
    "phone_number", default=""
)
_current_contact_name: contextvars.ContextVar[str] = contextvars.ContextVar(
    "contact_name", default=""
)

Setagem — antes de invoke

def run_agent(message, session_id, phone_number, contact_name):
    _current_session_id.set(session_id)
    _current_phone_number.set(phone_number)
    _current_contact_name.set(contact_name)

    result = agent_executor.invoke(...)
    return result

Uso dentro da tool

@tool
def transferir_atendimento(motivo: str) -> str:
    """..."""
    session_id = _current_session_id.get()
    phone = _current_phone_number.get()
    # ...

Threads

ContextVar é thread-local por padrão (e também isolada por asyncio Task). Como o workflow roda em threading.Thread, cada request tem seu próprio valor — sem colisão entre conversas simultâneas.

Cuidado com threading.Thread manual

Se você spawna uma thread filha dentro de uma tool, o valor do ContextVar não se propaga automaticamente:

# não propaga — thread filha vê string vazia
threading.Thread(target=worker).start()

# propaga — copia contexto manualmente
import copy
ctx = copy.copy(contextvars.copy_context())
threading.Thread(target=ctx.run, args=(worker,)).start()

Em prática, as tools dos projetos atuais não spawnam threads filhas.

Quando NÃO usar contextvar

Se o valor é sem importância para o LLM e pode variar por chamada, passar como parâmetro ainda é o idiomático. Contextvar é para estado ambiente que não pode errar — session, phone.

Anti-padrão observado

Nenhum dos dois projetos cometeu, mas é tentador:

# NÃO FAÇA
_globals = {}

def run_agent(..., session_id, ...):
    _globals["session_id"] = session_id   # não é thread-safe

@tool
def minha_tool():
    return _globals["session_id"]          # race condition garantida

Use ContextVar. Foi feito para isso.

Atalhos úteis

def current_session() -> str:
    return _current_session_id.get()

def current_phone() -> str:
    return _current_phone_number.get()

def current_name() -> str:
    return _current_contact_name.get()

Evita importar os _current_* diretamente em vários lugares.