Pular para conteúdo

Resolver médico por nome

O agente IA recebe o nome do médico do paciente em linguagem natural ("Dr. João", "Dra. Sabrina", "o Mauricio Siqueira", "o ortopedista"). O cliente OTIMUS precisa transformar isso em um agenda_id numérico.

Esta é a armadilha #1 do padrão OTIMUS

Ver Problema: primeiro nome do doutor antes de implementar.

Estratégia canônica: lookup estrito por prioridade

Não use fuzzy matching. Use uma lista ordenada de keywords, em ordem de prioridade:

  1. Multi-palavra primeiro (ex: "JOAO PAULO").
  2. Primeiros nomes únicos.
  3. Sobrenomes garantidamente únicos como fallback.
_DOCTOR_KEYWORDS = [
    # Multi-palavra (testar antes de primeiro nome)
    ("JOAO PAULO",  6),

    # Primeiros nomes — TODOS únicos entre os N médicos
    ("MAURICIO",    0),
    ("VALMIR",      1),
    ("JURIADYSON",  2),
    ("REGINA",      3),
    ("SABRINA",     4),
    ("CHRISTIANE",  5),

    # Sobrenomes 100% únicos (fallback se primeiro nome não bateu)
    ("BRITO",       5),   # só Christiane tem Brito
    ("ALMEIDA",     6),   # só João Paulo tem Almeida
    ("SIQUEIRA",    0),   # só Mauricio tem Siqueira
]

def buscar_procedimento_por_medico(nome_medico: str) -> dict | None:
    nome_norm = _normalize(nome_medico)
    for keyword, idx in _DOCTOR_KEYWORDS:
        if keyword in nome_norm:
            return PROCEDIMENTOS[idx]
    return None

Regra de exclusão de sobrenomes compartilhados

Não colocar sobrenomes que mais de um médico tem

Ex: se Regina e Sabrina compartilham sobrenome CUNHA, não coloque "CUNHA" em _DOCTOR_KEYWORDS. A primeira ocorrência ganha silenciosamente e o paciente vai com a médica errada.

Da mesma forma DALCOL aparece em Juriadyson e Sabrina — excluído.

Validação na inicialização

Trave o boot se alguém adicionar médico com primeiro nome duplicado:

# scheduling.py — executado na importação do módulo
def _validate_unique_first_names() -> None:
    seen = set()
    for p in PROCEDIMENTOS:
        first = _normalize(p["medico"]).split()[0]
        if first in seen:
            raise RuntimeError(
                f"ERRO CRITICO: primeiro nome duplicado '{first}' em PROCEDIMENTOS. "
                f"Atualize _DOCTOR_KEYWORDS e _validar_agenda_medico para diferenciar."
            )
        seen.add(first)

_validate_unique_first_names()

Vantagem: erro aparece no boot, não em produção no meio de um agendamento.

Estratégia do CLINFETO (agenda_id direto)

O CLINFETO tem um lookup mais simples porque cada médico tem um agenda_id próprio e mapeia diretamente:

# agendamento.py:54-70 (CLINFETO)
_DOCTOR_KEYWORDS = [
    ("RENNAN",   59),
    ("PRISCILA", 54),
    ("RAFAELLA", 55),
    ("SUELEM",   57),
    ("JOANA",    58),
]

_AGENDA_FIRST_NAME = {
    59: "RENNAN",
    54: "PRISCILA",
    55: "RAFAELLA",
    57: "SUELEM",
    58: "JOANA",
}

def resolver_agenda(nome_medico: str) -> int | None:
    norm = _normalize(nome_medico)
    for keyword, agenda_id in _DOCTOR_KEYWORDS:
        if keyword in norm:
            return agenda_id
    return None

Validação pós-resposta (agendaNome)

A keyword acertou o agenda_id certo, mas o OTIMUS às vezes retorna a agenda errada (por bug ou cache). Revalide o campo agendaNome da resposta:

# scheduling.py:157-188 (MEDCENTER) — versão explicada
def _validar_agenda_medico(dias: list[dict], medico_esperado: str) -> bool:
    """Confirma que a agenda retornada é do médico certo.

    - Compara pelo PRIMEIRO NOME (ou multi-palavra 'JOAO PAULO')
    - Aceita se agendaNome estiver ausente (só filtro por agenda_id vale)
    - Rejeita se agendaNome presente e divergente
    """
    medico_norm = _normalize(medico_esperado)
    if not medico_norm:
        return False

    if "JOAO PAULO" in medico_norm:
        check_token = "JOAO PAULO"
    else:
        check_token = medico_norm.split()[0]  # primeiro nome único

    for dia in dias:
        for agenda in dia.get("agendas", []):
            agenda_nome_raw = agenda.get("agendaNome", "")
            agenda_nome = _normalize(agenda_nome_raw)
            if not agenda_nome:
                continue
            if check_token not in agenda_nome:
                logger.error(
                    "VALIDACAO FALHOU: agendaNome='%s' nao contem '%s'",
                    agenda_nome_raw, check_token,
                )
                return False
    return True

Caso "sem preferência de médico"

def _is_sem_preferencia(nome_medico: str) -> bool:
    if not nome_medico:
        return True
    norm = _normalize(nome_medico)
    return any(k in norm for k in ("SEM PREFERENCIA", "QUALQUER", "TANTO FAZ", "NAO TENHO"))

Se sim, itera todos os médicos que oferecem o procedimento até achar um com horários.

O que NÃO fazer

Anti-padrão Por quê
Usar difflib.get_close_matches / rapidfuzz Match probabilístico dá falso positivo silencioso
for m in medicos: if nome_medico in m["medico"]: return m "Ana" bate com "Ana Claudia" e "Ana Maria"
Só primeiro nome sem validar unicidade Escala mal — dois "João" e acabou
Sobrenome genérico (SILVA, SOUZA, OLIVEIRA) como keyword Nenhum é único — ambiguidade garantida
Não validar agendaNome no response OTIMUS já retornou agenda errada em produção — dá bug grave

Caminho de evolução (para escalar)

O padrão atual trava em ~10 médicos por clínica. Se uma clínica nova precisa de mais:

  1. Na conversa, forçar o paciente a escolher da lista numerada (não aceitar texto livre para médico).
  2. Passar para a tool um medico_id canônico (ex: medico_3) em vez do nome. O lookup vira int → dict direto.
  3. Manter o _validar_agenda_medico para defesa.

Ver o problema de escalabilidade.