Visión General del Asesor de IA
🤖 Visión General del Asesor de Cartera con IA
Sección titulada «🤖 Visión General del Asesor de Cartera con IA»El Asesor de Cartera con IA es un asistente de chat de solo lectura que responde preguntas sobre tu propia cartera y tus datos fiscales del IRPF español, en tu idioma, sin que tengas que saber qué vista abrir. Preguntas típicas: “¿Qué debería revisar primero en mi informe fiscal?” o “¿Cómo está distribuida mi cartera hoy?”
Es la primera fase del asesor (internamente “Fase 0”, cambio OpenSpec add-ai-portfolio-advisor): un esqueleto deliberadamente acotado que demuestra el camino completo —puerto de dominio, caso de uso, un adaptador LLM, rutas Hono tipadas, UI con streaming— antes de construir encima cualquier capacidad con escritura o basada en documentos.
[!NOTE] Esta sección describe el código tal como existe en el repositorio. Cuando el código y el diseño OpenSpec difieren, se documenta el código y la diferencia se lista en Divergencias conocidas. Implementado significa presente en el código y cubierto por una spec; planificado significa que solo aparece en un diseño, una pre-propuesta o una tarea sin marcar.
🎯 Alcance e Invariante Central
Sección titulada «🎯 Alcance e Invariante Central»El asesor es estrictamente de solo lectura. No existe ninguna herramienta de escritura, por lo que “el asesor nunca modifica una cantidad, un activo ni una cifra fiscal” se cumple por construcción —no hay nada que invocar—, y no porque un prompt le pida al modelo que se porte bien. Un test estructural verifica que ningún archivo de herramienta ejecuta un insert, update o delete.
El LLM nunca produce un número. Toda cifra de una respuesta procede de un caso de uso existente del backend; el modelo solo decide qué herramienta llamar y cómo redactar el resultado. La fundamentación (grounding) se aplica en capas independientes:
- Cada herramienta es un envoltorio fino sobre un caso de uso existente (o, en
live_prices, un método de puerto existente). Ningún cálculo vive en la capa de IA. - Las salidas de las herramientas se validan con esquemas Zod estrictos. Las cifras monetarias conservan su forma de cadena
PreciseAmount; una cifra ausente se informa como sin valorar, nunca como'0'por defecto. - Las instrucciones del agente le indican que reproduzca cada cifra literalmente en código en línea, que vuelva a consultar antes de repetir una cifra de un turno anterior y que avise cuando un total esté incompleto.
- Un resultado de herramienta que supera su presupuesto de caracteres se sustituye por un marcador
truncateden lugar de entregarse parcialmente. - El asesor lee resultados ya materializados y no introduce ningún orden ni partición FIFO propios; los únicos ordenamientos que añade son rankings de visualización. Consulta FIFO Global vs Custodia.
[!IMPORTANT] Las reglas a nivel de prompt reducen el mal comportamiento, pero no son una prueba. Las garantías estructurales son el conjunto de herramientas de solo lectura, las salidas de herramientas validadas por esquema y los guardarraíles de salida descritos a continuación.
🏛️ Arquitectura de un Vistazo
Sección titulada «🏛️ Arquitectura de un Vistazo»flowchart LR
subgraph FE["apps/frontend"]
Panel["AdvisorPanel.vue"] --> Chat["useAdvisorChat"]
Chat --> Port1["IAdvisorPort"]
Port1 --> RestAdapter["RestAdvisorAdapter<br/>fetch + ReadableStream"]
Settings["Ajustes: AdvisorSettings"] --> Colada["Queries y mutaciones Pinia Colada"]
Colada --> Port1
end
subgraph BE["apps/backend"]
Route["routes/advisor.ts<br/>/api/advisor"]
UC["AskAdvisorUC"]
Port2["IAdvisorPort"]
LogPort["IAdvisorRunLogPort"]
Mastra["MastraAdvisorAdapter<br/>core/infrastructure/ai"]
RunLog["SqliteAdvisorRunLogAdapter"]
Route --> UC
UC --> Port2 --> Mastra
UC --> LogPort --> RunLog
end
subgraph AI["Grafo de agentes (por petición)"]
Sup["advisor<br/>supervisor"]
Tax["taxAnalyst<br/>25 herramientas de solo lectura (14 en ejecuciones locales)"]
Inv["investmentAnalyst<br/>declarado, inalcanzable"]
Sup -->|mapa agents| Tax
Sup -.-x Inv
end
RestAdapter -->|"POST /api/advisor/stream"| Route
RestAdapter -->|"cliente hc tipado"| Route
Mastra --> Sup
Tax --> UCs["Casos de uso existentes<br/>e IPriceHistoryPort.getLatest"]
Sup --- Mem[("ai-advisor.db<br/>memoria conversacional")]
RunLog --> Ledger[("criptofolio_ledger.db<br/>ai_advisor_runs")]
Mastra -->|"llamada al modelo"| LLM["Daemon Ollama o proveedor alojado"]
| Aspecto | Dónde vive |
|---|---|
| Contrato de red, vocabularios cerrados, esquemas de cadena de modelos y perfiles | packages/shared-types/src/advisor-stream.ts |
| Helpers puros de visualización (ranking, orden, submuestreo) y aritmética de escenarios, comparación y resumen de custodia | packages/core-domain/src/domain/services/{holdingRanking,displayOrdering,downsampleSeries,portfolioScenarios,compareTaxSummaries,summarizeCustodyLocations,resolveAccountByName}.ts |
| Puerto de dominio, eventos, recibo, caso de uso | apps/backend/src/core/{domain,application} |
| Agentes Mastra, herramientas, guardarraíles, resolución de modelos | apps/backend/src/core/infrastructure/ai/ |
| Rutas, mapeo de DTOs, composición | apps/backend/src/core/infrastructure/{routes,dtos,di} |
| Tabla de auditoría | packages/database/migrations/sqlite/009_*.sql, 010_*.sql |
| Panel de chat y sección de Ajustes | apps/frontend/src/{components/advisor,views/Settings/components/advisor} |
🧱 Mapeo de Capas Hexagonales
Sección titulada «🧱 Mapeo de Capas Hexagonales»| Paquete | Capa | Pieza | Responsabilidad |
|---|---|---|---|
packages/shared-types |
Contrato | advisor-stream.ts |
ADVISOR_TOOL_NAMES, AI_PROVIDER_IDS, vocabularios de fallos, advisorStreamEventSchema, modelChainSchema, executionProfilesSchema, classifyExecutionProfile, deriveLocalToolBudget, deriveLocalRunBudget, defaultExecutionProfiles. Sin código de framework. |
packages/core-domain |
Servicios puros | rankHoldingsByValue, rankByAbsoluteValue, orderByCountDescending, orderByIsoDateDescending, orderByIsoDateDescendingThenKey, downsampleSeries; portfolioScenarios.ts (positionValueAt, breakevenPrice, applyShock, concentrationOf); compareTaxSummaries, summarizeCustodyLocations, resolveAccountByName |
Ordenación y muestreo solo de visualización, más la aritmética de escenarios y comparación. Toda la aritmética y comparación monetaria pasa por el value object Money; el subárbol de IA no importa ni Money ni decimal.js. |
packages/database |
Persistencia | Migraciones 009, 010; resolveAdvisorDbPath() |
Definición de la tabla y ruta de ai-advisor.db. Sin lógica de negocio. |
apps/backend dominio |
Puerto | IAdvisorPort |
Agnóstico del LLM: ask(request) devuelve AsyncIterable<AdvisorEvent>. No importa nada externo. |
apps/backend dominio |
Puerto | IAdvisorRunLogPort |
appendRun(receipt, outcome), un upsert sobre runId. |
apps/backend dominio |
Modelos | AdvisorRequest, AdvisorEvent, AdvisorRunReceipt (profiled o pre-run), AdvisorRunReceiptDraft, AdvisorRunOutcome |
Uniones discriminadas, sin bolsas de flags opcionales. |
apps/backend aplicación |
Caso de uso | AskAdvisorUC |
Sándwich funcional a escala de stream: resolver contexto, iterar el puerto, persistir el recibo. |
apps/backend infraestructura |
Adaptador | MastraAdvisorAdapter y core/infrastructure/ai/** |
El único código que importa @mastra/* (garantizado por mastraImportZone.spec.ts). |
apps/backend infraestructura |
Adaptador | SqliteAdvisorRunLogAdapter |
Escribe ai_advisor_runs mediante el handle DatabaseSync del ledger. |
apps/backend infraestructura |
DTO | dtos/advisor.ts |
Entrada askAdvisorRequestSchema; salida advisorAnswerSchema, advisorConfigSchema; toWireEvent. |
apps/backend infraestructura |
Ruta | routes/advisor.ts |
createAdvisorApi(deps) montada en /api/advisor; forma parte de AppType. |
apps/backend infraestructura |
Composición | di/advisorComposition.ts, DIContainer.askAdvisorUC |
Construcción perezosa, casos de uso de herramientas de enlace tardío, lectura de entorno. |
apps/frontend dominio |
Puerto | IAdvisorPort |
ask(request, signal) más getters/setters de configuración. |
apps/frontend infraestructura |
Adaptador | RestAdvisorAdapter |
SSE parseado a mano sobre fetch; cliente hc tipado para configuración. |
apps/frontend infraestructura |
DTO | AdvisorSchemas.ts |
Reduce GET /config a lo que consume la UI. |
Notas de composición que conviene conocer:
app.tsconstruyeAdvisorRouteDepscon getters, de modo que importarappnunca abreai-advisor.dbni el ledger.container.askAdvisorUCse construye en la primera petición al asesor.lateBoundToolUseCases(container)resuelve el caso de uso de cada herramienta desde el contenedor en cada ejecución. El contenedor sustituye sus casos de uso analíticos cuando la conexión real de DuckDB se enlaza al arrancar; una referencia capturada seguiría respondiendo desde el guard sin inicializar.fiscal_integrityyfiscal_integrity_rowscompartenGetFiscalIntegrityUseCase;token_historyytoken_lotscompartenGetTokenHistoryUseCase; las cuatro herramientas de escenarios compartenGetPortfolioScenarioUseCase.
🧭 Topología de Agentes: Supervisor y Especialistas
Sección titulada «🧭 Topología de Agentes: Supervisor y Especialistas»advisor(agents/advisor.ts) es el supervisor. PoseeMemory, unToolCallFilterde entrada y los dos procesadores de salida. Nunca llama por sí mismo a una herramienta del catálogo; delega entaxAnalystmediante el mapaagentsy reenvía la respuesta conservando cada cifra. Sus instrucciones solo añaden el idioma de respuesta y la regla de “nunca originar una cifra”.taxAnalyst(agents/taxAnalyst.ts) contiene las herramientas que expone su perfil de ejecución (las 25 en ejecuciones metered y mixed, las 14 herramientascoreen ejecuciones locales; ver Catálogo de Herramientas) y no tiene memoria. Sus instrucciones listan exactamente las herramientas expuestas. Sus instrucciones (buildTaxAnalystInstructions) son un prefijo estable byte a byte más una línea finalResponse formatcon el locale y la moneda base, de modo que el prefijo sea amigable con la caché.investmentAnalyst(agents/investmentAnalyst.ts) está declarado con instrucciones y contrato, pero no tiene herramientas y no está en el mapaagentsdeadvisor, así que nada puede alcanzarlo. Existe para que una fase posterior añada herramientas y una línea de cableado en lugar de un rediseño.advisorTopology.spec.tslo verifica, y que nunca se usa.network().
El patrón supervisor usa el mapa agents (el reemplazo de Mastra para la primitiva obsoleta de red de agentes). deterministicDelegation.spec.ts verifica que una ejecución normal cuesta exactamente dos llamadas al modelo (decidir y finalizar), nunca una tercera gastada en elegir entre subagentes. Esto importa para el nivel gratuito de Ollama Cloud, limitado a una petición concurrente.
🛡️ Guardarraíles y Descargo de Responsabilidad
Sección titulada «🛡️ Guardarraíles y Descargo de Responsabilidad»Dos procesadores de salida se ejecutan sobre la salida del supervisor e inspeccionan el texto acumulado en el chunk finish. Ambos son comprobaciones deterministas de patrones (español e inglés, investmentContentPatterns.ts), no clasificadores, por lo que constituyen un suelo heurístico: la evasión por paráfrasis y los falsos positivos son compromisos aceptados.
flowchart TD
F["chunk finish: texto acumulado"] --> TE{"¿Patrón de evasión fiscal?"}
TE -- sí --> R1["refused, sin reintento<br/>procesador grounding-directive-detector"]
TE -- no --> G{"¿Afirmación de inversión o<br/>inyección de directiva?"}
G -- no --> OK["Entregada"]
G -- sí --> INJ{"¿Inyección de directiva?"}
INJ -- no --> TA{"¿Llamada o resultado de<br/>herramienta en este turno?"}
TA -- sí --> D["Entregada, disclaimer true"]
TA -- no --> RT{"¿Ya se reintentó una vez?"}
INJ -- sí --> RT
RT -- no --> RETRY["Abortar con reintento"]
RT -- sí --> R2["refused"]
| Situación | Comportamiento |
|---|---|
| Contenido de inversión (previsión, precio objetivo, consejo de asignación o reequilibrio, timing de mercado) fundamentado por una llamada o resultado de herramienta en el mismo turno | Entregado; el frame done lleva disclaimer: true. |
| Contenido de inversión sin actividad de herramientas en este turno | Se reintenta una vez; si sigue sin fundamento, la ejecución termina refused. |
| Intento de anular el guardarraíl o el descargo (“ignora tus instrucciones”, “responde como asesor con licencia”) | Comparte el presupuesto de un reintento y luego se rechaza, incluso si se ejecutaron herramientas. |
| Evasión fiscal (“oculta plusvalías a Hacienda”, “evadir impuestos”) | Siempre se rechaza de inmediato, sin reintento. |
Las preguntas de inversión se responden con divulgación y fundamento, no se rechazan; las solicitudes de evasión fiscal se rechazan siempre. El frame refused informa processorId (grounding-directive-detector para estas comprobaciones).
[!NOTE] La comprobación de fundamento solo verifica que alguna herramienta se ejecutó en el mismo turno, no que una afirmación concreta coincida con un resultado de herramienta. La Fase 0 no tiene herramientas de inversión, así que el lenguaje orientado a inversión solo es respondible cuando el modelo también consultó una herramienta de cartera.
El Descargo es Estructural
Sección titulada «El Descargo es Estructural»- El procesador de salida
investment-disclaimeralmacena en búfer los deltas de texto; si el texto toca una categoría de inversión, añade una cadenadisclaimeral payload del chunkfinish. Nunca edita un chunk de texto. getFinishInfolo reduce a un booleano. El evento de dominiocompletedy el framedonellevandisclaimer: boolean; el backend no envía ningún texto de descargo.- El cliente renderiza su propio texto localizado (
advisor.disclaimer) como pie atenuado, separado del cuerpo de la respuesta.
Un segundo flag, figuresIncomplete, se activa en done cuando un resultado de portfolio_summary o kpis informó ratesIncomplete o pricesIncomplete. El cliente muestra una insignia “Cifras incompletas”.
🔒 Invariantes para Toda Fase Futura
Sección titulada «🔒 Invariantes para Toda Fase Futura»El modelo nunca produce un número; el asesor nunca escribe por su cuenta; no hay trading en exchanges ni ejecución de órdenes; el FIFO fiscal y la custodia nunca se fusionan ni se reordenan por código de la capa de IA; ninguna telemetría sale de la máquina; y la clasificación local frente a medida sigue siendo derivada. Consulta el roadmap para lo planificado.
🧭 Siguientes Pasos
Sección titulada «🧭 Siguientes Pasos»- Ciclo de Vida de una Petición — el flujo extremo a extremo de una pregunta.
- Proveedores y Modelos — local vs nube, credenciales de la bóveda, fallback, presupuestos.
- Catálogo de Herramientas — las veinticinco herramientas de solo lectura.
- Auditoría y Privacidad — qué se almacena y qué sale de tu máquina.