Todas las contribuciones
IA & Machine Learninglanggraphqdrantmem0

Orquestar tres agentes para una clínica dental con LangGraph, Qdrant, Mem0 y Temporal

Caso completo: agente de citas + agente de recordatorios + agente de reseñas, orquestados con LangGraph y Postgres como checkpointer. Memoria semántica de pacientes con Mem0, flujos largos con Temporal, guardrails sanitarios y trazas en Langfuse.

Numoru EngineeringPublicado el 28 de junio de 202619 min de lectura
Compartir
Propuesta de implementacióngithub.com/numoru-ia/clinic-agents-langgraph

TL;DR

Desplegamos tres agentes especializados para una clínica dental: agente de citas (toma llamadas/WhatsApp y agenda), agente de recordatorios (envía confirmaciones y pre-consultas) y agente de reseñas (post-cita, invita a dejar review). Los tres orquestados con LangGraph y checkpointer en Postgres, memoria de paciente en Mem0 sobre Qdrant, flujos largos (recordatorio 24h, follow-up 7 días) con Temporal, guardrails sanitarios con NeMo Guardrails y observabilidad completa en Langfuse. El sistema reduce no-shows 38%, captura 87% de leads fuera de horario y libera ~60 horas/mes de recepción. Todo el código, los tools MCP y los workflows Temporal quedan publicados.

-38%
No-show rate
Medición 90 días del piloto
+75 pts
Leads fuera de horario capturados
12% → 87%
62 h
Tiempo recepción liberado / mes
170 → 108
$420
Costo plataforma / mes
3 doctores, 850 visitas

Por qué tres agentes y no uno gigante

Un agente monolítico puede responder a todo pero pierde foco. Hablar con paciente que quiere agendar es distinto de recordarle que tiene cita mañana o pedirle reseña a las 24h post-tratamiento. Tres ventajas del diseño multi-agente:

  1. Prompts más cortos y focalizados — cada agente sabe exactamente su trabajo.
  2. Evaluación independiente — puedes mejorar uno sin tocar los demás.
  3. Separación de privilegios — el agente de reseñas nunca puede agendar; el de citas nunca puede enviar emails masivos.

Diagrama general

  ┌──────────────────────────────────────────────────────────┐
  │                    LangGraph orchestrator                │
  │                                                          │
  │   ┌──────────────┐                                       │
  │   │ Router       │ ◄─── Canal (voz / WhatsApp / web)     │
  │   └──────┬───────┘                                       │
  │          │                                               │
  │  ┌───────┼──────────┬───────────────┐                    │
  │  ▼       ▼          ▼               ▼                    │
  │ Citas  Recordatorio Reseña       Fallback human         │
  │          ▲              ▲                               │
  │          │              │                               │
  │     ┌────┴─────┐   ┌────┴───────┐                        │
  │     │ Temporal │   │ Temporal   │                        │
  │     │ 24h/1h   │   │ 24h post   │                        │
  │     └──────────┘   └────────────┘                        │
  └──────────────────────────────────────────────────────────┘
         │                 │                  │
         ▼                 ▼                  ▼
      Calendario        WhatsApp            Google Reviews
      (MCP)            (MCP)               (MCP)
         │                 │                  │
         └─────────────────┴──────────────────┘
                           │
                           ▼
             Postgres + Qdrant + Redis + Mem0 + Langfuse

Stack en concreto

PiezaTecnologíaRol
OrquestadorLangGraph (Python)Grafo de estados entre agentes
CheckpointerPostgresPersistencia de estado conversacional
Memoria semánticaMem0 sobre QdrantHechos por paciente
Working memoryRedisContexto por turno
Flujos largosTemporalRecordatorios 24h, follow-ups 7d
LLMClaude Sonnet vía LiteLLMRazonamiento
VozPipecat + Deepgram + CartesiaCanal telefónico
WhatsAppMCP mcp-whatsappCanal mensajería
CalendarioMCP mcp-calendarSlots disponibles
ReseñasMCP custom mcp-reviewsGoogle Business Profile
GuardrailsNeMo GuardrailsNo diagnóstico, no precios inventados
ObservabilidadLangfuse self-hostTrazas, evals, sesiones

El grafo LangGraph

Archivo clinic_graph.py:

from langgraph.graph import StateGraph, END
from langgraph.checkpoint.postgres import PostgresSaver
from typing import TypedDict, Literal

class ClinicState(TypedDict):
    session_id: str
    patient_id: str | None
    channel: Literal["voice", "whatsapp", "web"]
    user_message: str
    intent: Literal["book", "reschedule", "cancel", "ask_info", "confirm", "review", "unknown"]
    working_memory: dict
    response: str
    tool_calls: list
    done: bool

def router_node(state: ClinicState) -> ClinicState:
    state["intent"] = classify_intent(state["user_message"])
    return state

def booking_node(state: ClinicState) -> ClinicState:
    # agente especializado con su propio prompt + tools de calendario
    result = booking_agent.run(state)
    return {**state, **result}

def reminder_handler(state: ClinicState) -> ClinicState:
    # atiende confirmaciones de recordatorios
    result = reminder_agent.run(state)
    return {**state, **result}

def review_handler(state: ClinicState) -> ClinicState:
    result = review_agent.run(state)
    return {**state, **result}

def human_fallback(state: ClinicState) -> ClinicState:
    trigger_human_handoff(state)
    return {**state, "done": True, "response": "Te paso con una recepcionista."}

def route(state: ClinicState) -> str:
    intent = state["intent"]
    if intent in ("book", "reschedule", "cancel", "ask_info"):
        return "booking"
    if intent == "confirm":
        return "reminder"
    if intent == "review":
        return "review"
    return "fallback"

graph = StateGraph(ClinicState)
graph.add_node("router", router_node)
graph.add_node("booking", booking_node)
graph.add_node("reminder", reminder_handler)
graph.add_node("review", review_handler)
graph.add_node("fallback", human_fallback)

graph.set_entry_point("router")
graph.add_conditional_edges("router", route, {
    "booking": "booking",
    "reminder": "reminder",
    "review": "review",
    "fallback": "fallback",
})
graph.add_edge("booking", END)
graph.add_edge("reminder", END)
graph.add_edge("review", END)
graph.add_edge("fallback", END)

checkpointer = PostgresSaver.from_conn_string(os.getenv("POSTGRES_URL"))
app = graph.compile(checkpointer=checkpointer)

Agente de citas

Prompt corto, tools específicos, guardrails:

BOOKING_SYSTEM = """
Eres el agente de reservas de Clínica Dental Numoru.
Tu única tarea es ayudar a agendar, reagendar o cancelar citas.

Reglas:
- Confirma nombre completo y teléfono antes de agendar.
- Respeta horario: lu-sa 9:00-19:00.
- Si el paciente pregunta precio o diagnóstico, usa search_clinic_info;
  si no está en el RAG, ofrece hablar con el doctor, nunca inventes.
- Si detectas urgencia médica real, transfiere inmediatamente con transfer_to_emergency.
"""

booking_agent = Agent(
    system_prompt=BOOKING_SYSTEM,
    model="claude-sonnet",
    tools=[
        tool.find_available_slot,
        tool.book_appointment,
        tool.reschedule,
        tool.cancel,
        tool.search_clinic_info,
        tool.transfer_to_emergency,
    ],
    guardrails=NeMoGuardrails(config="guardrails/clinic.colang"),
)

Agente de recordatorios (con Temporal)

El agente de recordatorios es reactivo (contesta) pero el envío lo dispara Temporal:

# workflow Temporal
@workflow.defn
class AppointmentReminderWorkflow:
    @workflow.run
    async def run(self, appt: Appointment):
        # 24h antes
        await workflow.sleep(appt.start - timedelta(hours=24))
        await workflow.execute_activity(
            send_whatsapp_template,
            ReminderInput(patient_id=appt.patient_id, slot=appt.start, kind="24h"),
            start_to_close_timeout=timedelta(minutes=2),
        )
        # 1h antes
        await workflow.sleep(appt.start - timedelta(hours=1))
        await workflow.execute_activity(
            send_whatsapp_template,
            ReminderInput(patient_id=appt.patient_id, slot=appt.start, kind="1h"),
            start_to_close_timeout=timedelta(minutes=2),
        )

Temporal sobrevive reinicios, upgrades y caídas del worker. Si el paciente responde al WhatsApp, el mensaje entrante dispara el graph LangGraph con intent="confirm".

Agente de reseñas (post-cita)

@workflow.defn
class PostVisitReviewWorkflow:
    @workflow.run
    async def run(self, appt: Appointment):
        # 4h post-cita
        await workflow.sleep(appt.end + timedelta(hours=4))
        satisfied = await workflow.execute_activity(
            nps_one_question,
            NpsInput(patient_id=appt.patient_id),
            start_to_close_timeout=timedelta(minutes=10),
        )
        if satisfied >= 8:
            # 24h después, pedir reseña pública
            await workflow.sleep(timedelta(hours=20))
            await workflow.execute_activity(
                request_review,
                ReviewInput(patient_id=appt.patient_id, channel="google"),
            )

El agente no pide reseña a pacientes insatisfechos — dispara un workflow interno para que un humano los contacte antes.

Memoria de paciente con Mem0

Hechos extraídos de conversaciones pasadas (con consentimiento):

from mem0 import Memory

memory = Memory.from_config({
    "vector_store": {
        "provider": "qdrant",
        "config": {"host": "qdrant", "port": 6333, "collection_name": "clinic_patients"},
    },
    "llm": {"provider": "litellm", "config": {"model": "claude-sonnet"}},
})

def enrich_prompt_with_memory(patient_id: str, query: str) -> str:
    facts = memory.search(query=query, user_id=patient_id, limit=5)
    if not facts:
        return ""
    bullets = "\n".join(f"- {f['memory']}" for f in facts)
    return f"\n\nContexto previo del paciente:\n{bullets}\n"

Ejemplos de facts útiles:

  • "Prefiere citas por la tarde"
  • "Alérgico a la penicilina"
  • "Últimas 2 consultas: limpieza y endodoncia en molar 36"
  • "Trae seguro GNP"

Guardrails sanitarios

Archivo guardrails/clinic.colang:

define user ask medical_diagnosis
  "¿tengo caries?"
  "¿qué enfermedad tengo?"
  "¿es grave lo que tengo?"
  "¿necesito ortodoncia?"

define bot refuse medical_diagnosis
  "Para diagnosticar necesitamos verte. ¿Te agendo con el doctor?"

define user ask price
  "¿cuánto cuesta?"
  "¿qué precio tiene?"

define flow
  user ask medical_diagnosis
  bot refuse medical_diagnosis

define flow
  user ask price
  execute search_clinic_info
  # si no encuentra precio en RAG, el tool devuelve error y el flujo default responde "consulta con recepción"

Observabilidad en Langfuse

Cada ejecución del graph emite una session con:

  • patient_id (hash).
  • channel.
  • intent detectado.
  • Tool calls con input/output.
  • Score de "goal completion" (evaluación automática: ¿cerró la intención del usuario?).

Dashboards que miramos:

  • Goal completion rate por intent (target >85% para book).
  • Transfer rate humano (target <12%).
  • Latencia p95 (voz: <2.5s por turno).
  • No-show rate post-recordatorio (baseline vs actual).

Métricas reales del cliente piloto

Clínica dental con 3 doctores, 850 citas/mes.

MétricaPrePost 90 días
No-show rate22%14%
Leads capturados fuera de horario12%87%
Horas recepción/mes170108
NPS promedio7.88.4
Reseñas Google nuevas/mes418
Costo operativo agente (USD/mes)420

ROI: la recepcionista adicional que no contrataron cuesta ~$600 USD/mes. El agente cuesta menos y trabaja 24/7.

Cliente piloto — deltas a 90 días (indexado, pre = 100)

Misma práctica, 3 doctores, 850 visitas al mes. Agentes desplegados en secuencia (citas → recordatorios → reseñas) durante 6 semanas; periodo de estabilización al día 60.

Captura fuera de horarioReseñas Google / mesHoras de recepciónNo-shows0200400600800
  • Pre (baseline = 100)
  • 90 días post

Telemetría de cliente Numoru, medida contra baseline del año anterior.

Impacto de negocio y casos

Business & commercial impact

Dónde se concentra el valor

La configuración multi-agente desbloquea revenue que un solo agente de voz no puede. Recordatorios es la línea de dólares más grande — cada punto de no-show recuperado es un slot de $120-180 que se rellena. Reseñas es el motor de crecimiento a largo plazo — nuevos pacientes llegan vía ranking de Google Maps, que se mueve con reseñas frescas de 5 estrellas. El agente de citas es apuesta mínima; los otros dos son el moat.

Dónde se vende este patrón más allá de dental

Ticket de orquestación multi-agente por vertical (Numoru, USD)

Grupos dentales (3-10 sedes)
Citas + recordatorios + reseñas + elegibilidad de seguro.
$1,200 – 3,500 / mes
12 mo
Cadenas estética / MedSpa
Cobro de anticipo + pre-care + follow-up 7 días + NPS.
$1,800 – 4,500 / mes
12 mo
Fisioterapia / rehab
Adherencia al plan + recordatorios + renovación de seguro.
$900 – 2,400 / mes
12 mo
Cadenas veterinarias
Recordatorios de vacunación + receta + wellness anual.
$1,100 – 2,800 / mes
12 mo
Escuelas privadas / tutorías
Agente de enrollment + comms con padres + recordatorios + NPS.
$1,400 – 3,200 / mes
Año escolar 9 meses
Fitness boutique
Booking de trial + nudges de asistencia + win-back + reseñas.
$700 – 1,800 / mes
12 mo

Benchmarks públicos que anclan el valor

Public case studyManagement dental · EE.UU. · 2024

Dental Intelligence — analítica de agenda y recordatorios

Challenge
Cuantificar el lift de recordatorios automatizados + re-care recall sobre la agenda de una práctica.
Solution
Telemetría agregada de 12,000+ prácticas usando su Recall Manager y suite de recordatorios.
Results
Reducción de no-shows
-31%
Con recordatorios SMS + voz
Reactivación de re-care
18%
Pacientes lapsados responden al recall
Producción incremental
$4,200 / mes
Práctica promedio 8 sillones
Public case studySearch · Global · 2023

Google — impacto de local search + reseñas

Challenge
Medir cómo el volumen y la recencia de reseñas mueven el ranking de Google Maps y el CTR para negocios locales.
Solution
Estudios de Google Business Profile combinados con el reporte independiente MOZ Local Search Ranking Factors.
Results
Factores top-3 de ranking Maps
Reseñas
Entre los 5 signals con más peso
CTR con 4.8+ vs <4.0
+93%
En resultados del local pack
Visitas orgánicas por reseñas
+23%
YoY, pequeño negocio
Public case studyFramework IA · Global · 2024-2025

LangChain / LangGraph — casos de producción multi-agente

Challenge
Documentar despliegues de producción que usan checkpointer de LangGraph para trabajo agentic de larga duración.
Solution
La librería de customer stories de LangChain cubre despliegues en Replit, LinkedIn, Rakuten y otros orquestando 2-10 agentes especializados.
Results
Velocity de dev reportado
2-3× más rápido
Vs agente monolítico
Granularidad de evals
Por agente
Más fácil aislar regresiones
Durabilidad de estado
Postgres
Sobrevive a restart, soporta replay

Caso ilustrativo — cadena de estética

Illustrative caseSalud estética · 6 clínicas · 48 staff · $4.8M ARR · México + EE.UU. (Miami)

Cadena estética / MedSpa (6 sedes) adoptando el stack de 3 agentes

Baseline
Tasa de no-show 26% (los procedimientos electivos no se sienten urgentes). $0 de cobro de anticipo. 11 reseñas Google / mes en 6 sedes. 2 recepcionistas por sede, $3.1k / mes blended cada una.
Intervention
Rollout multi-agente: booking + agente de cobro de anticipo, agente de recordatorio con instrucciones pre-care, agente de reseñas con flujo de photo release + testimonio. Workflows Temporal para follow-up a 7 / 14 / 30 días post-tratamiento.
Projected outcome (12 mo)
Tasa de no-show
26% → 13%
Anticipo + recordatorios combinados
Cobro de anticipo
0% → 72%
Default al agendar
Reseñas / mes (todas las sedes)
11 → 94
Sostenido 6 meses
Nuevos pacientes por búsqueda
+41%
Lift en ranking Google Maps
Lift de revenue anual
+$980K
Bookings incrementales + menos huecos
Costo plataforma
$28,800 / año
6 × tier $400 × 12
Modelo de revenue basado en benchmarks operativos MedSpa Boss 2024 y datos de CTR de Google Business Profile. Caso sintético — no representa a un cliente específico de Numoru.

Calculadora ROI — práctica dental de 3 doctores

Práctica dental única, 3 doctores, 850 visitas / mes (12 meses)

Payback: < 1
Assumptions
Visitas / mes (baseline)850
Valor promedio de procedimiento$180
No-show pre-agente22%
No-show post-agente14%
Captura fuera de horario pre12%
Captura fuera de horario post87%
Leads fuera de horario / mes90
Conversión lead → paciente28%
Retainer plataforma (12 mo × $420)−$5,040
Implementación (one-time)−$6,500
Recuperación de no-shows (+68 visitas × $180 × 12)+$146,880
Nuevos pacientes por captura fuera de horario (+19 × $720 LTV × 12)+$164,160
Horas recepción ahorradas (62 × $12 × 12)+$8,928
Visitas nuevas por reseñas (+6 × $720 × 12)+$51,840
Contribución bruta neta año 1+$360,268

Tiers de pricing que Numoru vende

Una sede
$420/ mes
Una clínica. Los 3 agentes.
  • 1 sede / 1 teléfono / 1 WhatsApp
  • Orquestación LangGraph + Temporal
  • Memoria paciente Mem0
  • Guardrails sanitarios NeMo
  • Dashboard mensual Langfuse
  • Google Calendar + Google Reviews
  • Setup: $6,500 one-time
Grupo multi-sede
$320/ clínica / mes
3-10 sedes bajo una KB.
  • KB Qdrant compartida
  • Branding + voz por sede
  • Dashboard analítico cross-site
  • Add-on elegibilidad de seguros
  • Integración Dentrix / OpenDental
  • Revisión de evals trimestral
  • Setup: $14,000 one-time
Franquicia / enterprise
$240/ clínica / mes
10+ sedes, white-label disponible.
  • Opción self-hosted (HIPAA scope)
  • Branding white-label del agente
  • Contrato maestro / friendly a PO
  • CSM dedicado
  • Guardrails + políticas custom
  • Auditoría anual de compliance
  • Setup: $28,000+ one-time

El mensual incluye bucket de uso (2,000 llamadas + 5,000 WhatsApp + 8,000 inferencias LLM por clínica). Overages a rate card.

Deploy en el stack OSS

Sobre el droplet del stack self-hosted, agregamos:

services:
  clinic-orchestrator:
    image: numoru/clinic-orchestrator:latest
    environment:
      POSTGRES_URL: postgres://lf:${LF_DB_PASSWORD}@langfuse-db:5432/clinic
      QDRANT_URL: http://qdrant:6333
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/3
      LITELLM_URL: http://litellm:4000
      LANGFUSE_PUBLIC_KEY: ${LF_PUBLIC_KEY}
      TEMPORAL_HOST: temporal:7233
    depends_on: [langfuse-db, qdrant, redis, litellm, temporal]

  temporal:
    image: temporalio/auto-setup:1.25
    environment:
      DB: postgres12
      DB_PORT: 5432
      POSTGRES_USER: lf
      POSTGRES_PWD: ${LF_DB_PASSWORD}
      POSTGRES_SEEDS: langfuse-db
    networks: [core]

  clinic-temporal-worker:
    image: numoru/clinic-temporal-worker:latest
    environment:
      TEMPORAL_HOST: temporal:7233
      LITELLM_URL: http://litellm:4000
    depends_on: [temporal]

Compliance y regulación

  • LFPDPPP (México): consentimiento explícito para memoria; endpoint /patient/{id}/data para acceso/borrado.
  • HIPAA (si paciente US): todo on-prem, sin datos fuera del droplet del cliente.
  • AI Act (si paciente UE): transparencia en el canal de voz ("este asistente es IA"), logs auditables en Langfuse, plan de supervisión humana (fallback claro).

Tests y evals

  • Unit tests de cada tool MCP.
  • Integration tests del graph con Postgres embebido.
  • Suite de evals con 80 guiones doradas por agente.
  • E2E manual semanal: equipo de recepción revisa 10 conversaciones al azar.

Anti-patrones frecuentes

  1. Un solo agente que hace todo. Prompts de 5k tokens, evals imposibles, cambios riesgosos.
  2. Recordatorios por cron. Sin durabilidad, si el servidor se reinicia los pierdes. Temporal es la respuesta.
  3. Memoria global sin user_id. Data leak entre pacientes. Mandatorio user_id en cada operación de Mem0.
  4. Sin humano en el loop. Guardrails fallan alguna vez; hay que tener ruta clara de transferencia y alertas para casos de borde.
  5. Deploy sin backup. Un drop table appointments accidental es fatal. Restic diario mínimo.

FAQ

¿Se puede adaptar a otro vertical (bufete, veterinaria, spa)?Sí. Mismo esqueleto; cambian tools (catálogo de servicios), guardrails y tono. En general 1-2 semanas de adaptación por vertical nuevo.

¿Cuántos agentes puede aguantar el droplet de $40?Comodamente 3-5 clientes pequeños en el mismo droplet. A partir de 10, separar Temporal a su propia instancia.

¿Funciona sin Temporal?Se puede usar un simple cron + Postgres como cola. Funciona hasta que falla un reinicio y te das cuenta de por qué todos usan Temporal/Inngest.

¿Cómo manejas pacientes que no aceptan IA?Al inicio del canal voz/WhatsApp se les informa y pueden decir "quiero hablar con humano" — el graph transfiere inmediatamente.

¿Es más caro que contratar recepcionista? Sobre el 2do mes en volúmenes medios. Con clínicas muy pequeñas (<200 citas/mes), una recepcionista medio tiempo sigue siendo más barata.

Próximos pasos

Repo: github.com/numoru-ia/clinic-agents-langgraph. Incluye seeds de prompts, datos sintéticos para pruebas, playbooks de adaptación a otros verticales y guías de operación (runbook de incidentes, escalamiento, compliance). El siguiente artículo de la serie cubre evals CI/CD sobre este tipo de agentes.

¿Quieres resultados así para tu empresa?

Iniciar conversación
Compartir