Introdução

O AgentMedicSheduler é um agente de IA conversacional desenvolvido com o framework OpenAI Agents SDK para automatizar o agendamento de consultas médicas via WhatsApp.

Tecnologias: OpenAI GPT-5-nano, Agents SDK, SQLite Sessions, ChromaDB RAG, Flask, PostgreSQL

Características Principais

  • Conversação Natural: Entende expressões como "amanhã", "próxima segunda", "semana que vem"
  • Contexto Persistente: Mantém histórico de conversa via SQLiteSession
  • Input/Output Guardrails: Proteção contra inputs maliciosos e vazamento de dados
  • RAG Integration: Busca contexto em documentação interna via ChromaDB
  • Token Management: Controle de consumo diário com circuit breaker
  • Fallback Humano: Transferência automática para atendentes

Especificações Técnicas

NomeWhatsApp Medic Sheduler
Modelogpt-5-nano
Max Turns20 iterações por conversa
Session StorageSQLite (persistente por usuário)
TracingAgentOps (opcional)

Arquitetura

Fluxo de Processamento

WhatsApp

Mensagem do Paciente

Webhook

Twilio/Vonage

Input Guardrail

Validação Segurança

Agent Core

GPT-5-nano

Tools

Function Calling

Output Guardrail

Validação Saída

Resposta

Envio ao Paciente

Componentes Principais

AgentMedicSheduler - Inicialização
class AgentMedicSheduler:
    def __init__(self, user_number, app): 
        self.app = app
        self._chatkit_guard = create_systembreakdown_server(app)
        self.guard = self.get_guard()
        self.user_number = user_number
        self.session_id = hash_user_number(user_number)
        
        # Sessão SQLite persistente por usuário
        self.session = SQLiteSession(
            self.session_id, 
            db_path=f'convthreads/{self.session_id}/{self.session_id[:8]}.db'
        )
        
        # Configuração do modelo
        model_settings = ModelSettings(
            include_usage=True,
            workflow_name="WhatsApp Medic Sheduler",
            parallel_tool_calls=False  # Tools executadas sequencialmente
        )
        
        # Definição do Agente
        self.assistant = Agent[MedicShedulerAgentContext]( 
            model="gpt-5-nano",
            name="WhatsApp Medic Sheduler",
            instructions=BASE_INSTRUCTIONS,
            tools=TOOLS,  # 7 ferramentas disponíveis
            model_settings=model_settings,
            input_guardrails=[input_safety_guardrail],
            output_guardrails=[output_safety_guardrail],
        )

Ciclo de Vida da Mensagem

1

System Breakdown Check

Verifica se o sistema está operacional. Se is_system_broken=True, retorna fallback imediato para humano.

2

Contexto do Paciente

Busca dados do paciente via NetPacsConnector usando o número WhatsApp. Preenche MedicShedulerAgentContext.

3

Input Guardrail

Analisa mensagem de entrada para detectar conteúdo malicioso, injeção de prompt ou tentativas de bypass.

4

Agent Runner

Executa o agente com max_turns=20. O agente pode chamar ferramentas múltiplas vezes até completar a tarefa.

5

Output Guardrail

Valida resposta para prevenir exfiltração de dados sensíveis (CPF, tokens, dados médicos).

6

Token Logging

Registra consumo de tokens via SytemBreakDownHooks para controle de custos e circuit breaker.

Model Settings

Configuração do Modelo
model_settings = ModelSettings(
    include_usage=True,          # Incluir métricas de uso
    workflow_name="WhatsApp Medic Sheduler",
    parallel_tool_calls=False    # Ferramentas em sequência
)

run_config = RunConfig(
    tracing_disabled=False,              # Habilitar tracing
    trace_include_sensitive_data=False,  # Não logar dados sensíveis
    model_settings=model_settings,
)
ConfiguraçãoValorDescrição
parallel_tool_callsfalseFerramentas executadas sequencialmente para garantir ordem
include_usagetrueRetorna métricas de tokens consumidos
max_turns20Máximo de iterações por conversa
tracing_disabledfalseHabilita observabilidade via AgentOps

Contexto do Agente

O MedicShedulerAgentContext armazena todas as informações necessárias para o agente processar a conversa:

MedicShedulerAgentContext
@dataclass
class MedicShedulerAgentContext:
    """Contexto do agente para agendamento médico."""
    
    request_context: dict[str, Any]  # number_whatsapp, session_id
    sheduler_context: Optional[ShedulerContext] = None

class ShedulerContext(BaseModel):
    """Contexto completo do agendamento."""
    
    basic_patient_info: BasicPatientInfo    # id, name, whatsapp
    sensive_patient_info: SensivePatientInfo  # cpf, rg, email, etc
    accessionNumber: Optional[str] = None
    protocolo: Optional[str] = None
    idHorario: Optional[str] = None

BasicPatientInfo

CampoTipoDescrição
idstringID único do paciente no sistema
namestringNome completo do paciente
number_whatsappstringWhatsApp do paciente
patient_socialstringNome social (se houver)

SensivePatientInfo

CampoTipoDescrição
sexstringSexo (M/F)
cpfstringCPF do paciente
rgstringRG do paciente
emailstringE-mail de contato
alturastringAltura em metros
pesostringPeso em kg
birthdatestringData nascimento (YYYY/MM/DD)

Sessão Persistente

O agente utiliza SQLiteSession para manter histórico de conversa por usuário:

# Cada usuário tem um banco de dados separado
session_id = hash_user_number(user_number)  # SHA256 do telefone
session_db_path = f'convthreads/{session_id}/{session_id[:8]}.db'

self.session = SQLiteSession(
    session_id, 
    db_path=session_db_path
)
Estrutura: backend/convthreads/{hash_do_telefone}/{hash[:8]}.db

Ferramentas do Agente

O agente possui 9 ferramentas para interagir com o sistema. Cada ferramenta é testável independentemente através de scripts standalone localizados em backend/scripts/tools/.

Scripts de Teste: Todos os scripts estão disponíveis em backend/scripts/tools/ e podem ser executados isoladamente ou via TEST_ALL_TOOLS.py para validação completa. Veja a seção "Scripts de Teste" para mais detalhes.

GetCurrentDate

Obtém a data e hora atual do servidor para mapear expressões naturais.

Quando usar:
  • Paciente usa "amanhã", "próxima segunda", "semana que vem"
  • Validação de janela de 25h para reagendamento/cancelamento
{
  "status": "success",
  "timestamp": 1737259200,
  "data_hora_atual": "2025-01-19T14:30:00",
  "data_BR": "19/01/2025",
  "hora": "14:30:00",
  "numero_dia_semana": 6,
  "message": "Data e hora atual: 19/01/2025 às 14:30:00"
}

ListProcedimentos

Lista procedimentos médicos disponíveis (consultas, exames, cirurgias).

Parâmetros:
limitintLimite de resultados (padrão: 100) - Opcional
filtersdict (opcional)Dicionário de filtros (não obrigatório)
{
  "status": "success",
  "total": 5,
  "data": [
    {"id_procedimento": 1, "nome": "Tomografia Crânio", "tipo": "EXAME", "price": 450.00, "duracao_procedimento": 30},
    {"id_procedimento": 2, "nome": "Consulta Geral", "tipo": "CONSULTA", "price": 250.00, "duracao_procedimento": 20},
    {"id_procedimento": 3, "nome": "Ressonância Magnética", "tipo": "EXAME", "price": 800.00, "duracao_procedimento": 45}
  ]
}

ListConvenios

Lista convênios (planos de saúde) disponíveis para agendamento.

Parâmetros:
limitintLimite de resultados (padrão: 100) - Opcional
filtersdict (opcional)Dicionário de filtros (não obrigatório)
{
  "status": "success",
  "total": 3,
  "data": [
    {"id_convenio": 1, "id_filial": 1, "nome": "Particular", "ativo": true},
    {"id_convenio": 24, "id_filial": 1, "nome": "Unimed", "ativo": true},
    {"id_convenio": 28, "id_filial": 1, "nome": "Amil", "ativo": true}
  ]
}

ListFiliais

Lista filiais/unidades de atendimento da rede de saúde disponíveis para agendamento.

Parâmetros:
limitintLimite de resultados (padrão: 100) - Opcional
filtersdict (opcional)Dicionário de filtros (não obrigatório)
{
  "status": "success",
  "total": 3,
  "data": [
    {"id_filial": 1, "nome": "Unidade Centro", "cidade": "São Paulo", "endereco": "Av. Paulista, 1000"},
    {"id_filial": 2, "nome": "Unidade Zona Leste", "cidade": "São Paulo", "endereco": "Rua A, 500"},
    {"id_filial": 3, "nome": "Unidade Campinas", "cidade": "Campinas", "endereco": "Av. B, 1500"}
  ]
}

ListPlanosConvenio

Lista planos disponíveis para um convênio específico.

Parâmetros:
id_conveniointID do convênio (OBRIGATÓRIO)
{
  "status": "success",
  "total": 3,
  "id_convenio": "24",
  "data": [
    {"id_plano_convenio": 1, "nome": "Unimed Básico", "tipo": "Individual"},
    {"id_plano_convenio": 2, "nome": "Unimed Premium", "tipo": "Individual"},
    {"id_plano_convenio": 3, "nome": "Unimed Empresarial", "tipo": "Empresarial"}
  ]
}

OpenBooking

Confirma o agendamento de uma consulta/exame no sistema NetAnimati após paciente selecionar horário disponível.

Parâmetros (BookingParams):
id_pacienteintID do paciente (OBRIGATÓRIO)
id_conveniointID do convênio (OBRIGATÓRIO)
id_plano_conveniointID do plano do convênio (OBRIGATÓRIO)
data_stringstrData do agendamento (OBRIGATÓRIO)
hora_inicial_stringstrHora inicial (OBRIGATÓRIO)
duracao_procedimentointDuração em minutos (OBRIGATÓRIO)
id_escalaintID da escala médica (OBRIGATÓRIO)
id_horarioint (opcional)ID do horário (Opcional - pode ser None)
id_medicointID do médico (OBRIGATÓRIO)
id_procedimentointID do procedimento (OBRIGATÓRIO)
id_salaintID da sala (OBRIGATÓRIO)
peso_pacienteDecimal (opcional)Peso do paciente em kg (Opcional)
{
  "status": "success",
  "message": "Agendamento realizado com sucesso",
  "protocolo": "HUM-20250122-abc123de",
  "accessionNumber": "ACC-12345-20250122"
}

FallbackToHuman

Transfere a conversa para um atendente humano.

Quando usar:
  • Reagendamento/Cancelamento com mais de 25 horas
  • Paciente solicita explicitamente
  • Problema técnico não resolvível
  • Insatisfação persistente
Parâmetros (FallbackParams):
reasonstrMotivo da transferência (OBRIGATÓRIO)
summarystr (opcional)Resumo da conversa
patient_namestr (opcional)Nome do paciente
patient_whatsappstr (opcional)WhatsApp do paciente
{
  "status": "success",
  "ticket_id": "HUM-20250122143022-abc123de",
  "message": "Estamos te redirecionando para um atendimento humano."
}

retrieve_documentation_context

Busca contexto relevante na documentação interna via RAG (ChromaDB).

Parâmetros:
querystringPergunta ou termo de busca
kintNúmero de resultados (1-10)
Importante: Esta ferramenta deve ser chamada PRIMEIRO em toda conversa para carregar regras e tom de voz da rede de saúde.

SearchUnidade

Busca informações detalhadas de uma unidade/filial específica.

Parâmetros:
id_unidadeintID da unidade (OBRIGATÓRIO)
{
  "status": "success",
  "data": {
    "id_unidade": "1",
    "nome": "Unidade Centro",
    "endereco": "Av. Paulista, 1000",
    "telefone": "(11) 3000-0000",
    "email": "centro@hospital.com.br",
    "horario_atendimento": "Seg-Sex: 08:00-20:00, Sab: 09:00-14:00"
  }
}

SearchSala

Busca informações detalhadas de uma sala/consultório específico.

Parâmetros:
id_salaintID da sala (OBRIGATÓRIO)
{
  "status": "success",
  "data": {
    "id_sala": "5",
    "nome": "Sala Tomografia",
    "id_unidade": "1",
    "id_filial": "1",
    "tipo_atendimento": "Diagnóstico por Imagem",
    "equipamentos": ["Tomógrafo 128 canais", "Sala de controle"]
  }
}

Scripts de Teste

✨ NOVO: 11 scripts de teste criados para validar cada ferramenta independentemente! Localizados em backend/scripts/tools/

Todos os scripts de teste estão localizados em backend/scripts/tools/ e podem ser executados de forma independente para validar cada ferramenta:

Scripts Disponíveis

ScriptFerramentaPropósitoLocalização
GetCurrentDate.py GetCurrentDate Obter data/hora atual para cálculos backend/scripts/tools/GetCurrentDate.py
ListProcedimentos.py ListProcedimentos Listar procedimentos médicos backend/scripts/tools/ListProcedimentos.py
ListFiliais.py ListFiliais Listar unidades de atendimento backend/scripts/tools/ListFiliais.py
ListConvenios.py ListConvenios Listar planos de saúde backend/scripts/tools/ListConvenios.py
ListPlanosConvenio.py ListPlanosConvenio Listar planos de um convênio backend/scripts/tools/ListPlanosConvenio.py
SearchSlots.py SearchSlots Buscar horários disponíveis backend/scripts/tools/SearchSlots.py
SearchUnidade.py SearchUnidade Buscar informações de unidade backend/scripts/tools/SearchUnidade.py
SearchSala.py SearchSala Buscar informações de sala backend/scripts/tools/SearchSala.py
OpenBooking.py OpenBooking Criar agendamento backend/scripts/tools/OpenBooking.py
FallbackToHuman.py FallbackToHuman Transferir para atendente backend/scripts/tools/FallbackToHuman.py
RetrieveDocumentationContext.py retrieve_documentation_context Buscar contexto via RAG backend/scripts/tools/RetrieveDocumentationContext.py
TEST_ALL_TOOLS.py Validação Completa Testar todas as 9 ferramentas sequencialmente backend/scripts/tools/TEST_ALL_TOOLS.py

Como Usar

1. Testar uma Ferramenta Específica

Terminal
cd WhatsAppMedicSheduler
python backend/scripts/tools/ListProcedimentos.py

2. Validar Todas as Ferramentas (Recomendado)

Terminal
cd WhatsAppMedicSheduler
python backend/scripts/tools/TEST_ALL_TOOLS.py
Output Esperado (100% de Sucesso)
🧪 TESTE DE TODAS AS TOOLS
[1/11] GetCurrentDate...              ✓ SUCESSO
[2/11] ListProcedimentos...           ✓ SUCESSO
[3/11] ListFiliais...                 ✓ SUCESSO
[4/11] ListConvenios...               ✓ SUCESSO
[5/11] ListPlanosConvenio...          ✓ SUCESSO
[6/11] SearchSlots...                 ✓ SUCESSO
[7/11] SearchUnidade...               ✓ SUCESSO
[8/11] SearchSala...                  ✓ SUCESSO
[9/11] FallbackToHuman...             ✓ SUCESSO
[10/11] RetrieveDocumentation...      ✓ SUCESSO
[11/11] OpenBooking...                ✓ SUCESSO

✓ Testes aprovados: 11
✗ Testes falhados: 0
📈 Taxa de sucesso: 100.0%

✨ TODOS OS TESTES PASSARAM COM SUCESSO!

3. Testar via Docker (Recomendado )

Terminal
# Iniciar container
docker-compose up webhooks_medicsheduler

# Em outro terminal, executar teste
docker-compose exec webhooks_medicsheduler python backend/scripts/tools/TEST_ALL_TOOLS.py

Guardrails

O agente possui duas camadas de segurança para proteger contra inputs maliciosos e vazamento de dados:

Input Guardrail

Analisa mensagens de entrada antes de processar.

Detecta:
  • Injeção de prompt
  • Tentativas de bypass
  • Conteúdo malicioso
  • Comandos suspeitos
class InputSafetyOutput(BaseModel):
    is_malicious: bool
    reason: Optional[str] = None
    reasoning: Optional[str] = None
    matches: Optional[List[str]] = []
    action: Optional[str] = None  # allow, block_and_alert, sanitize_and_warn

Output Guardrail

Valida respostas antes de enviar ao paciente.

Previne:
  • Exfiltração de dados sensíveis
  • Vazamento de CPF/RG
  • Exposição de tokens
  • Dados médicos indevidos
class OutputSafetyOutput(BaseModel):
    is_exfiltration: bool
    reason: Optional[str] = None
    reasoning: Optional[str] = None
    response: Optional[str] = None
    redacted_output: Optional[str] = None
    matches: Optional[List[str]] = []
Se um guardrail é acionado (tripwire_triggered=True), a conversa é interrompida e retorna False.

System Breakdown

Sistema de circuit breaker que monitora consumo de tokens e pode desligar o agente automaticamente:

SytemBreakDown - Estados do Sistema
class Sytem(BaseModel):
    working_hours: List[str]          # Horários de funcionamento
    last_time_checked: datetime       # Última verificação
    is_email_sent: bool               # Alerta enviado?
    the_system_is_broken: bool        # Sistema desligado?
    tokens_info: TokensInfo           # Informações de tokens

class TokensInfo(BaseModel):
    tokens_used: int = 0              # Tokens consumidos hoje
    tokens_limit: int = 100000        # Limite diário
    max_tokens_limit: int = 500000    # Limite máximo
    users_using: int = 0              # Usuários ativos
    users_ids: List[str] = []         # IDs dos usuários

Verificação Automática

O SytemBreakDown executa verificações periódicas via APScheduler:

  • Verifica consumo de tokens a cada CHECK_INTERVAL_SECONDS
  • Se tokens_used > tokens_limit: envia email de alerta
  • Se tokens_used > max_tokens_limit: define the_system_is_broken=True
  • Sistema quebrado → todas as mensagens vão para fallback humano

Token Management

O consumo de tokens é registrado via SytemBreakDownHooks:

class SytemBreakDownHooks(RunHooks):
    async def on_agent_end(self, context: RunContextWrapper, agent: Agent, output: Any):
        u = context.usage
        total_tokens = u.total_tokens
        
        # Registra no PostgreSQL
        with self.app.app_context():
            cr = ChatRequest(
                user_id=user_identifier, 
                tokens_used=total_tokens
            )
            db.session.add(cr)
            db.session.commit()

Métricas de Uso

MétricaDescrição
input_tokensTokens da mensagem de entrada
cached_tokensTokens em cache (prompt caching)
reasoning_tokensTokens de raciocínio do modelo
output_tokensTokens da resposta gerada
total_tokensTotal consumido na interação

Fluxo de Agendamento

O agente segue um fluxo obrigatório de 6 passos para agendamento:

0

retrieve_documentation_context

Carregar regras, tom de voz e personalidade da rede de saúde via RAG.

1

ListConvenios()

Perguntar qual convênio o paciente vai utilizar. Armazenar id_convenio e id_filial.

2

ListPlanosConvenio(id_convenio)

Perguntar qual plano do convênio. Armazenar id_plano_convenio.

3

Perguntar Data

Crítico: Perguntar "Para quando você gostaria?" ANTES de buscar horários.

4

GetCurrentDate() + SearchSlots()

Se expressão natural ("amanhã"), calcular data. Buscar horários disponíveis.

5

OpenBooking()

Confirmar agendamento imediatamente após paciente escolher horário. Retorna protocolo de agendamento.

Proibido: Pular etapas, buscar horários sem convênio/plano, mostrar IDs ao paciente, pedir confirmação extra.

Reagendar/Cancelar

A API não suporta reagendamento/cancelamento direto. Todas as solicitações passam por validação de 25 horas:

Lógica de Validação
1. GetCurrentDate() → Data/hora atual

2. Calcular: Diferença = (Data da Consulta) - (Data Atual)

3. SE diferença < 25 horas:
   └─ NÃO executar ferramenta
   └─ Informar: "Nossa equipe entrará em contato em até 24h"

4. SE diferença > 25 horas:
   └─ Executar: FallbackToHuman(reason="Reagendamento com +25h")
   └─ Informar: "Vou transferir para nossa equipe"

Tratamento de Erros

Guardrail Triggered

except InputGuardrailTripwireTriggered as e:
    logger.warning(f"Input guardrail triggered - blocked")
    return 0, False  # Tokens=0, Output=False

except OutputGuardrailTripwireTriggered as e:
    logger.warning(f"Output guardrail triggered - blocked")
    return 0, False

System Broken

is_system_broken = self.guard.is_system_broken()
if is_system_broken:
    logger.info("Sistema quebrado, fallback para humano")
    return 0, False  # Todas as mensagens vão para humano

Erros de Ferramentas

Cada ferramenta retorna {"status": "error", "message": "...", "error": "..."} em caso de falha. O agente deve informar o paciente e tentar alternativas.