Fluxo: Agendamento completo¶
O paciente escolheu data/hora e forneceu seus dados pessoais. Agora o agente
chama agendar_consulta(...) que executa o fluxo crítico.
Parâmetros da tool¶
agendar_consulta(
nome_completo: str,
cpf: str, # apenas dígitos, 11 chars
data_nascimento: str, # YYYY-MM-DD
nome_medico: str,
data_exame: str, # YYYY-MM-DD
hora_exame: str, # HH:MM
nome_procedimento: str,
nome_convenio: str,
telefone: str, # injeção via contextvar em CLINFETO
)
Passos¶
flowchart TD
A[agendar_consulta chamada] --> B[guardian_validate + normalize_date]
B --> C[Resolver IDs:<br/>agenda_id, especialidade_id,<br/>procedimento_id, convenio_id]
C --> D{Todos resolvidos?}
D -->|não| FAIL1[FALHA_CONSULTA com motivo]
D -->|sim| E[Pre-check: POST /agendamento/horarios<br/>+ _slot_disponivel]
E --> F{Slot ainda livre?}
F -->|não| FAIL2[FALHA_CONSULTA: slot indisponivel]
F -->|sim| G[POST /cadastros/pacientes/cpf/CPF]
G --> H{Paciente existe?}
H -->|não| I[POST /paciente/multiplos]
I --> G2[POST /cadastros/pacientes/cpf/CPF novamente]
G2 --> H2{Agora existe?}
H2 -->|não| FAIL3[FALHA_CONSULTA: cadastro falhou]
H2 -->|sim| J[extrai paciente_id]
H -->|sim| J
J --> K[POST /agendamento/agendamento]
K --> L{200 OK?}
L -->|não| FAIL4[FALHA_CONSULTA: erro no agendamento]
L -->|sim| M[Retorna description + preparo]
Implementação de referência (Clinfeto)¶
# agendamento.py:757-834 (CLINFETO)
@entrypoint(checkpointer=agendamento_checkpointer)
def agendar_consulta_workflow(payload: dict) -> str:
nome_completo = payload.get("nome_completo", "")
cpf = _only_digits(payload.get("cpf", ""))
data_nascimento = _normalize_date(payload.get("data_nascimento", ""))
nome_medico = payload.get("nome_medico", "")
data_exame = _normalize_date(payload.get("data_exame", ""))
hora_exame = (payload.get("hora_exame", "") or "").strip()[:5]
nome_procedimento = payload.get("nome_procedimento", "")
nome_convenio = payload.get("nome_convenio", "")
telefone = _only_digits(payload.get("telefone", ""))
nome_convenio, nome_procedimento = guardian_validate(nome_convenio, nome_procedimento)
id_convenio = buscar_convenio(nome_convenio)
if id_convenio is None:
return f"FALHA_CONSULTA: convenio nao encontrado: {nome_convenio}"
id_agenda = resolver_agenda(nome_medico)
if id_agenda is None:
return f"FALHA_CONSULTA: medico '{nome_medico}' nao encontrado"
proc_info = buscar_procedimento(nome_procedimento, id_agenda)
if proc_info is None:
return f"FALHA_CONSULTA: procedimento '{nome_procedimento}' nao encontrado"
# Pre-check de disponibilidade
data_inicio = data_exame
data_fim = (_parse_date_yyyymmdd(data_exame) + timedelta(days=90)).strftime("%Y-%m-%d")
data_items = _consultar_horarios_medico(proc_info, id_convenio, data_inicio, data_fim, id_agenda)
if data_items is None:
return "FALHA_CONSULTA: erro ao consultar horarios apos 2 tentativas"
if not _slot_disponivel(data_items, data_exame, hora_exame):
return "FALHA_CONSULTA: slot indisponivel (pode ter sido ocupado)"
# Buscar ou criar paciente
paciente = fetch_paciente_task(cpf)
if paciente is None:
criar_paciente_task(nome_completo, cpf, data_nascimento, telefone)
paciente = fetch_paciente_task(cpf)
if paciente is None:
return "FALHA_CONSULTA: cadastro de paciente falhou"
paciente_id = paciente.get("paciente_id") or paciente.get("id")
# Confirmar agendamento
data_dmy = _yyyymmdd_to_ddmmyyyy(data_exame)
resp = criar_agendamento_task(
paciente_id=paciente_id,
cpf=cpf,
convenio_id=id_convenio,
data_consulta_dmy=data_dmy,
hora_consulta=hora_exame,
agenda_id=id_agenda,
procedimento_id=proc_info["id_procedimento"],
)
description = resp.get("description", "Agendamento confirmado.")
preparo = resp.get("preparo", "")
if preparo:
return f"{description}\n\n{preparo}"
return description
Implementação de referência (Medcenter)¶
# scheduling.py:550-642 (MEDCENTER)
def agendar_consulta(
nome_completo: str,
cpf: str,
data_nascimento: str,
nome_medico: str,
data_exame: str,
hora_exame: str,
nome_procedimento: str,
nome_convenio: str,
telefone: str,
) -> str:
proc = buscar_procedimento_por_medico(nome_medico)
if not proc:
return f"FALHA_CONSULTA: medico '{nome_medico}' nao possui agendamento"
conv = buscar_convenio(nome_convenio)
if not conv:
return f"FALHA_CONSULTA: convenio nao encontrado"
# verifica disponibilidade + valida agenda do medico
dias = _verificar_disponibilidade(proc, conv, data_exame, hora_exame)
if not dias:
return "FALHA_CONSULTA: horario nao mais disponivel"
if not _validar_agenda_medico(dias, proc["medico"]):
return "FALHA_CONSULTA: agenda retornada nao bate com o medico esperado"
data_dmy = _normalize_data_para_dmy(data_exame)
cpf_digits = _only_digits(cpf)
paciente = _buscar_paciente_cpf(cpf_digits)
if paciente is None:
paciente = _cadastrar_paciente(nome_completo, cpf_digits, data_nascimento, telefone)
if paciente is None:
return "FALHA_CONSULTA: cadastro de paciente falhou"
paciente_id = paciente["paciente_id"]
resp = _confirmar_agendamento(
paciente_id=paciente_id,
cpf=cpf_digits,
convenio_id=conv["id"],
data_consulta_dmy=data_dmy,
hora_consulta=hora_exame,
agenda_id=proc["id_agenda"],
procedimento_id=proc["id_procedimento"],
)
return f"{resp.get('description','Agendado.')}\n\n{resp.get('preparo','')}"
Pre-check: por que re-consultar antes de confirmar¶
Entre o momento em que o agente mostrou opções ao paciente e o momento em que ele confirmou, outro paciente pode ter ocupado o slot. O pre-check evita criar agendamento em cima de outro.
O pre-check usa exatamente o endpoint 1 com data == data_exame e um
endDate == data_exame + 90 dias. Depois, _slot_disponivel() verifica se
(data, hora) aparece nos resultados:
# agendamento.py:735-754 (CLINFETO)
def _slot_disponivel(data_items: list, data_exame_ymd: str, hora_exame: str) -> bool:
hora = (hora_exame or "").strip()[:5]
if not data_exame_ymd or not hora:
return False
data_dmy = _yyyymmdd_to_ddmmyyyy(data_exame_ymd)
for item in data_items:
if (item.get("data") or "").strip() != data_dmy:
continue
for agenda in item.get("agendas", []):
for horario in agenda.get("horarios", []):
h = (horario.get("hora") or "").strip()[:5]
if h == hora:
return True
return False
Buscar antes de cadastrar (sempre)¶
Nunca ir direto para /paciente/multiplos. Duplicar paciente é muito
ruim de desfazer no OTIMUS. O padrão canônico:
GETpaciente por CPF (endpoint 2).- Se não existir →
POST /paciente/multiplos(endpoint 3). GETpaciente por CPF de novo para pegar opaciente_idreal.
Mensagem final ao paciente¶
O description retornado pelo OTIMUS é adequado como confirmação, mas o
agente deve ainda assim reforçar com:
- Data/hora
- Nome do médico
- Endereço da clínica
preparo(se houver)- Instrução de comparecimento antecipado (típico: 15 min)