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.
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
| Nome | WhatsApp Medic Sheduler |
| Modelo | gpt-5-nano |
| Max Turns | 20 iterações por conversa |
| Session Storage | SQLite (persistente por usuário) |
| Tracing | AgentOps (opcional) |
Arquitetura
Fluxo de Processamento
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
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
System Breakdown Check
Verifica se o sistema está operacional. Se is_system_broken=True, retorna fallback imediato para humano.
Contexto do Paciente
Busca dados do paciente via NetPacsConnector usando o número WhatsApp. Preenche MedicShedulerAgentContext.
Input Guardrail
Analisa mensagem de entrada para detectar conteúdo malicioso, injeção de prompt ou tentativas de bypass.
Agent Runner
Executa o agente com max_turns=20. O agente pode chamar ferramentas múltiplas vezes até completar a tarefa.
Output Guardrail
Valida resposta para prevenir exfiltração de dados sensíveis (CPF, tokens, dados médicos).
Token Logging
Registra consumo de tokens via SytemBreakDownHooks para controle de custos e circuit breaker.
Model Settings
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ção | Valor | Descrição |
|---|---|---|
parallel_tool_calls | false | Ferramentas executadas sequencialmente para garantir ordem |
include_usage | true | Retorna métricas de tokens consumidos |
max_turns | 20 | Máximo de iterações por conversa |
tracing_disabled | false | Habilita observabilidade via AgentOps |
Contexto do Agente
O MedicShedulerAgentContext armazena todas as informações necessárias para o agente processar a conversa:
@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
| Campo | Tipo | Descrição |
|---|---|---|
id | string | ID único do paciente no sistema |
name | string | Nome completo do paciente |
number_whatsapp | string | WhatsApp do paciente |
patient_social | string | Nome social (se houver) |
SensivePatientInfo
| Campo | Tipo | Descrição |
|---|---|---|
sex | string | Sexo (M/F) |
cpf | string | CPF do paciente |
rg | string | RG do paciente |
email | string | E-mail de contato |
altura | string | Altura em metros |
peso | string | Peso em kg |
birthdate | string | Data 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
)
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/.
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:
limit | int | Limite de resultados (padrão: 100) - Opcional |
filters | dict (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:
limit | int | Limite de resultados (padrão: 100) - Opcional |
filters | dict (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:
limit | int | Limite de resultados (padrão: 100) - Opcional |
filters | dict (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_convenio | int | ID 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"}
]
}
SearchSlots
Busca horários disponíveis para agendamento via NetAnimati API.
Parâmetros (SearchSlotsParams):
data_busca | str | Data inicial para busca (OBRIGATÓRIO) |
id_paciente | int | ID do paciente (OBRIGATÓRIO) |
id_convenio | int | ID do convênio (OBRIGATÓRIO) |
id_filial | int | ID da filial/unidade (OBRIGATÓRIO) |
id_plano_convenio | int | ID do plano do convênio (OBRIGATÓRIO) |
id_procedimento | int | ID do procedimento (OBRIGATÓRIO) |
peso_paciente | Decimal (opcional) | Peso do paciente em kg (Opcional) |
{
"status": "success",
"total": 5,
"data": [
{
"idHorario": null,
"idEscala": 10277,
"idMedico": 10,
"nomeMedico": "Dr. João Silva",
"idSala": 5,
"nomeSala": "TOMOGRAFIA",
"idProcedimento": 1,
"nomeProcedimento": "TOMOGRAFIA CRANIO",
"dataString": "22/01/2025",
"horaInicialString": "09:00:00",
"horaFinalString": "09:30:00",
"duracaoProcedimento": 30,
"disponivel": true
}
]
}
OpenBooking
Confirma o agendamento de uma consulta/exame no sistema NetAnimati após paciente selecionar horário disponível.
Parâmetros (BookingParams):
id_paciente | int | ID do paciente (OBRIGATÓRIO) |
id_convenio | int | ID do convênio (OBRIGATÓRIO) |
id_plano_convenio | int | ID do plano do convênio (OBRIGATÓRIO) |
data_string | str | Data do agendamento (OBRIGATÓRIO) |
hora_inicial_string | str | Hora inicial (OBRIGATÓRIO) |
duracao_procedimento | int | Duração em minutos (OBRIGATÓRIO) |
id_escala | int | ID da escala médica (OBRIGATÓRIO) |
id_horario | int (opcional) | ID do horário (Opcional - pode ser None) |
id_medico | int | ID do médico (OBRIGATÓRIO) |
id_procedimento | int | ID do procedimento (OBRIGATÓRIO) |
id_sala | int | ID da sala (OBRIGATÓRIO) |
peso_paciente | Decimal (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):
reason | str | Motivo da transferência (OBRIGATÓRIO) |
summary | str (opcional) | Resumo da conversa |
patient_name | str (opcional) | Nome do paciente |
patient_whatsapp | str (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:
query | string | Pergunta ou termo de busca |
k | int | Número de resultados (1-10) |
SearchUnidade
Busca informações detalhadas de uma unidade/filial específica.
Parâmetros:
id_unidade | int | ID 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_sala | int | ID 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
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
| Script | Ferramenta | Propósito | Localizaçã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
cd WhatsAppMedicSheduler
python backend/scripts/tools/ListProcedimentos.py
2. Validar Todas as Ferramentas (Recomendado)
cd WhatsAppMedicSheduler
python backend/scripts/tools/TEST_ALL_TOOLS.py
🧪 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 )
# 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]] = []
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:
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: definethe_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étrica | Descrição |
|---|---|
input_tokens | Tokens da mensagem de entrada |
cached_tokens | Tokens em cache (prompt caching) |
reasoning_tokens | Tokens de raciocínio do modelo |
output_tokens | Tokens da resposta gerada |
total_tokens | Total consumido na interação |
Fluxo de Agendamento
O agente segue um fluxo obrigatório de 6 passos para agendamento:
retrieve_documentation_context
Carregar regras, tom de voz e personalidade da rede de saúde via RAG.
ListConvenios()
Perguntar qual convênio o paciente vai utilizar. Armazenar id_convenio e id_filial.
ListPlanosConvenio(id_convenio)
Perguntar qual plano do convênio. Armazenar id_plano_convenio.
Perguntar Data
Crítico: Perguntar "Para quando você gostaria?" ANTES de buscar horários.
GetCurrentDate() + SearchSlots()
Se expressão natural ("amanhã"), calcular data. Buscar horários disponíveis.
OpenBooking()
Confirmar agendamento imediatamente após paciente escolher horário. Retorna protocolo de agendamento.
Reagendar/Cancelar
A API não suporta reagendamento/cancelamento direto. Todas as solicitações passam por validação de 25 horas:
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.