Ir al contenido

Arquitectura

Backends

emaemi · Cloudflare Worker

Clientes

501

tools

tools

tool

emahealth.io

chatbot widget

emaclinic

EmiDrawer + Actions

emalab

EmiDrawer

emavault

EmiDrawer

emapath

planned

emawell

not implemented

authMiddleware

API key + X-Tenant-Id

access-check

tenant_emi_access + rate limit

audit

emi_invocations log

Router

app → agent

SalesAgent · GA

lead_management

SupportAgent · GA

aidbox tools

AnalyticsAgent · GA

vault tools

LabAssistantAgent · alpha

PathologyAgent · planned

WellnessCoach · not impl

lib/claude.ts

fetch + retry + tool loop

cost.ts

USD por modelo

aidbox-credentials

AES-GCM decrypt + cache

Claude API

haiku-4-5

Aidbox FHIR

ORGBAC scoped

credenciales per-tenant

Supabase emashared

tenant_emi_access · emi_invocations ·

tenant_aidbox_credentials

Supabase Vault

finanzas · pipeline · leads

emaemi/
├── src/
│ ├── index.ts # Hono app, CORS, /api/health
│ ├── env.d.ts # Cloudflare bindings (Wave 3 surface)
│ ├── api/
│ │ └── chat.ts # POST /api/chat — access + audit + agent
│ ├── orchestrator/
│ │ └── router.ts # AppName → AgentName
│ ├── agents/
│ │ ├── base.ts # BaseAgent (tool loop + usage agg + enrichResponse)
│ │ ├── sales.ts # emahealth — lead_management
│ │ ├── support.ts # emaclinic — Aidbox tools per-tenant
│ │ ├── analytics.ts # emavault — Vault Supabase
│ │ ├── lab.ts # emalab — alpha placeholder
│ │ └── pathology.ts # emapath — planned placeholder
│ ├── middleware/
│ │ └── auth.ts # API key + X-Tenant-Id
│ ├── lib/
│ │ ├── claude.ts # Anthropic API client + runAgent loop
│ │ ├── types.ts # AppName, AgentName, ChatContext, AgentResponse
│ │ ├── permissions.ts # App → agents/tools scoping
│ │ ├── access-check.ts # tenant_emi_access + rate limit
│ │ ├── audit.ts # logInvocation + summarizeContext (privacy-safe)
│ │ ├── cost.ts # computeCost per model
│ │ ├── aidbox-credentials.ts # AES-GCM decrypt + cache TTL 5 min
│ │ ├── fhir-client.ts # ORGBAC-scoped fhirRequest/fhirQuery
│ │ └── logger.ts # JSON structured logger
│ └── tools/
│ ├── clinic.ts # FHIR queries (per-tenant)
│ ├── vault.ts # Supabase Vault queries
│ └── lead-management.ts # CRM upsert + activity log
└── tests/
auditToolClaude APIAgenteRouter(emashared)checkAccessauthMiddlewareClienteauditToolClaude APIAgenteRouter(emashared)checkAccessauthMiddlewareClientealt[stop_reason = "tool_use"][stop_reason = "end_turn"]loop[hasta end_turn · máx 5 iteraciones]alt[denied][allowed]POST /api/chatBearer + X-Tenant-Id?1Resolver AppName2¿tenant tiene activación + cuota?3SELECT tenant_emi_access + count invocaciones hoy4allowed | denied5logInvocation(status='denied')6INSERT emi_invocations7403 / rate limited8Forward (app, context)9getAgent(app)10buildSystemPrompt(locale, turn, tenantId, ...)11messages + system + tools12response13ejecutar tool con input14tool_result15append tool_result + reanudar16break17logInvocation(status='success', tokens, cost_usd)18INSERT emi_invocations19{ message, agent, turnCount, leadId? }20

Tabla en permissions.ts → APP_SCOPES:

AppNameAgenteStatusToolsTenant requerido
emahealthsalesGAlead_managementno
emaclinicsupportGAaidbox per-tenant
emavaultanalyticsGAsupabase-vaultno
emalablab_assistantalpha
emapathpathology_assistantplanned
emawellwellness_coach501

emawell retorna 501 Not Implemented explícito hasta que se construya el agente. emahealth y emavault no requieren tenant: el primero recibe visitantes anónimos del sitio público (con tracking opcional via ChatContext), el segundo es dashboard interno sin multi-tenancy.

  • Activación per-tenant: tenant_emi_access controla qué apps puede invocar cada tenant. Sin row activa → 403.
  • Rate limit: daily_invocation_limit por app por tenant; cuenta invocaciones exitosas del día (UTC).
  • Audit obligatorio: logInvocation corre siempre (success y errors) y es fail-silent — no bloquea respuestas al usuario.
  • Multi-tenancy FHIR: cada tenant tiene credenciales Aidbox propias encriptadas con AES-GCM en tenant_aidbox_credentials. Decryptadas runtime con cache TTL 5 min in-memory por Worker isolate.
  • Sin persistencia de conversaciones: stateless. Historial viene en el body de cada request.