Ir al contenido

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.


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


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}

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.ts construye AdvisorRouteDeps con getters, de modo que importar app nunca abre ai-advisor.db ni el ledger. container.askAdvisorUC se 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_integrity y fiscal_integrity_rows comparten GetFiscalIntegrityUseCase; token_history y token_lots comparten GetTokenHistoryUseCase; las cuatro herramientas de escenarios comparten GetPortfolioScenarioUseCase.

🧭 Topología de Agentes: Supervisor y Especialistas

Sección titulada «🧭 Topología de Agentes: Supervisor y Especialistas»
  • advisor (agents/advisor.ts) es el supervisor. Posee Memory, un ToolCallFilter de entrada y los dos procesadores de salida. Nunca llama por sí mismo a una herramienta del catálogo; delega en taxAnalyst mediante el mapa agents y 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 herramientas core en 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 final Response format con 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 mapa agents de advisor, 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.ts lo 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.

  1. El procesador de salida investment-disclaimer almacena en búfer los deltas de texto; si el texto toca una categoría de inversión, añade una cadena disclaimer al payload del chunk finish. Nunca edita un chunk de texto.
  2. getFinishInfo lo reduce a un booleano. El evento de dominio completed y el frame done llevan disclaimer: boolean; el backend no envía ningún texto de descargo.
  3. 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”.


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.