Cómo la asistente Nebulina del ERP responde con un proveedor de IA real (DeepSeek) configurado por tenant en nebula-tasks: el front llama por el gateway y con el token del usuario, el servidor arma el contexto de la opción abierta con un prefijo estable para aprovechar el caché del proveedor, controla la cuota mensual y los topes, y nunca expone la clave.
Versión: 1.0 · Fecha: 28 de octubre de 2026 · Arquitecto: Carlos Alberto Torres Camargo
Estado: IMPLEMENTADO en develop (nebula-tasks 1.8.0 a 1.8.2). Este documento describe lo construido.
Origen: encargo E-2026-10-007 del tablero (decisión D-2026-10-017), con su detalle en Documentation/centrica/00_workspace_ia/encargos/nebulina/ENCARGO_NEBULINA_IA.md; contexto por opción en ENCARGO_AI_CONTEXT_OPCIONES.md.
Cómo leer este documento. Cada afirmación sobre lo construido cita su archivo (sección 13). Lo que es inferencia o riesgo observado va marcado así.
Que Nebulina deje de responder con texto simulado y use una IA real, con estas órdenes del arquitecto (22-oct-2026, citadas en el encargo):
| # |
Orden |
| O-1 |
«simappe-client ya tiene un api para ia, y SAR la usa … quiero usar esa api de catcsoft con la configuración que tiene SAR» |
| O-2 |
«Fase 1 pero le dije use la tabla de simappe-admin obvio para que el tenant funcione debe estar en el tenant del cliente así que hay que pasar eso a un servicio de nebula creo nebula-task» |
| O-3 |
«1 la clave la pongo yo 2. se llama como todo el ecosistema es decir pasan por el gateway. y con token» |
| O-4 |
«debe poder soportar contexto es decir las capacidades de deepseek para que sea eficiente» |
| O-5 |
«ojo que esto debe tener controles de límites así como ya se hace en otras implementaciones» |
Simappe no se toca (R-C01): los precedentes de simappe-client, SimappeShared y simappe-shared se copiaron y adaptaron a código de Nebula (encargo, «Precedentes»).
| # |
Requisito |
Cómo quedó |
| RQ-01 |
Configuración en el tenant del cliente (O-2) |
Tabla TASKS.IA_PROVEEDOR_CONFIG, SequenceBusinessBaseEntity, consultas nativas con el filtro de tres partes |
| RQ-02 |
Por el gateway y con token (O-3) |
Front a /nebula-tasks/api/v1/ia; el tenant sale del token |
| RQ-03 |
La clave la registra el arquitecto (O-3) |
Columna CLAVE_API nula hasta que se registra; las respuestas solo dicen si está configurada (S/N) |
| RQ-04 |
Contexto y eficiencia (O-4) |
Historial de varios turnos; prefijo estable (sistema + ai_context + opción) para el caché de contexto; registro de tokens con acierto y fallo de caché |
| RQ-05 |
Límites (O-5) |
Límite mensual por tenant, reinicio al cambiar de mes, 429 al llegar al límite, endpoint de cuota; topes de tokens por respuesta, de historial y de largo de textos |
flowchart LR
subgraph FRONT["nebula-erp"]
NEB["NebulinaComponent<br/>libs/layout/.../nebulina"]
MENU["MenuIconService<br/>opción abierta + ai_context"]
IAS["IaService<br/>libs/shared/data-access/src/lib/ia"]
end
GW["Gateway del ecosistema<br/>token del usuario"]
subgraph TASKS["nebula-tasks · v1/ia"]
IC["IaController<br/>/api/v1/ia"]
CC["IaProveedorConfigController<br/>/api/v1/ia/configuracion"]
CONV["IaConversacionComponent"]
USO["IaUsoComponent<br/>cuota y contadores"]
VAL["IaValidator<br/>topes"]
CLI["IaProveedorClient<br/>WebClient propio, sin reintentos"]
end
ORA[("Oracle · TASKS.IA_PROVEEDOR_CONFIG<br/>vista sin la clave")]
ADMIN[("simappe_admin · admin.option.config<br/>ai_context = clave i18n")]
DS["DeepSeek<br/>POST /chat/completions · GET /models"]
AUD["Auditoría<br/>AuditLogService"]
ADMIN --> MENU --> NEB
NEB --> IAS --> GW --> IC
GW --> CC
IC --> CONV --> VAL
CONV --> USO --> ORA
CONV --> CLI --> DS
CONV --> AUD
CC --> ORA
sequenceDiagram
participant U as Usuario
participant N as Nebulina (front)
participant G as Gateway
participant C as IaConversacionComponent
participant V as IaValidator
participant S as IaUsoComponent
participant P as IaProveedorClient
participant D as DeepSeek
U->>N: Abre el panel
N->>G: GET /nebula-tasks/api/v1/ia/cuota
G->>C: cuota (tenant del token)
C->>S: activa (reinicia el mes si cambió)
S-->>N: limite, usado, agotada, activa, claveConfigurada
N->>N: sin clave, desactivada o agotada deshabilita el panel
U->>N: Pregunta
N->>G: POST /ia/conversar (pregunta, historial, aiContext, nombreOpcion)
G->>C: conversar
C->>V: topes de pregunta, contexto e historial
C->>S: activa y exigirDisponible
alt cuota agotada o sin clave
S-->>N: 429 o 409
else disponible
C->>P: sistema + aiContext + opción, historial (últimos 20), pregunta
P->>D: POST /chat/completions (max_tokens 1024, stream false)
D-->>P: texto y usage (cache hit / miss)
C->>S: contar (un UPDATE atómico)
C-->>N: respuesta, tokens, uso y límite
end
IaConversacionComponent.mensajes() arma siempre, en este orden:
system: el texto fijo SISTEMA de Nebulina, luego el ai_context de la opción y luego Opción: <nombre>. No lleva nada que cambie entre llamadas.
- El historial recortado a los
historialMaxMensajes más recientes (20), con rol user o assistant.
- La pregunta, como
user.
La API de DeepSeek no guarda la conversación: cada llamada lleva el historial completo. El caché del proveedor es automático por prefijo y su uso llega en prompt_cache_hit_tokens y prompt_cache_miss_tokens (Javadoc de IaProveedorClient).
| Clase |
Qué hace |
IaController |
conversar, cuota, probar-conexion |
IaProveedorConfigController |
CRUD de la configuración con la firma del patrón: index, read, get, create, update, delete, page, get-record, page-response |
IaConversacionComponent |
Orquesta la conversación, la cuota y la prueba de conexión; audita solo proveedor, modelo y conteos de tokens, ni la pregunta ni la respuesta |
IaUsoComponent |
Configuración activa del tenant, reinicio mensual, rechazo por límite o sin clave, cuenta atómica de cada llamada exitosa; cada método es una transacción corta, para no tener conexión abierta mientras el proveedor responde |
IaProveedorClient |
Llamada HTTP a DeepSeek; la clave viaja solo en la cabecera; del proveedor no se registra ni devuelve más que su código HTTP; sin reintentos, porque la conversación cobra tokens y no es idempotente |
IaValidator |
Topes de largo y unicidad de la configuración (un proveedor por tenant; una sola activa) |
IaConfig, IaProperties |
WebClient propio con tiempos de conexión y respuesta; propiedades nebula.ia.* |
IaProveedorConfigEntity, IaProveedorConfigView, mapper, lightloader, repositorios |
Persistencia; la vista y el record no tienen la clave |
| Propiedad |
Defecto |
limiteMensualDefecto |
1500 |
maxTokensRespuesta |
1024 |
historialMaxMensajes |
20 |
preguntaMaxCaracteres |
4000 |
aiContextMaxCaracteres |
6000 |
historialMensajeMaxCaracteres |
8000 |
razonamiento |
false (thinking deshabilitado) |
timeoutConexionMs |
5000 |
timeoutRespuestaMs |
110000 |
Los límites efectivos de DEV están en simappeconfig (commit ff5bf3e8, según la ficha del encargo).
TASKS.IA_PROVEEDOR_CONFIG (secuencia TASKS.IA_PROVEEDOR_CONFIG_SEQ): PROVEEDOR, CLAVE_API, MODELO, URL_BASE, LIMITE_MENSUAL, USO_MENSUAL, FECHA_REINICIO, ACTIVO (S/N, sin status de negocio), CONFIGURACION (JSON), TOKENS_ENTRADA_MES, TOKENS_CACHE_ACIERTO_MES, TOKENS_CACHE_FALLO_MES, TOKENS_SALIDA_MES, más el tenant de la base.
| Script |
Contenido |
DEV |
QA |
oracle/tasks/ddl/V2026_10_017 |
Tabla |
Sí |
Sí (25-oct) |
oracle/tasks/dml/V2026_10_018 |
Fila deepseek por empresa de los clientes 7 y 9, sin clave |
Sí |
No, por orden del arquitecto |
oracle/tasks/vistas/V2026_10_019 |
Vista sin la clave |
Sí |
Sí (25-oct) |
oracle/tasks/dml/V2026_10_020 |
Cuota agotada en 9/9 para la captura |
Sí |
No, por orden del arquitecto |
i18n/tasks/023, i18n/front/144 |
Mensajes del servidor y del panel |
Sí |
Sí (26-oct) |
Estado por ambiente tomado de la ficha E-2026-10-007 del tablero.
| Pieza |
Dónde |
Qué hace |
IaService |
libs/shared/data-access/src/lib/ia/ia.service.ts |
Base /nebula-tasks/api/v1/ia; espera de 120 s al conversar (por encima de los 110 s del servidor, para que el corte sea suyo) y 10 s para la cuota; mismos topes que el servidor; traduce el estado HTTP a un fallo propio |
NebulinaComponent |
libs/layout/src/lib/feature-layout/src/lib/nebulina/ |
Al abrir consulta la cuota; manda la pregunta, el historial del panel y, si el contexto está activo, el ai_context traducido y el nombre de la opción |
MenuIconService |
libs/shared/data-access/src/lib/menu/menu-icon.service.ts |
Publica el nombre y la clave ai_context de la opción abierta |
| Operación |
Ruta |
Respuesta |
| Conversar |
POST /api/v1/ia/conversar |
IaConversarResponse: texto, proveedor, modelo, tokens (entrada, caché acierto, caché fallo, salida), uso y límite |
| Cuota |
GET /api/v1/ia/cuota |
IaCuotaResponse: límite, usado, restante, porcentaje, agotada, activa, clave configurada, fecha de reinicio y próximo reinicio, tokens del mes |
| Probar conexión |
POST /api/v1/ia/probar-conexion |
IaPruebaConexionResponse: conectado S/N y código HTTP del proveedor; usa GET /models, no cobra tokens ni cuenta en la cuota |
| Configuración |
/api/v1/ia/configuracion + rutas del patrón |
IaProveedorConfigDto / IaProveedorConfigResponse, sin la clave |
| HTTP |
Causa en el servidor |
Fallo en el front |
¿Deshabilita el panel? |
| 400 |
Un texto pasa su tope |
error |
No |
| 404 |
El tenant no tiene configuración |
sin-configuracion |
Sí |
| 409 |
Sin la clave del proveedor |
clave-sin-registrar |
Sí |
| 429 |
Cuota del mes agotada |
cuota-agotada |
Sí |
| 502 |
El proveedor rechazó la clave o el saldo (401, 402, 403) |
clave-rechazada |
No |
| 503 |
Configuración desactivada, o proveedor no disponible |
no-disponible; el front consulta la cuota para distinguir y, si está desactivada, deshabilita |
Según la cuota |
| 504 |
El proveedor no respondió a tiempo |
tiempo-agotado |
No |
El mensaje que ve el usuario siempre sale del i18n del front, nunca del servidor (IaService).
flowchart TD
A["Llega conversar"] --> B{"Textos dentro de su tope"}
B -- no --> E400["400"]
B -- sí --> C{"Configuración del tenant"}
C -- no hay --> E404["404"]
C -- desactivada --> E503["503"]
C -- activa --> R{"¿Cambió el mes?"}
R -- sí --> RS["Uso y tokens a cero"] --> L
R -- no --> L{"uso >= límite"}
L -- sí --> E429["429"]
L -- no --> K{"¿Hay clave?"}
K -- no --> E409["409"]
K -- sí --> P["Llamada al proveedor<br/>max_tokens 1024, historial 20"]
P -- falla --> EP["502 / 503 / 504<br/>no cuenta"]
P -- éxito --> U["UPDATE atómico:<br/>uso + 1 y tokens"]
Una llamada que falla no cuenta contra el límite (Javadoc de IaConversacionComponent).
| Con |
Relación |
simappe-client AiGatewayComponent |
Precedente de cuota, reinicio mensual y rechazo; copiado y adaptado, no usado |
SimappeShared AiClient, SimappeVote AiChatOrchestratorComponent |
Precedentes de llamada y orquestación |
simappe-shared AiWebService |
Precedente del servicio del front |
admin.option.config |
Guarda la clave i18n ai_context de cada opción; llega al ERP por el menú (ENCARGO_AI_CONTEXT_OPCIONES.md) |
DISENO_TECNICO_CENTRO_DE_TAREAS.md |
Comparte el servicio nebula-tasks y el esquema TASKS; la IA no corre como tarea |
- Tenant: la configuración se lee con el filtro de tres partes; el tenant sale del token.
- Clave: nunca sale en una respuesta ni en un log (
@ToString.Exclude, vista y record sin la clave); la registra el arquitecto.
- Privacidad: la auditoría guarda proveedor, modelo y tokens; no la pregunta ni la respuesta.
- Caché del proveedor: depende de que el prefijo no cambie; cualquier dato variable en el mensaje de sistema lo rompe.
- Sin reintentos: cada llamada cobra tokens.
- Formato de respuesta: el mensaje de sistema pide texto plano, sin markdown.
- Idioma: Nebulina responde en el idioma de la pregunta (mensaje de sistema).
- La fila de configuración del tenant existe y está activa.
- La clave del proveedor está registrada.
- Las opciones de menú tienen su
ai_context y su texto i18n para que la respuesta tenga contexto (si no, responde sin él).
- La salida HTTPS a
api.deepseek.com (o a la URL_BASE configurada) está permitida desde el contenedor (inferencia: no se verificó la red).
| # |
Riesgo |
Evidencia o naturaleza |
Estado |
| R-01 |
Endpoints de configuración sin restricción por rol |
Ficha E-2026-10-007, «Deuda»: no hay opción de menú para gestionarla |
Abierto |
| R-02 |
Clave guardada en una columna sin cifrado |
IaProveedorConfigEntity: CLAVE_API es String con @Column, sin convertidor (observado en la entidad; no se revisó cifrado a nivel de base) |
Por decidir |
| R-03 |
Llamadas simultáneas pueden pasar el límite por unas cuantas |
La verificación (exigirDisponible) ocurre antes de la llamada y la cuenta después (inferencia del flujo) |
Aceptable para cuota mensual; por confirmar |
| R-04 |
Clave no verificada en DEV ni QA |
Ficha: «no verificado el 26-oct» |
Pendiente del arquitecto |
| R-05 |
Datos 018 y 020 fuera de QA |
Ficha, por orden del arquitecto |
Decisión del arquitecto |
| R-06 |
Dependencia de un solo proveedor |
URL_BASE_DEEPSEEK por defecto y cuerpo con el formato de DeepSeek (thinking, prompt_cache_*) |
Otro proveedor exigiría adaptar el cliente |
| R-07 |
Sin ai_context, respuestas genéricas |
Encargo de contexto por opción con etapas de redacción y aprobación |
Según avance de ese encargo |
| Componente |
Repo |
Estado |
Paquete v1/ia |
nebula-tasks |
En develop |
DTO y records de IA (tasks/ia/dto, tasks/ia/record) |
nebula-models |
En develop |
| Tabla, vista y datos |
nebula-erp-scripts-db-i18n/oracle/tasks 017 a 020 |
DEV completo; QA 017 y 019 |
| i18n |
i18n/tasks/023, i18n/front/144 |
DEV y QA |
| Límites |
simappeconfig |
DEV |
IaService y Nebulina |
nebula-erp |
En develop |
| Clave del proveedor |
— |
La registra el arquitecto |
- Herramientas (function calling) ni acceso de la IA a datos del tenant: la IA responde solo con el contexto de la opción y la conversación.
- Respuestas en streaming (
stream: false).
- Pantalla de administración de la configuración (ver R-01).
| Afirmación |
Fuente (origin/develop salvo indicación) |
| Órdenes y precedentes |
Documentation/centrica/00_workspace_ia/encargos/nebulina/ENCARGO_NEBULINA_IA.md |
| Estado por ambiente y deuda |
Documentation/centrica/00_workspace_ia/tablero/encargos/E-2026-10-007_nebulina-ia-por-tenant.md |
| Endpoints |
nebula-tasks/.../v1/ia/controller/IaController.java (50, 124, 145, 165) e IaProveedorConfigController.java (64 a 280) |
| Prefijo, auditoría, mensaje de sistema |
nebula-tasks/.../v1/ia/component/IaConversacionComponent.java (SISTEMA, mensajes(), conversar()) |
| Cuota, reinicio, rechazo, cuenta |
nebula-tasks/.../v1/ia/component/IaUsoComponent.java |
| Llamada a DeepSeek, sin reintentos, fallos |
nebula-tasks/.../v1/ia/client/IaProveedorClient.java |
| Topes |
nebula-tasks/.../v1/ia/validator/IaValidator.java (81 a 93) |
| Propiedades |
nebula-tasks/.../config/IaProperties.java (27 a 59) |
| UPDATE atómico |
nebula-tasks/.../v1/ia/repository/IaProveedorConfigRepository.java (117 a 125) |
| Tabla y columnas |
nebula-tasks/.../v1/ia/entity/IaProveedorConfigEntity.java (47 a 104) |
| Versión |
nebula-tasks/pom.xml: 1.8.2-SNAPSHOT |
| Front |
nebula-erp: libs/shared/data-access/src/lib/ia/ia.service.ts; libs/layout/.../nebulina/nebulina.component.ts (114 a 117, 183, 231, 294 a 307) |
ai_context por opción |
Documentation/centrica/00_workspace_ia/encargos/nebulina/ENCARGO_AI_CONTEXT_OPCIONES.md |
| Versión |
Fecha |
Autor |
Descripción |
| 1.0 |
2026-10-28 |
Carlos Torres |
Documento de arquitectura de lo construido, a partir del encargo E-2026-10-007 y el código en develop. |