Ir al contenido

Arquitectura

emaemi/
├── src/
│ ├── index.ts # Hono app + CORS
│ ├── env.d.ts # Cloudflare bindings (secrets)
│ ├── api/
│ │ └── chat.ts # POST /api/chat
│ ├── orchestrator/
│ │ └── router.ts # AppName → AgentName
│ ├── agents/
│ │ ├── base.ts # BaseAgent abstracto, tool loop
│ │ ├── sales.ts # SalesAgent (sin tools)
│ │ ├── support.ts # SupportAgent (11 tools FHIR)
│ │ └── analytics.ts # AnalyticsAgent (6 tools Supabase)
│ ├── middleware/
│ │ └── auth.ts # Resolver de API key + tenant validator
│ ├── lib/
│ │ ├── claude.ts # fetch nativo + retry 529 + tool loop
│ │ ├── fhir-client.ts # Query builder Aidbox
│ │ ├── supabase-client.ts # Query builder Supabase
│ │ ├── permissions.ts # APP_SCOPES (qué agente y tools por app)
│ │ └── types.ts # AppName, AgentName, ChatRequest, etc.
│ └── tools/
│ ├── clinic.ts # 11 herramientas FHIR
│ └── vault.ts # 6 herramientas Supabase
└── tests/
[Cliente: emahealth / emaclinic / emavault]
↓ POST /api/chat
↓ Authorization: Bearer EMI_API_KEY
↓ X-Tenant-Id (sólo emaclinic)
↓ Body: { messages, context }
[authMiddleware]
├─ valida API key → resuelve AppName (emahealth | emaclinic | emavault)
└─ valida X-Tenant-Id si applica
[Router.getAgent(app)]
└─ retorna instancia singleton del agente
[agent.run(messages, context)]
├─ buildSystemPrompt(locale, turnCount, tenantId, patientId, encounterId)
├─ runAgent() → invoca Claude API
└─ tool_use loop (MAX_TOOL_ITERATIONS = 5)
├─ Claude pide tool
├─ ToolExecutor ejecuta función (FHIR / Supabase)
├─ inserta tool_result
└─ vuelve a Claude hasta `end_turn`
[Response: { message, agent, turnCount }]

Tabla en permissions.ts → APP_SCOPES:

AppNameAgenteToolsTenant requerido
emahealthSalesAgentninguna (Q&A puro)no
emaclinicSupportAgent11 FHIR (Aidbox) (X-Tenant-Id)
emavaultAnalyticsAgent6 Supabaseno
  • Simple (Phase 1): AppName → AgentName fijo (1:1).
  • Scoped: cada app sólo puede invocar agentes/tools que están en su whitelist.
  • Tenant-aware: emaclinic obliga X-Tenant-Id en el header. Las queries FHIR se scope-an automáticamente al tenant; sin header → 400.

Los system prompts viven hardcodeados en cada agente (SYSTEM_PROMPT const). La función buildSystemPrompt() inyecta contexto dinámico:

  • locale (ej. es, en)
  • turnCount (cuántos mensajes lleva el usuario)
  • tenantId, patientId, encounterId (cuando aplican)

No hay versionado ni hot-swap: cambiar un prompt requiere redeploy. Phase 3 propone mover los prompts a un store versionado.

No persiste conversaciones. Cada request es stateless: el historial viene en el body de la request (lo administra el cliente).

Phase 3 contempla persistir conversaciones en una BD para auditoría y context windowing inteligente.