Pular para conteúdo

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:

  1. GET paciente por CPF (endpoint 2).
  2. Se não existir → POST /paciente/multiplos (endpoint 3).
  3. GET paciente por CPF de novo para pegar o paciente_id real.

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)