Modelo de datos y tools
Persistencia interna
Sección titulada «Persistencia interna»A diferencia de Phase 1, Wave 3 sí persiste dos tipos de datos en
emashared:
- Activación y rate limit (
tenant_emi_access) — qué apps puede invocar cada tenant. - Audit log (
emi_invocations) — bitácora completa de invocaciones exitosas y fallidas, sin PHI.
Las conversaciones siguen siendo stateless: el historial vive en el cliente.
Tablas en emashared
Sección titulada «Tablas en emashared»| Tabla | Propósito |
|---|---|
tenant_emi_access | Activación per-tenant per-app: status, daily_invocation_limit, fecha alta. |
emi_invocations | Audit log: timestamp, app, agent, tenant_id, status (success/denied/error), turn_count, input/output tokens, cost_usd, summary del context (sin PHI). |
tenant_aidbox_credentials | Credenciales Aidbox encriptadas con AES-GCM por tenant: client_id, client_secret_ciphertext, encryption_key_version. |
Tools por agente
Sección titulada «Tools por agente»SalesAgent (emahealth) — 1 tool
Sección titulada «SalesAgent (emahealth) — 1 tool»Wave 3 le dio a SalesAgent capacidad de capturar leads:
| Tool | Qué hace |
|---|---|
create_or_update_lead | Upsert en emavault.leads por contact_email (case-sensitive) más insert en emavault.lead_activities (non-fatal). |
Decisiones (F010-F012):
- Match por email, no por nombre.
sourcedefault =emi_chatsi el agente no lo especifica.- Si
lead_activitiesfalla, ellead_idigual se devuelve (priority 1 = persistir el lead).
SupportAgent (emaclinic) — 11 tools FHIR
Sección titulada «SupportAgent (emaclinic) — 11 tools FHIR»Acceden a Aidbox con scoping por tenant. Las credenciales se decryptan
runtime usando aidbox-credentials.ts.
| Tool | Qué hace |
|---|---|
list_appointments | Agenda del día o rango |
get_patient_summary | Condiciones, alergias, medicamentos, órdenes activas, flags GES |
search_patients | Búsqueda por nombre o RUT |
list_alerts | ENO, valores críticos, plazos GES |
list_encounters | Worklist del día (paciente, status, tipo, hora) |
get_encounter_tasks | Tasks y formularios SDC asociados a un encuentro |
evaluate_ges | Aplicación de criterios GES (70 patologías) |
get_observations | Labs, vitales (FHIR Observation) |
get_diagnostic_reports | Reportes diagnósticos |
list_prescriptions | Medicamentos activos del paciente |
get_dashboard_summary | KPIs operativos de la clínica |
AnalyticsAgent (emavault) — 6 tools
Sección titulada «AnalyticsAgent (emavault) — 6 tools»Acceden a Supabase del proyecto Vault (emahub):
| Tool | Qué hace |
|---|---|
get_finance_summary | MRR, gastos, EBITDA, margin %, runway, BUPA |
list_expenses | Gastos por categoría/período (fijos vs variables) |
list_leads | Pipeline de 7 etapas con probability weighted |
get_team_hours | Tracking horas por persona/fecha |
list_obligations | Vencimientos legales y fiscales |
list_editorial | Calendario editorial (status + publish dates) |
LabAgent, PathologyAgent, WellnessCoach
Sección titulada «LabAgent, PathologyAgent, WellnessCoach»Sin tools en Wave 3. Sus system prompts indican honestamente qué capacidad no está disponible todavía. WellnessCoach retorna 501 antes de llegar a Claude.
Multi-tenancy FHIR
Sección titulada «Multi-tenancy FHIR»Cada tenant tiene credenciales Aidbox propias en
tenant_aidbox_credentials:
{ tenant_id: uuid, client_id: text, client_secret_ciphertext: text, -- formato AES-GCM v{n}:{iv}:{ct} encryption_key_version: int -- referencia a AIDBOX_SECRET_KEY_V{n}}Decisiones (F007-F009):
- Cache TTL 5 min in-memory por Worker isolate. Sin KV (latencia).
- Master key versionada (
AIDBOX_SECRET_KEY_V1,_V2, …) para rotación incremental sin re-encrypt en bloque. - Wire format AES-GCM
v{n}:{iv_b64}:{ct_b64}(3 partes; el auth tag va embebido en los últimos 16 bytes del ciphertext per WebCrypto).
Audit log
Sección titulada «Audit log»Cada invocación (success o error) genera una fila en emi_invocations.
La función helper logInvocation es fail-silent (F006) — un fallo
en audit nunca bloquea la respuesta al usuario.
summarizeContext (F005) registra presencia de campos sensibles, no
los valores: has_patient_id, has_encounter_id, etc. Más metadata
no-PHI: action, locale, route. Nunca prompts/responses
completos ni PHI.
Ejemplo de fila:
{ "id": "uuid", "invoked_at": "2026-05-09T01:34:21Z", "app": "emaclinic", "agent": "support", "tenant_id": "uuid", "status": "success", "turn_count": 3, "input_tokens": 1240, "output_tokens": 320, "cost_usd": 0.00184, "context_summary": { "has_patient_id": true, "has_encounter_id": false, "locale": "es", "route": "/encuentros/123" }}Cost tracking
Sección titulada «Cost tracking»cost.ts calcula el costo USD por modelo y agrega tokens a través del
tool loop completo (F015), no solo el último turno. Pricing
hardcoded en código (F014); Wave 4 lo moverá a
emashared.model_pricing para que Vault lo gestione vía UI.
Logger estructurado
Sección titulada «Logger estructurado»logger.ts emite JSON para Cloudflare Observability con campos:
level, event, request_id, app, agent, tenant_id, latency_ms,
input_tokens, output_tokens, cost_usd. Sin PHI ni prompts.
ChatContext (extendido en Wave 3)
Sección titulada «ChatContext (extendido en Wave 3)»type ChatContext = { // Multi-uso locale?: string; metadata?: Record<string, unknown>;
// Clínico (emaclinic, emalab, emapath) patientId?: string; encounterId?: string; organizationId?: string;
// Visitante anónimo (emahealth) sessionId?: string; visitorEmail?: string; visitorName?: string; visitorCountry?: string; referrer?: string; conversationId?: string;};El campo visitorEmail permite al SalesAgent invocar
create_or_update_lead cuando el visitante deja datos voluntariamente.
Integración con productos cliente
Sección titulada «Integración con productos cliente»| Producto | Path proxy en cliente | Cómo invoca |
|---|---|---|
emahealth | /api/chat | Widget chatbot del website |
emaclinic | /api/emi/chat (con apiFetch) | EmiDrawer + EmiActions en ContextPanel |
emavault | /api/emi/chat (con apiFetch) | EmiDrawer en header |
emalab | /api/emi/chat (planned) | EmiDrawer (Wave 5+) |
emapath | /api/emi/chat (planned) | EmiDrawer (Wave 6) |
El cliente nunca llama a emi.emahealth.io directamente desde el
browser: siempre pasa por su propio worker (proxy) para incluir el token
de Supabase del usuario y el X-Tenant-Id cuando corresponde.