Ir al contenido

Modelo de datos y tools

A diferencia de Phase 1, Wave 3 sí persiste dos tipos de datos en emashared:

  1. Activación y rate limit (tenant_emi_access) — qué apps puede invocar cada tenant.
  2. 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.

TablaPropósito
tenant_emi_accessActivación per-tenant per-app: status, daily_invocation_limit, fecha alta.
emi_invocationsAudit log: timestamp, app, agent, tenant_id, status (success/denied/error), turn_count, input/output tokens, cost_usd, summary del context (sin PHI).
tenant_aidbox_credentialsCredenciales Aidbox encriptadas con AES-GCM por tenant: client_id, client_secret_ciphertext, encryption_key_version.

Wave 3 le dio a SalesAgent capacidad de capturar leads:

ToolQué hace
create_or_update_leadUpsert 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.
  • source default = emi_chat si el agente no lo especifica.
  • Si lead_activities falla, el lead_id igual se devuelve (priority 1 = persistir el lead).

Acceden a Aidbox con scoping por tenant. Las credenciales se decryptan runtime usando aidbox-credentials.ts.

ToolQué hace
list_appointmentsAgenda del día o rango
get_patient_summaryCondiciones, alergias, medicamentos, órdenes activas, flags GES
search_patientsBúsqueda por nombre o RUT
list_alertsENO, valores críticos, plazos GES
list_encountersWorklist del día (paciente, status, tipo, hora)
get_encounter_tasksTasks y formularios SDC asociados a un encuentro
evaluate_gesAplicación de criterios GES (70 patologías)
get_observationsLabs, vitales (FHIR Observation)
get_diagnostic_reportsReportes diagnósticos
list_prescriptionsMedicamentos activos del paciente
get_dashboard_summaryKPIs operativos de la clínica

Acceden a Supabase del proyecto Vault (emahub):

ToolQué hace
get_finance_summaryMRR, gastos, EBITDA, margin %, runway, BUPA
list_expensesGastos por categoría/período (fijos vs variables)
list_leadsPipeline de 7 etapas con probability weighted
get_team_hoursTracking horas por persona/fecha
list_obligationsVencimientos legales y fiscales
list_editorialCalendario editorial (status + publish dates)

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.

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}
}

No

Tool Support invoca

fhirRequest tenantId

¿credenciales

en cache · TTL 5 min?

Bearer token listo

SELECT * FROM

tenant_aidbox_credentials

AES-GCM decrypt

con AIDBOX_SECRET_KEY_V_n_

Almacenar en cache

+ TTL 5 min

OAuth client_credentials

token Aidbox

Aidbox

Organization id /fhir/...

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).

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.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.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.

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.

ProductoPath proxy en clienteCómo invoca
emahealth/api/chatWidget 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.