Add functional RAG service with code mode

This commit is contained in:
OpenCode 2026-04-05 17:49:35 +02:00
commit 7600f36f48
32 changed files with 5695 additions and 0 deletions

7
RAG/.dockerignore Normal file
View file

@ -0,0 +1,7 @@
node_modules
dist
.env
.env.local
npm-debug.log*
tests
docs

13
RAG/.env.example Normal file
View file

@ -0,0 +1,13 @@
PORT=3000
NODE_ENV=development
QDRANT_URL=http://localhost:6333
QDRANT_API_KEY=
QDRANT_COLLECTION=rag_chunks
EMBEDDING_PROVIDER=openrouter
EMBEDDING_MODEL=qwen/qwen3-embedding-8b
EMBEDDING_BASE_URL=https://openrouter.ai/api/v1
EMBEDDING_API_KEY=
ANSWER_PROVIDER=openrouter
ANSWER_MODEL=openai/gpt-4.1-mini
ANSWER_BASE_URL=https://openrouter.ai/api/v1
ANSWER_API_KEY=

5
RAG/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
node_modules/
dist/
.env
.env.local
npm-debug.log*

15
RAG/Dockerfile Normal file
View file

@ -0,0 +1,15 @@
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]

View file

@ -0,0 +1,498 @@
# Bitacora de diseño del RAG
**Proyecto:** Workspace de tools IA para empresas
**Modulo:** RAG
**Ultima actualizacion:** 2026-04-02
**Ultima modificacion por:** Agente tools IA para potenciar servicios empresariales
**Estado:** Activa
---
## Proposito
Esta bitacora registra el camino de diseño que estamos siguiendo para construir el RAG.
No se limita a guardar decisiones finales. Tambien recoge:
- como se llego a una decision
- por que se prefirio una opcion frente a otra
- que posibilidades quedan abiertas para mas adelante
- que vision general da sentido al resto de documentos del modulo
Su funcion es servir como panorama vivo del diseño del RAG.
---
## Vision general actual
El RAG que se esta diseñando no busca ser una demo cerrada ni una herramienta pensada solo para este workspace.
La intencion es crear una base reutilizable que pueda:
- integrarse con proyectos de clientes
- potenciar agentes y soluciones con IA ya existentes
- servir tanto para conocimiento empresarial como para desarrollo de software
- crecer hacia un modelo de servicio consumible por distintas herramientas
La carpeta `docs/` del workspace solo se usara como primer caso real de prueba, no como limite conceptual del sistema.
Otro criterio ya explicitado es que las decisiones de la v1 deben tomarse como base estable y duradera del sistema. La evolucion futura debe venir por ampliacion de capacidades, no por sustituir continuamente decisiones nucleares ya cerradas.
---
## Casos de uso principales aceptados
### 1. Agente experto en empresa
El RAG debe poder ayudar a un agente a atender clientes durante venta, postventa, soporte y seguimiento.
Eso implica que el sistema debe poder aportar contexto sobre:
- empresa
- productos
- servicios
- formas de trabajo
- empleados, areas y responsabilidades
- informacion relevante para atender correctamente al cliente
### 2. Agente experto en proyecto o desarrollo
El RAG debe poder dar a un agente una vision util de un proyecto para:
- iniciar un desarrollo desde cero
- continuar un proyecto existente
- retomar proyectos de terceros
- comprender tecnologia, arquitectura, metodos y estructura interna
- desarrollar nuevas funcionalidades bien integradas con lo que ya existe
---
## Decisiones y razonamiento acumulado
### El RAG no debe limitarse a pregunta y respuesta
**Decision:**
El sistema no se diseñara solo como `pregunta -> respuesta`.
**Por que:**
- los casos de uso reales requieren dar contexto util a agentes, no solo contestar preguntas
- en desarrollo y continuidad de proyectos, el mayor valor esta en cargar panorama y contexto rico
- para atencion al cliente tambien puede ser util devolver contexto y no solo respuesta final
**Consecuencia:**
La salida del RAG debe contemplar al menos recuperacion de contexto, y no solo respuesta sintetizada.
---
### El RAG debe poder funcionar como servicio
**Decision:**
La base de acceso principal se esta pensando como API o endpoint.
**Por que:**
- facilita que otras apps, agentes y soluciones lo consuman
- permite pruebas inmediatas desde terminal
- encaja bien con una futura capa MCP
- es mas util para integracion que pensarlo primero como libreria interna
**Posibilidad abierta:**
- MCP como forma estandar de conexion a sistemas de IA
- CLI como capa opcional de uso interno o pruebas
**Decision tecnica posterior vinculada a esta direccion:**
La v1 se orienta a un stack basado en `Node.js + TypeScript + Express + Qdrant`, con API HTTP como base principal y proveedor de embeddings desacoplado.
**Matiz importante ya detectado:**
Desacoplar el proveedor de embeddings no evita que cambiar de modelo obligue normalmente a reindexar el contenido afectado.
Lo correcto sera:
- registrar con que proveedor, modelo y dimensiones se genero cada embedding
- mantener trazabilidad de la version de indexacion
- evitar mezclar embeddings de modelos distintos dentro de la misma logica de recuperacion
**Decision cerrada posterior:**
Se adopta `Qwen3 Embedding 8B` como modelo base estable del sistema.
**Por que se cierra asi:**
- el usuario quiere que las decisiones nucleares de la v1 nazcan como base duradera
- el modelo encaja con recuperacion textual y de codigo
- ofrece una via realista de arranque ahora y despliegue local despues
- encaja mejor con la estrategia general del RAG que un modelo puramente comercial y dependiente de proveedor cerrado
**Referencia comparativa mantenida:**
`OpenAI text-embedding-3-large` se conserva como referencia de calidad y comparacion, pero no como decision principal del sistema.
---
### La arquitectura debe separar entrada y salida
**Decision:**
Se ha aceptado que el RAG tiene al menos dos grandes usos operativos:
- entrada de informacion
- salida de informacion por consulta o recuperacion de contexto
**Por que:**
- ambas tareas son distintas y complementarias
- ayudan a pensar el sistema con responsabilidades claras
- permiten dejar preparada una arquitectura robusta y ampliable
**Ampliacion ya detectada:**
Tambien se considera necesario un futuro subsistema de actualizacion del conocimiento.
---
### Debe existir una via futura para actualizacion y versionado
**Decision:**
Aunque no se implementara de entrada, el sistema debe nacer preparado para manejar contenido cambiante.
**Por que:**
- el conocimiento empresarial y tecnico no es estatico
- parte de la informacion puede cambiar, quedar obsoleta o dejar de ser valida
- un RAG robusto no puede asumir que todo lo ingerido vale para siempre
**Idea asociada en exploracion:**
Una inspiracion parecida a git puede ser util para pensar el historico, la vigencia y la actualizacion incremental del conocimiento.
---
### El modelo no solo participa en la salida, tambien puede participar en la entrada
**Decision en formacion:**
Se deja claro que, cuando hablamos de uso de modelo, no nos referimos solo al modelo que consumira el contexto del RAG en consulta.
Tambien se contempla un modelo que participe en el proceso de entrada de informacion al RAG.
**Por que:**
- un modelo puede ayudar a entender documentos de calidad irregular
- puede identificar tema, estructura y puntos clave
- puede enriquecer la informacion que entra al sistema antes de indexarla
**Criterio adoptado:**
- no conviene depender desde el inicio de modelos caros para esta fase
- puede existir mas adelante un modelo fijo y economico dentro del pipeline de ingesta
- tambien queda abierta una fase posterior con una interfaz interactiva de alimentacion, en la que el usuario pueda elegir un modelo mas potente para procesar mejor ciertas entradas
**Direccion que sugiere esta idea:**
- fase basica: ingesta principalmente mecanica y economica
- fase posterior: ingesta asistida por modelo
- fase mas avanzada: ingesta interactiva con eleccion de modelo segun necesidad
**Matiz posterior aceptado:**
La arquitectura debe nacer preparada para recibir parametros que indiquen si la ingesta va en modo basico o en modo enriquecido/interactivo.
La idea es que:
- si no llegan parametros especiales, el sistema opere por defecto de forma mecanica y con su modelo predefinido
- si llegan parametros desde una interfaz, el sistema pueda cambiar de modelo o recibir directrices y marcado del usuario
**Consecuencia positiva de este enfoque:**
Permite que el sistema crezca hacia una ingesta mas rica sin exigir una reescritura posterior del flujo base.
---
### Identidad del conocimiento por niveles
**Decision:**
Se ha aceptado una jerarquia de identificacion con tres niveles:
- `source_id`
- `document_id`
- `chunk_id`
**Por que:**
- no basta con identificar solo la carpeta o fuente global
- los cambios reales suelen ocurrir a nivel de documento
- los chunks son unidades de recuperacion, no la identidad principal del conocimiento
**Decision relacionada:**
Los documentos sueltos tambien tendran `source_id` mediante una fuente logica por defecto, evitando valores nulos.
**Propuesta concreta ya aceptada como base de trabajo:**
- `source_id`: `src:<tenant>:<source_kind>:<source_name>`
- `document_id`: `doc:<source_id>:<document_key>`
- `chunk_id`: `chk:<document_id>:<chunk_mode>:<chunk_index>`
**Por que esta forma parece adecuada:**
- mantiene legibilidad humana
- deja espacio para multi-tenant desde ahora sin obligar a usarlo plenamente
- separa bien fuente, documento y chunk
- permite distinguir tambien el modo de chunking en el identificador del fragmento
**Aclaracion importante sobre `tenant`:**
En este diseño, `tenant` significa el espacio logico de separacion de datos dentro del servicio RAG.
No obliga a que desde el primer dia haya varios clientes activos, pero evita que la arquitectura quede cerrada a un unico contexto.
Usar `default` al inicio permite trabajar simple hoy y quedar preparados para mañana.
**Hallazgo practico posterior:**
Una misma coleccion vectorial puede servir varias fuentes distintas siempre que queden bien separadas por identificadores y por alcance de consulta.
Ejemplo ya probado:
- `docs/` del workspace como documentacion global
- `RAG/docs/` como documentacion propia del modulo RAG
La separacion operativa actual se hace por `sourceRef` y `source_id` dentro del `scope` de consulta.
Esto demuestra en pequeno el mismo patron que luego servira para separar clientes, proyectos o dominios dentro del mismo sistema.
**Decision complementaria sobre `document_key`:**
Se adopta como regla base que el documento se identifique por una clave interna normalizada, preferiblemente relativa a la fuente.
La idea es:
- usar rutas relativas cuando existan
- usar nombres normalizados para documentos sueltos
- usar referencias estables para fuentes externas o derivadas
**Por que esta decision completa bien el esquema:**
- evita depender de rutas absolutas locales
- mantiene IDs legibles y estables
- hace posible tratar con la misma logica archivos, PDFs sueltos, APIs documentales u otras fuentes futuras
**Matiz adicional aceptado:**
`document_key` no sustituye al localizador real del fichero o recurso. Su papel es servir como identidad interna estable dentro del sistema.
---
### Metadatos minimos aceptados
**Decision:**
Se ha aceptado como base este conjunto:
- `source_id`
- `source_type`
- `source_path` o `source_ref`
- `document_id`
- `chunk_id`
- `title`
- `updated_at`
- `hash`
- `status`
- `version`
- `tags`
**Por que:**
- equilibran simplicidad y preparacion para evolucion
- permiten trazabilidad
- preparan el sistema para cambios y actualizaciones futuras
- evitan tener que rediseñar demasiado pronto el modelo de datos
---
### Chunking: una sola estrategia como implementacion inicial, pero no como vision final
**Decision:**
Se empezo aceptando una estrategia comun de chunking para la v1 por simplicidad.
**Matiz importante posterior:**
Dado que el RAG tambien servira para desarrollo de software, la vision final no debe quedarse en un unico chunking universal.
**Conclusion actual:**
La arquitectura debe quedar preparada al menos para dos modos:
- `documental`
- `codigo`
**Aclaracion importante:**
El modo `codigo` no se deja de lado como direccion del sistema. Sigue siendo parte del objetivo del RAG por su uso previsto en desarrollo de software.
Lo que se prioriza primero es cerrar y construir una v1 documental estable para arrancar antes, sin renunciar a incorporar despues el modo `codigo` sobre una arquitectura ya preparada.
**Decision posterior ya implementada:**
El modo `codigo` ha quedado implementado en la v1 con:
- soporte de extensiones tecnicas habituales
- chunking por bloques top-level
- retrieve filtrable por `mode: codigo`
- answer con referencias de bloque y rango de lineas cuando aplica
**Prueba funcional ya realizada:**
se ingirio `RAG/src/` y se respondio correctamente a una consulta real sobre como se construye `source_id` en el sistema.
**Por que:**
- documentacion y codigo no se parten ni se recuperan igual de bien
- un chunking puramente documental podria aprovechar mal el codigo fuente
- el objetivo del sistema exige servir a ambos mundos
**Matiz importante ya asumido:**
Cambiar de forma relevante la estrategia de chunking en el futuro implicara normalmente reprocessar e indexar de nuevo la informacion afectada.
Esto no invalida avanzar con una v1 simple, pero refuerza la idea de elegir una base suficientemente solida desde ahora.
**Decision ya cerrada para el modo documental de la v1:**
- cortar primero por estructura natural
- subdividir por parrafos cuando haga falta
- usar corte por longitud solo como ultimo recurso
- trabajar con una referencia aproximada por caracteres
- aplicar overlap pequeno y fijo
**Valores elegidos para arrancar:**
- objetivo aproximado: 1500 caracteres
- maximo aproximado: 2200 caracteres
- overlap aproximado: 200 caracteres
**Por que se cierran ya estos valores:**
- permiten construir la v1 sin seguir aplazando decisiones basicas
- mantienen una complejidad baja
- ofrecen una base razonablemente estable para documentacion normal
- dejan abierta una futura variante `documental_largo` para libros o contenido tecnico extenso
---
### Seleccion de modo: enfoque hibrido
**Decision:**
La seleccion del modo debe ser hibrida.
**Forma acordada:**
- el consumidor puede indicar el modo explicitamente
- si no lo hace, el sistema intenta inferirlo
**Por que:**
- combina control y comodidad
- evita obligar a multiples interfaces separadas demasiado pronto
- permite a un agente elegir segun el contenido o dejar que el sistema ayude
**Modo base previsto:**
- `documental`
- `codigo`
- `auto`
---
### La salida del RAG debe contemplar contexto, no solo respuestas
**Decision:**
Se consideran validos al menos estos tipos de salida conceptuales:
- `retrieve`
- `answer`
- opcion intermedia de contexto mas sintesis
**Por que:**
- el mayor valor para agentes de desarrollo esta en recuperar contexto rico y trazable
- atencion al cliente puede beneficiarse de contexto mas respuesta sintetizada
- una salida solo orientada a respuesta dejaría corto el sistema para los usos previstos
**Decision posterior ya implementada:**
`answer` se monta sobre `retrieve`, no como un camino independiente.
**Por que:**
- mantiene una arquitectura mas limpia
- obliga a que la respuesta final siga dependiendo del contexto recuperado
- hace visible que mejorar `retrieve` mejora tambien `answer`
**Ajuste posterior relevante:**
`retrieve specific` se ha afinado para preguntas operativas frecuentes mediante subconsultas y reordenacion por intencion.
**Efecto visible:**
mejora respuestas como la consulta de pendientes del workspace, que ahora ya baja a lineas concretas del backlog en lugar de quedarse solo en la descripcion general del documento.
---
### Retrieve inicial y retrieve especifico
**Decision en formacion:**
Se ve mucho valor en separar:
- un `retrieve inicial` o de arranque
- un `retrieve especifico` para profundizar
**Idea del retrieve inicial:**
Debe comportarse como un mapa del dominio, no como una respuesta en detalle.
**Ese mapa deberia incluir:**
- vision general
- temas principales
- puntos criticos
- estructura o areas
- referencias para profundizar
**Aporte adicional aceptado:**
Algunas piezas podran marcarse explicitamente como criticas para que el sistema les de prioridad en ese contexto inicial.
**Por que:**
- evita saturar al modelo con demasiado detalle desde el principio
- da orientacion general al agente
- permite usar consultas posteriores para profundizar solo donde haga falta
**Decision posterior ya estructurada:**
El `retrieve` inicial queda entendido como un paquete de contexto de arranque con esta estructura base:
- `mode`
- `intent`
- `summary`
- `topics`
- `critical_points`
- `items`
- `follow_up_refs`
- `scope`
**Sentido de esta estructura:**
- combina panorama general y trazabilidad
- sirve tanto a agentes conversacionales como a agentes de desarrollo
- permite pasar de una carga inicial de contexto a una profundizacion posterior sin perder el mapa del dominio
**Matiz posterior aceptado:**
El `retrieve`, y en especial el `bootstrap`, debe poder recibir un alcance explicito para cargar solo el contexto de una fuente concreta, por ejemplo un workspace o proyecto determinado.
**Decision posterior ya implementada:**
El `bootstrap` deja de comportarse como una sola busqueda ampliada y pasa a usar varias subconsultas internas para construir un mapa inicial mas util.
**Por que:**
- mejora la cobertura del contexto de arranque
- recupera mejor documentos base como indice, README, pendientes o historial
- se ajusta mejor al objetivo de volver rapidamente experto a un agente sobre un workspace o proyecto concreto
---
## Posibilidades abiertas que siguen vivas
- integrar el RAG con MCP como canal de conexion a sistemas de IA
- potenciar el RAG con Obsidian como posible fuente, capa de organizacion o entorno complementario de conocimiento
- usarlo como servicio comun para distintos clientes y aplicaciones
- permitir mejoras del motor que beneficien a todos los consumidores del servicio
- evolucionar hacia algun tipo de historico o versionado inspirado en ideas parecidas a git
- introducir estrategias especificas de chunking por tipo de documento o fuente
- ampliar el modo documental con soporte especializado para PDFs complejos o escaneados mediante OCR si llegara a hacer falta
- definir memoria base de sesion mas retrieves dinamicos posteriores
---
### Los PDFs entran en la v1
**Decision:**
Los PDFs no se aplazan. Deben estar soportados desde la primera version del sistema.
**Por que:**
- en entornos reales de empresa y proyectos, mucha informacion relevante llega en PDF
- dejarlos fuera desde el inicio recortaria demasiado el valor practico de la v1
- el objetivo del RAG es ser util desde pronto en escenarios reales, no solo con Markdown o texto plano
**Matiz importante:**
El soporte inicial se entiende para PDFs con texto extraible. PDFs escaneados o de mala calidad podran requerir despues una capa adicional de OCR o tratamiento especializado.
---
## Pendientes inmediatos del diseño
1. Definir como se construiran exactamente `source_id`, `document_id` y `chunk_id`.
2. Definir la estrategia concreta del chunking inicial.
3. Definir que debe devolver exactamente `retrieve`.
4. Definir que bloques tendra el `retrieve inicial` tipo mapa de dominio.
5. Definir como se accedera a los servicios minimos del RAG desde la API.
6. Definir si la ingesta asistida por modelo estara presente desde la v1 o quedara para la siguiente fase.
---
## Rol de esta bitacora dentro del modulo
Los otros documentos del modulo deben ir recogiendo decisiones concretas por areas.
Esta bitacora, en cambio, debe conservar la panoramica del camino seguido:
- que se quiere construir
- para que se quiere construir
- como se esta razonando el diseño
- que se decide ahora
- que se deja abierto para mas adelante

533
RAG/docs/INGESTA.md Normal file
View file

@ -0,0 +1,533 @@
# Ingesta
**Proyecto:** Workspace de tools IA para empresas
**Modulo:** RAG
**Ultima actualizacion:** 2026-04-02
**Ultima modificacion por:** Agente tools IA para potenciar servicios empresariales
**Estado:** En definicion
---
## Proposito
Definir que es la ingesta dentro del sistema RAG, que responsabilidades tiene y que debe dejar preparado para el resto de modulos.
---
## Que es la ingesta
La ingesta es el punto de entrada del conocimiento al RAG.
Su funcion es recibir fuentes de informacion y convertirlas en entradas manejables para el sistema, dejandolas listas para su posterior procesado, indexacion y recuperacion.
No debe encargarse de todo el pipeline. Su objetivo es incorporar y registrar correctamente las fuentes.
---
## Responsabilidades de la ingesta
La ingesta debe ocuparse de:
1. Recibir una fuente de informacion.
2. Identificar su tipo.
3. Leer su contenido o referencia.
4. Registrar metadatos basicos.
5. Entregar el resultado a la fase de procesado.
---
## Tipos de fuente previstos
La primera version puede empezar por documentos, pero la ingesta debe nacer pensada para admitir mas tipos de fuente.
Fuentes previstas:
- archivos locales
- carpetas
- documentacion de proyecto
- codigo fuente de proyecto
- repositorios
- endpoints o APIs
- bases de datos
- otras fuentes conectables en el futuro
---
## Alcance de la primera version
La primera version de la ingesta debe centrarse en:
- leer archivos de documentacion
- leer PDFs con texto extraible
- leer codigo fuente del proyecto
- recorrer carpetas concretas
- registrar que fuente entro al sistema
- dejar preparada la extension a nuevas fuentes mas adelante
Caso practico inicial:
- carpeta `docs/` del workspace como primera fuente real de prueba
- carpeta `RAG/src/` como primera fuente real de prueba para el modo `codigo`
---
## Salida esperada de la ingesta
La ingesta no debe devolver aun conocimiento recuperable final. Debe devolver una representacion normalizada de la fuente para el siguiente modulo.
Esa salida deberia incluir como minimo:
- identificador de fuente
- tipo de fuente
- origen o ruta
- contenido bruto o accesible
- fecha de lectura
- metadatos basicos disponibles
---
## Metadatos minimos aceptados
La primera propuesta aceptada para metadatos minimos del sistema es esta:
- `source_id`
- `source_type`
- `source_path` o `source_ref`
- `document_id`
- `chunk_id`
- `title`
- `updated_at`
- `hash`
- `status`
- `version`
- `tags`
Metadatos recomendados adicionales para la capa de embeddings:
- `embedding_provider`
- `embedding_model`
- `embedding_dimensions`
No todos tendran el mismo peso en la v1, pero conviene que el diseño nazca preparado para soportarlos.
**Por que se adopta este conjunto:**
- `source_id`, `document_id` y `chunk_id` permiten trazabilidad y actualizacion por niveles.
- `source_type` y `source_path` o `source_ref` permiten entender de donde salio cada entrada.
- `title` mejora legibilidad, depuracion y futuras recuperaciones.
- `updated_at`, `hash`, `status` y `version` preparan el sistema para cambios, invalidez y evolucion de contenido.
- `tags` deja abierta una capa ligera de clasificacion sin obligar a una taxonomia compleja en la v1.
---
## Jerarquia de identificacion
Se adopta una jerarquia simple de 3 niveles:
1. `source`
- origen logico de ingesta
- ejemplos: carpeta `docs/`, carpeta `src/`, repositorio, API, tabla o coleccion externa
2. `document`
- unidad concreta dentro de la fuente
- ejemplos: `docs/HISTORIAL_SESIONES.md`, `src/app/main.ts`, `manual_cliente.pdf`
3. `chunk`
- fragmento recuperable del documento
Esta separacion permite actualizar una pieza concreta sin tener que tratar toda la fuente como si hubiera cambiado por completo.
**Por que se adopta esta jerarquia:**
- una carpeta o repositorio no debe ser la unica unidad de control, porque los cambios reales suelen ocurrir a nivel de documento.
- un mismo documento debe poder actualizarse sin reingestar toda la fuente completa.
- el chunk debe ser la unidad de recuperacion, no de identidad principal del conocimiento.
- esta estructura se parece a un sistema de versionado por niveles, y deja abierta la puerta a historico, reindexacion parcial y auditoria.
---
## Criterio para los identificadores
Se trabajara con estos tres identificadores:
- `source_id`: identifica la fuente logica
- `document_id`: identifica el documento concreto
- `chunk_id`: identifica cada fragmento recuperable
La carpeta `docs/` puede ser una `source`, pero `docs/HISTORIAL_SESIONES.md` debe tener su propio `document_id` para poder detectar cambios, reindexar y mantener trazabilidad fina.
**Por que se decide asi:**
- si `source_id` absorbiera toda la identidad, perderiamos control fino sobre los documentos internos.
- si solo identificaramos chunks, perderiamos una unidad intermedia clave para actualizacion, limpieza y observabilidad.
- separar estos tres identificadores permite mantener el sistema simple pero preparado para crecer.
---
## Propuesta concreta para construir los identificadores
Se adopta esta propuesta inicial.
### `source_id`
**Formato:**
`src:<tenant>:<source_kind>:<source_name>`
**Ejemplos:**
- `src:default:folder:workspace-docs`
- `src:default:folder:project-code`
- `src:default:manual:client-upload`
- `src:cliente-a:repo:backend-api`
- `src:cliente-a:api:java-docs`
**Que representa:**
- el origen logico de ingesta
- no un archivo concreto
- debe mantenerse estable aunque cambien documentos internos
**Que significa `tenant`:**
- es el espacio logico de separacion de datos
- sirve para distinguir clientes, proyectos, entornos o contextos aislados dentro del mismo RAG
- en la v1 puede usarse simplemente `default` cuando aun no necesitemos separar nada
**Para que nos sirve en nuestro sistema:**
- deja preparado el RAG para dar servicio a varios clientes o aplicaciones sin mezclar su informacion
- permite que una misma arquitectura y un mismo servicio RAG trabajen para varios consumidores
- facilita crecer hacia un modelo centralizado en el que mejoramos el RAG una vez y varios clientes o apps se benefician
**Ejemplo practico de multi-tenant:**
- `src:cliente-a:folder:workspace-docs`
- `src:cliente-b:folder:workspace-docs`
Ambos pueden tener un archivo llamado `docs/manual.md`, pero seguiran siendo distintos porque pertenecen a tenants diferentes.
Esto evita mezclar conocimiento de clientes o proyectos distintos dentro del mismo sistema.
**Reglas:**
- `tenant`: `default` si no hay cliente o espacio separado aun
- `source_kind`: tipo logico de fuente, por ejemplo `folder`, `repo`, `manual`, `api`, `db`
- `source_name`: nombre corto, legible y estable
### `document_id`
**Formato:**
`doc:<source_id>:<document_key>`
**Ejemplos:**
- `doc:src:default:folder:workspace-docs:docs/historial_sesiones.md`
- `doc:src:default:folder:project-code:src/app/main.ts`
- `doc:src:default:manual:client-upload:manual_cliente.pdf`
- `doc:src:cliente-a:api:java-docs:list`
**Que representa:**
- el documento o unidad concreta dentro de una fuente
- es la unidad principal para detectar cambios, desapariciones o reingesta selectiva
**Reglas:**
- usar ruta relativa si existe
- si no existe ruta, usar nombre normalizado o referencia estable
- evitar espacios y diferencias cosmeticas innecesarias en el identificador
### `chunk_id`
**Formato:**
`chk:<document_id>:<chunk_mode>:<chunk_index>`
**Ejemplos:**
- `chk:doc:src:default:folder:workspace-docs:docs/historial_sesiones.md:documental:0001`
- `chk:doc:src:default:folder:project-code:src/app/main.ts:codigo:0003`
**Que representa:**
- cada fragmento recuperable del documento
**Reglas:**
- `chunk_mode`: permite distinguir si el chunk fue generado en modo `documental` o `codigo`
- `chunk_index`: indice secuencial con padding para mantener orden legible
**Ejemplo practico simple:**
Supongamos esta fuente:
- `source_id`: `src:default:folder:workspace-docs`
Y dentro de ella dos documentos:
- `document_id`: `doc:src:default:folder:workspace-docs:docs/readme.md`
- `document_id`: `doc:src:default:folder:workspace-docs:docs/historial_sesiones.md`
Si `docs/readme.md` se parte en 3 chunks documentales:
- `chk:doc:src:default:folder:workspace-docs:docs/readme.md:documental:0001`
- `chk:doc:src:default:folder:workspace-docs:docs/readme.md:documental:0002`
- `chk:doc:src:default:folder:workspace-docs:docs/readme.md:documental:0003`
Y `docs/historial_sesiones.md` se parte en 2 chunks documentales:
- `chk:doc:src:default:folder:workspace-docs:docs/historial_sesiones.md:documental:0001`
- `chk:doc:src:default:folder:workspace-docs:docs/historial_sesiones.md:documental:0002`
El `0001` no compite con el `0001` del otro documento.
El indice solo tiene sentido dentro de su propio `document_id`.
Eso permite:
- saber a que documento pertenece cada chunk
- conservar el orden interno del documento
- distinguir chunks de distintos modos si algun dia un mismo documento se procesa de manera diferente
---
## Normalizacion recomendada
Para mantener consistencia:
- usar minusculas cuando no rompa semantica importante
- reemplazar espacios por guion bajo o guion medio segun convenga
- usar rutas relativas como referencia principal cuando existan
- evitar caracteres redundantes o variables del entorno que rompan estabilidad
- no incluir hashes dentro del identificador principal
El `hash` debe vivir como metadato, no como parte del ID. Asi el identificador se mantiene estable y el hash sirve para detectar cambios.
---
## Propuesta para `document_key`
Se adopta esta regla general:
- si el documento pertenece a una fuente con estructura de rutas, usar su ruta relativa normalizada dentro de la fuente
- si el documento no tiene ruta natural, usar un nombre o referencia estable normalizada
- si la fuente es derivada o externa, usar una referencia corta y estable que identifique la unidad documental
### Formato esperado
`document_key` debe ser:
- legible
- estable
- relativo a la fuente cuando sea posible
- suficientemente especifico para no colisionar dentro de una misma `source`
### Reglas de normalizacion
1. Usar ruta relativa respecto de la fuente cuando exista.
2. Unificar separadores a `/`.
3. Eliminar prefijos absolutos del sistema.
4. Mantener la extension del archivo si aplica.
5. Convertir a minusculas cuando no rompa semantica importante.
6. Reemplazar espacios por `_`.
7. Evitar caracteres redundantes o decorativos.
8. No incluir hashes, timestamps volatiles ni parametros temporales.
### Ejemplos
#### Fuente tipo carpeta
- fuente: `src:default:folder:workspace-docs`
- archivo real: `/home/pancho/Documentos/Empresa/Desarrollo/IA/docs/HISTORIAL_SESIONES.md`
- `document_key`: `docs/historial_sesiones.md`
#### Fuente tipo repo
- fuente: `src:default:repo:backend-api`
- archivo real: `/repo/src/app/main.ts`
- `document_key`: `src/app/main.ts`
#### Documento suelto manual
- fuente: `src:default:manual:client-upload`
- archivo real: `Manual Cliente Final.pdf`
- `document_key`: `manual_cliente_final.pdf`
#### Fuente derivada o externa
- fuente: `src:default:api:java-docs`
- unidad documental: documentacion de `ArrayList`
- `document_key`: `java/util/arraylist`
### Por que se decide asi
- mantiene trazabilidad sin depender de rutas absolutas del entorno
- evita que mover el proyecto de maquina o carpeta rompa la identidad del documento
- funciona tanto para archivos estructurados como para documentos sueltos o fuentes externas
- deja el sistema preparado para futuras ingestas derivadas sin cambiar el modelo base
**Aclaracion importante:**
- `document_key` es una identidad interna estable
- no debe ser la unica referencia para recargar el fichero real
- para reingesta o recarga debe mantenerse tambien el localizador original del recurso
---
## Documentos sueltos y fuentes por defecto
El sistema debe soportar tambien documentos sueltos que no pertenezcan de forma natural a una carpeta o coleccion preexistente.
Ejemplo:
- un PDF entregado por un cliente
En esos casos:
- el documento seguira teniendo su propio `document_id`
- no se dejara `source_id` vacio o nulo
- se usara una fuente logica por defecto para agrupar este tipo de entradas
La idea es que el sistema pueda distinguir entre:
- documentos nacidos dentro de una fuente estructurada
- documentos incorporados como entradas sueltas
Esto evita nulos, mantiene consistencia y deja abierta la posibilidad de definir despues varias fuentes logicas para documentos manuales, aportados por cliente o cargados desde otras vias.
**Por que se decide asi:**
- un documento suelto sigue necesitando un origen logico dentro del sistema.
- evitar `null` en `source_id` simplifica reglas internas, validaciones y futuras integraciones.
- agrupar documentos sueltos bajo una fuente logica por defecto permite empezar simple y luego especializar si hace falta.
---
## Vision de integracion futura
Este diseño de ingesta tambien debe facilitar que el RAG pueda integrarse mas adelante con herramientas y entornos de trabajo del propio equipo, por ejemplo OpenCode por proyecto.
La idea es que, aunque la documentacion exista fisicamente en disco, el RAG pueda ofrecer una capa practica de acceso al conocimiento del proyecto a medida que ese conocimiento crezca.
Esto refuerza varias decisiones tomadas en este modulo:
- trazabilidad por fuente, documento y chunk
- soporte para carpetas completas y documentos sueltos
- preparacion para actualizacion de contenido
- diseño desacoplado del tipo concreto de consumidor
---
## Relacion con el modulo de actualizacion
La ingesta debe quedar preparada para convivir con un futuro sistema de actualizacion.
Eso implica que, desde el inicio, conviene contemplar:
- deteccion de nuevas fuentes
- deteccion de cambios en fuentes ya conocidas
- posibilidad de releer una fuente
- trazabilidad de cuando y desde donde entro una version concreta
No es necesario implementar todo esto en la primera version, pero si diseñarlo para no bloquear su evolucion posterior.
---
## Principios de diseño
- Simple en la primera version.
- Desacoplada del tipo concreto de fuente.
- Preparada para crecimiento.
- Trazable desde el origen.
- Compatible con futuras actualizaciones y versionado.
---
## Uso de modelo durante la ingesta
Se acepta que el sistema RAG podra usar un modelo tambien en la fase de entrada, no solo en la fase de salida o consumo.
Su funcion no seria responder al usuario final, sino ayudar a:
- entender mejor el contenido entrante
- detectar tema principal y puntos clave
- extraer estructura util
- identificar elementos criticos o destacables
- preparar mejor el conocimiento que luego consumira el RAG
**Criterio actual:**
- en la primera fase no debemos depender obligatoriamente de modelos caros para esta tarea
- mas adelante puede existir un modelo fijo de uso interno dentro del pipeline de ingesta
- tambien queda abierta una futura interfaz interactiva de alimentacion en la que el usuario pueda escoger un modelo mas potente si quiere enriquecer el procesamiento
**Implicacion de diseño:**
La ingesta no debe quedar pensada solo como lectura mecanica de archivos. Debe poder evolucionar hacia una ingesta asistida por modelo.
---
## Modos de ingesta previstos
Se adopta este criterio de funcionamiento:
- por defecto, la ingesta funcionara en modo mecanico
- ese modo seguira pudiendo usar un modelo predefinido dentro del pipeline si asi se define
- la arquitectura debe quedar preparada desde el inicio para recibir parametros de modo o de interaccion
- si esos parametros existen, la ingesta podra comportarse como una ingesta enriquecida o interactiva
Esto permite que la primera version funcione sin rehacer la arquitectura cuando se amplie.
### Modo por defecto
Si no se reciben parametros especiales desde una interfaz:
- la ingesta actua de forma automatica
- usa el modelo predefinido si corresponde
- no espera decisiones del usuario
### Modo interactivo o enriquecido
Si se reciben parametros desde una interfaz o flujo enriquecido:
- la ingesta puede detectar que no esta en modo basico
- puede permitir cambio de modelo
- puede recibir instrucciones, marcas o directrices del usuario
- puede priorizar o etiquetar informacion importante antes de indexar
### Objetivo de este diseño
Separar desde el inicio:
- funcionamiento base estable y automatizable
- enriquecimiento opcional sin romper el flujo normal de ingesta
Esto evita tener que rehacer el codigo cuando el sistema evolucione.
---
## Enriquecimiento ligero opcional
Se deja abierta una via para un enriquecimiento ligero opcional desde etapas tempranas, siempre que no entorpezca la ingesta.
La forma mas simple de hacerlo seria:
- mantener la ingesta mecanica como camino principal
- ejecutar el enriquecimiento solo si llega un parametro explicito
- limitar ese enriquecimiento a tareas pequenas y acotadas
Ejemplos adecuados para ese enriquecimiento ligero:
- extraer tema principal
- proponer palabras clave
- detectar posibles puntos criticos
- generar una descripcion corta del documento
Ejemplos que no conviene meter en esta fase ligera:
- analisis profundo costoso
- interacciones largas con usuario
- multiples rondas de refinado
- procesos que bloqueen innecesariamente la ingesta base
**Criterio recomendado:**
El enriquecimiento ligero debe ser:
- opcional
- rapido
- barato
- no bloqueante
- facil de omitir sin afectar al pipeline principal
---
## Preguntas a cerrar
1. Si la ingesta trabajara por archivos individuales, carpetas, colecciones o todo a la vez.
2. Como construiremos exactamente `source_id`, `document_id` y `chunk_id`.
3. Que nombre tendran las fuentes logicas por defecto para documentos sueltos.
4. Como distinguiremos entre fuente nueva, fuente actualizada y fuente obsoleta.

257
RAG/docs/PROCESADO.md Normal file
View file

@ -0,0 +1,257 @@
# Procesado
**Proyecto:** Workspace de tools IA para empresas
**Modulo:** RAG
**Ultima actualizacion:** 2026-04-02
**Ultima modificacion por:** Agente tools IA para potenciar servicios empresariales
**Estado:** En definicion
---
## Proposito
Definir el modulo de procesado del sistema RAG, encargado de transformar la salida de ingesta en contenido preparado para indexacion y recuperacion.
---
## Que es el procesado
El procesado toma las fuentes ya leidas por la ingesta y las prepara para el resto del pipeline.
Su trabajo incluye, segun el tipo de contenido:
- limpieza basica
- normalizacion
- extraccion de estructura util
- preparacion de metadatos
- division en chunks
---
## Responsabilidades del procesado
1. Recibir la salida de la ingesta.
2. Normalizar el contenido a una representacion interna util.
3. Conservar contexto estructural relevante.
4. Generar chunks recuperables.
5. Entregar el resultado listo para indexacion.
---
## Decision inicial sobre chunking
Para la v1 del sistema se adopta una estrategia comun de chunking para todos los documentos.
No se implementaran aun estrategias distintas por tipo de documento.
Decision cerrada para esta v1 documental:
- se adopta un modo `documental` estable como base
- se deja abierta una futura variante para documentos extensos, por ejemplo `documental_largo` o `documental_jerarquico`
Decision ya implementada adicional:
- se implementa tambien un modo `codigo`
- su chunking se hace por bloques top-level y no por parrafos
---
## Por que se decide asi
- reduce complejidad en la primera version
- permite validar antes el valor real del RAG
- evita abrir demasiadas variantes demasiado pronto
- deja la puerta abierta a estrategias especificas cuando haya evidencia real de necesidad
---
## Vision futura
Mas adelante el sistema podra permitir chunking distinto segun:
- tipo de documento
- tipo de fuente
- uso esperado del contenido
- formato tecnico o estructura interna
Ejemplos:
- documentacion general
- historiales o logs
- PDFs tecnicos
- codigo fuente
---
## Principio adoptado
Regla actual:
- una estrategia comun, simple y estable para la v1
Regla futura:
- estrategias adaptables por tipo de documento o fuente si el beneficio supera el coste de complejidad
---
## Politica oficial de chunking documental v1
Se adopta esta estrategia para el modo `documental`:
1. Intentar partir primero por estructura natural del documento.
2. Usar como unidades preferentes:
- titulos
- subtitulos
- bloques de parrafos relacionados
3. Si una seccion o bloque supera el tamano maximo permitido, subdividirla por parrafos.
4. Si aun asi sigue siendo demasiado grande, cortar por longitud sin romper mas de lo necesario.
5. Mantener metadatos estructurales del bloque, por ejemplo titulo o seccion de origen.
6. Aplicar overlap pequeno y fijo entre chunks contiguos.
---
## Criterio de tamano para la v1
Para mantener la implementacion simple y estable, la v1 trabajara con una medida aproximada por caracteres, no por tokens.
Propuesta cerrada:
- tamano objetivo por chunk: alrededor de 1500 caracteres
- tamano maximo por chunk: alrededor de 2200 caracteres
- overlap aproximado: alrededor de 200 caracteres
### Como se aplican estos valores
- si un bloque natural cabe dentro del rango objetivo, se conserva como un solo chunk
- si supera el objetivo, se intenta dividir por parrafos
- si supera el maximo incluso despues de agrupar o dividir por parrafos, se corta por longitud controlada
- el overlap se toma del final del chunk anterior para conservar continuidad
### Por que se decide asi
- caracteres es una medida mas simple de implementar en la v1
- estos valores son suficientemente pequenos para no cargar demasiado cada fragmento
- siguen siendo suficientemente amplios para conservar contexto util en documentacion normal
- el overlap pequeno ayuda a no perder continuidad entre ideas cercanas
---
## Limitaciones conocidas del modo documental v1
- funciona especialmente bien para documentacion general, notas, manuales, PDFs con texto extraible y contenido textual estructurado
- puede quedarse corto para libros tecnicos, documentos muy largos o contenido extremadamente denso
- para esos casos se deja prevista una futura variante especializada, sin romper el modo `documental` base
---
## Politica oficial de chunking de codigo v1
Se adopta esta estrategia para el modo `codigo`:
1. Detectar bloques top-level relevantes del fichero.
2. Usar como unidades preferentes:
- funciones
- clases
- interfaces
- tipos
- bloques exportados o definidos al nivel principal
3. Si un bloque supera el tamano maximo, subdividirlo por longitud controlada.
4. Conservar metadatos utiles del bloque:
- `sectionTitle`
- `startLine`
- `endLine`
### Por que se decide asi
- el codigo no debe partirse como si fueran parrafos documentales
- interesa recuperar unidades semanticas utiles para preguntas tecnicas
- mantener rango de lineas y cabecera del bloque mejora la trazabilidad practica
### Soporte actual de extensiones de codigo
- `ts`
- `tsx`
- `js`
- `jsx`
- `mjs`
- `cjs`
- `py`
- `json`
- `yml`
- `yaml`
---
## Impacto de cambiar la estrategia de chunking
La forma de chunking condiciona directamente como queda representado el conocimiento dentro del RAG.
Por eso, si en el futuro cambiamos de manera relevante la estrategia de chunking, lo normal sera tener que reprocesar e indexar de nuevo los documentos afectados.
### Que implica esto
- si cambia la forma de partir un documento, sus chunks dejan de ser equivalentes a los anteriores
- los embeddings asociados a esos chunks tambien dejan de representar exactamente la nueva estructura
- por tanto, lo correcto suele ser rehacer chunking e indexacion para esa parte del conocimiento
### Que no significa necesariamente
- no siempre hara falta rehacer todo el RAG completo
- puede bastar con reingestar solo determinadas fuentes, tipos documentales o tenants
### Criterio recomendado
- en la v1 conviene usar una estrategia estable y suficientemente buena
- no buscar la perfeccion absoluta desde el inicio
- dejar preparado el sistema para reindexacion parcial cuando haya mejoras posteriores
---
## Convivencia de estrategias distintas
En el futuro podran convivir estrategias distintas de chunking, por ejemplo `documental` y `codigo`.
Pero esa convivencia debe hacerse de forma controlada y explicita, no mezclando sin criterio chunks equivalentes del mismo tipo documental.
Ejemplo sano:
- documentos de negocio en modo `documental`
- codigo fuente en modo `codigo`
Ejemplo menos sano:
- misma clase de documentos documentales partida a la vez con dos reglas distintas sin control de version o motivo claro
Por eso, lo que decidimos ahora es importante: fija la base de la v1 y condiciona como se representara inicialmente el conocimiento.
---
## Overlap entre chunks
`Overlap` significa repetir una pequena parte del contenido entre un chunk y el siguiente.
### Ejemplo simple
Si un texto se parte asi:
- chunk 1: parrafos 1, 2 y 3
- chunk 2: parrafos 3, 4 y 5
entonces el parrafo 3 actua como zona de solapamiento entre ambos chunks.
### Para que sirve
- evita que una idea importante quede cortada justo en el limite entre chunks
- conserva mejor continuidad entre fragmentos vecinos
- mejora la recuperacion cuando una consulta depende de contexto compartido entre dos trozos
### Recomendacion para la v1
- usar overlap pequeno
- suficiente para conservar continuidad
- sin duplicar demasiado contenido ni disparar el coste de indexacion
---
## Preguntas a cerrar
1. Como sera exactamente la estrategia de chunking comun de la v1.
2. Si los chunks se construiran por estructura del documento, por longitud o con un enfoque mixto.
3. Si habra overlap entre chunks desde la primera version.
4. Que informacion estructural debe conservar cada chunk.

286
RAG/docs/SALIDA.md Normal file
View file

@ -0,0 +1,286 @@
# Salida
**Proyecto:** Workspace de tools IA para empresas
**Modulo:** RAG
**Ultima actualizacion:** 2026-04-02
**Ultima modificacion por:** Agente tools IA para potenciar servicios empresariales
**Estado:** En definicion
---
## Proposito
Definir como sale el conocimiento del sistema RAG hacia los consumidores, especialmente en forma de contexto util para agentes, aplicaciones y soluciones con IA.
---
## Vision general
La salida del RAG no se limita a responder preguntas.
El sistema debe poder:
- recuperar contexto util
- ofrecer un mapa inicial de conocimiento
- devolver respuesta apoyada en contexto cuando haga falta
Esto responde a los dos grandes casos de uso aceptados:
- agentes expertos en empresa
- agentes expertos en proyecto o desarrollo
---
## Tipos de salida contemplados
Se consideran validos estos tipos de salida conceptuales:
1. `retrieve`
- devuelve contexto recuperado
2. `answer`
- devuelve una respuesta apoyada en contexto
3. `context_plus_summary`
- devuelve contexto mas una sintesis util
En la practica, `retrieve` es la pieza clave para la v1, porque es la base que permite contextualizar agentes y otras herramientas.
---
## Modo `answer`
`answer` se apoya en `retrieve`.
Flujo aplicado:
1. recuperar contexto relevante
2. construir un prompt con resumen y citas del contexto recuperado
3. pedir a un modelo de respuesta que conteste usando solo ese contexto
4. devolver la respuesta junto con citas y trazabilidad minima
### Estructura esperada
- `mode`
- `intent`
- `answer`
- `summary`
- `topics`
- `criticalPoints`
- `citations`
- `scope`
### Por que se diseña asi
- mantiene a `retrieve` como nucleo del sistema
- evita que la respuesta se desconecte del contexto recuperado
- permite que el mismo RAG sirva tanto para cargar contexto como para responder directamente
- conserva trazabilidad para revisar de donde sale la respuesta
### Limite importante
La calidad de `answer` depende directamente de la calidad de `retrieve`.
Si la recuperacion no trae el fragmento concreto adecuado, la respuesta puede quedar demasiado prudente o incompleta aunque el modo `answer` funcione correctamente.
En modo `codigo`, `answer` incluye tambien `sectionTitle` y rango de lineas cuando estan disponibles.
---
## Mejora aplicada a `retrieve specific`
Para consultas operativas frecuentes, `retrieve specific` ya no se limita a una sola busqueda semantica directa.
Se ha mejorado con:
- subconsultas internas segun la intencion detectada
- reordenacion por alineacion con la pregunta
- refuerzo de documentos especialmente relevantes para ciertos casos, como backlog, reglas, historial o indice documental
**Objetivo de esta mejora:**
- que preguntas practicas como "que tenemos pendiente" o "cuales son las reglas" lleguen antes a los fragmentos verdaderamente utiles
- mejorar `answer` sin tener que cambiar su arquitectura
Tambien se ha ampliado para consultas tecnicas o conceptuales del modo `codigo`, por ejemplo preguntas sobre funciones, IDs, servicios, endpoints o flujo interno.
---
## Retrieve inicial y retrieve especifico
Se adopta esta separacion conceptual:
1. `retrieve` inicial o de arranque
- entrega un mapa general del dominio
- orienta al agente sobre el panorama del conocimiento disponible
- no busca profundizar en exceso
2. `retrieve` especifico
- profundiza en un tema, duda o necesidad concreta
- recupera contenido mas cercano a la consulta puntual
---
## Estructura acordada para `retrieve` inicial
Se adopta esta estructura base para el `retrieve` inicial.
### 1. `mode`
Indica el modo de recuperacion aplicado.
Valores previstos:
- `documental`
- `codigo`
- `auto`
**Por que se incluye:**
- permite trazabilidad de como se hizo la recuperacion
- ayuda al consumidor a entender el tipo de lectura aplicada
### 2. `intent`
Indica la intencion de la recuperacion.
Valores previstos:
- `bootstrap`
- `specific`
Para el `retrieve` inicial, el valor esperado es `bootstrap`.
**Por que se incluye:**
- separa claramente el mapa inicial de una consulta puntual
- evita confundir una carga de contexto general con una busqueda de detalle
### 3. `summary`
Resumen corto del contexto recuperado.
**Por que se incluye:**
- da una lectura rapida del panorama general
- permite al agente orientarse sin tener que leer todos los fragmentos completos de inmediato
### 4. `topics`
Lista de temas principales detectados.
**Por que se incluye:**
- ayuda a que el agente tenga un mapa tematico del dominio
- sirve como guia para decidir en que profundizar despues
### 5. `critical_points`
Lista de puntos criticos o prioritarios.
Estos puntos pueden salir de dos vias:
- inferidos por el sistema
- marcados explicitamente como criticos en las fuentes o durante la ingesta
**Por que se incluye:**
- algunos temas deben mantenerse visibles aunque el resumen general sea breve
- permite priorizar conocimiento especialmente sensible o importante
### 6. `items`
Lista de fragmentos o piezas recuperadas.
Cada item debe poder incluir como minimo:
- `chunk_id`
- `document_id`
- `source_id`
- `title`
- `content`
- `score`
**Por que se incluye:**
- es la parte trazable y reutilizable del contexto
- permite que un agente o sistema no dependa solo de una sintesis
- mantiene acceso a las piezas concretas del conocimiento recuperado
### 7. `follow_up_refs`
Referencias o pistas para profundizar despues.
Pueden apuntar a:
- temas
- documentos
- secciones
- preguntas sugeridas
**Por que se incluye:**
- convierte el `retrieve` inicial en una guia de navegacion
- facilita pasar de un mapa general a una consulta especifica sin perder el hilo
### 8. `scope`
Permite acotar la recuperacion a una fuente, workspace, proyecto o conjunto de tags concreto.
Campos previstos:
- `sourceId`
- `sourceRef`
- `tags`
**Por que se incluye:**
- evita que el bootstrap o la consulta especifica carguen contexto de todo el sistema cuando solo interesa un workspace o proyecto
- prepara el RAG para convivir con multiples fuentes sin mezclar panoramas
- hace util el retrieve inicial como carga contextual enfocada, no solo global
---
## Que representa realmente este `retrieve` inicial
No es una respuesta final.
Es un paquete de contexto de arranque pensado para:
- dar orientacion general
- volver experto al agente en ese dominio durante la sesion de trabajo
- servir como base para posteriores consultas especificas
Su papel es parecido al de una carga inicial de contexto o mapa operativo del conocimiento.
---
## Estrategia aplicada al `retrieve` inicial
El `bootstrap` no debe limitarse a ejecutar una sola busqueda semantica amplia.
Se adopta una estrategia de recuperacion orientada a mapa inicial:
- lanzar varias subconsultas relacionadas con panorama general, documentacion principal, pendientes y reglas del dominio
- fusionar resultados sin duplicar chunks
- ordenar los fragmentos priorizando relevancia y utilidad panoramica
- sintetizar un resumen final que ayude a orientarse rapido
**Por que se decide asi:**
- un retrieve inicial necesita mas cobertura que una consulta puntual
- una sola consulta suele dejar fuera piezas clave del mapa general
- combinar subconsultas mejora la calidad del contexto de arranque sin cambiar el modelo de embeddings ni la base vectorial
**Resultado esperado:**
- mejor identificacion de documentos base
- mejor panorama del workspace o proyecto
- referencias mas utiles para profundizar despues
---
## Por que se ha elegido esta estructura
- porque el RAG debe servir para mucho mas que preguntas aisladas
- porque el usuario quiere que un agente pueda arrancar con una vision rica del tema o proyecto
- porque un mapa inicial ayuda a profundizar despues sin saturar contexto desde el principio
- porque mezcla panorama, criticidad, trazabilidad y capacidad de profundizacion posterior
---
## Ajustes futuros posibles
Aunque esta estructura queda aceptada para la v1, se asume que en la practica podria requerir ajustes.
Posibles cambios futuros:
- afinar el nivel de detalle de `summary`
- separar mejor `topics` y `critical_points`
- enriquecer `items` con mas metadatos
- hacer que `follow_up_refs` sea mas estructurado
- incorporar niveles de confianza o explicacion de relevancia
La estructura actual se acepta como una base suficientemente clara para empezar a construir y evaluar.

View file

@ -0,0 +1,102 @@
# Sistema RAG base
**Proyecto:** Workspace de tools IA para empresas
**Modulo:** RAG
**Ultima actualizacion:** 2026-04-02
**Ultima modificacion por:** Agente tools IA para potenciar servicios empresariales
**Estado:** En definicion
---
## Proposito
Definir un sistema RAG base, bien planteado y reutilizable, que pueda integrarse con rapidez en proyectos de clientes y tambien servir como base para tools internas del workspace.
La prueba inicial con la carpeta `docs/` de este workspace se utilizara solo como ejemplo practico y entorno real de validacion, no como limite del sistema.
Desde la v1, el sistema debe contemplar tambien PDFs como parte valida de las fuentes documentales de entrada.
---
## Que es lo que queremos lograr
Queremos construir una base RAG que permita:
- indexar informacion relevante de un proyecto
- recuperar contexto util en funcion de una consulta
- entregar ese contexto a un agente o servicio para mejorar sus respuestas o decisiones
- integrarse con rapidez en otros proyectos sin rehacer la arquitectura cada vez
En terminos practicos, el objetivo es disponer de una pieza reutilizable que convierta documentacion, conocimiento interno y otras fuentes en contexto accesible para agentes y servicios de IA.
---
## Vision del sistema
Este sistema RAG debe quedar preparado para trabajar con diferentes tipos de fuentes, por ejemplo:
- documentacion de proyecto
- bases de conocimiento internas
- contenido operativo de clientes
- archivos tecnicos
- datos exportados desde otros sistemas
La carpeta `docs/` del workspace servira como primer caso real de prueba porque permite validar resultados con un conjunto de informacion pequeno, claro y facil de revisar.
---
## Objetivos principales
1. Diseñar una base RAG reutilizable y desacoplada del proyecto de prueba.
2. Permitir que pueda conectarse a otros servicios de IA o agentes con una integracion sencilla.
3. Hacer que el sistema funcione con fuentes reales, empezando por `docs/`.
4. Facilitar continuidad entre sesiones, agentes y proyectos gracias a recuperacion de contexto relevante.
5. Dejar lista una arquitectura que luego pueda crecer hacia casos mas complejos.
---
## Caso de prueba inicial
La primera validacion se hara con la carpeta `docs/` de este workspace.
La idea es comprobar si un agente puede:
- localizar rapidamente informacion relevante
- entender que se ha hecho y que queda pendiente
- responder con base documental sin releer todo manualmente
- usar la documentacion del workspace como conocimiento ampliado en tiempo de consulta
Esto servira para evaluar si la base conceptual y tecnica del RAG esta bien construida.
---
## Principios del sistema
- Reutilizable: debe servir para este workspace y para proyectos de clientes.
- Modular: ingesta, indexacion, recuperacion y consumo deben poder evolucionar por separado.
- Simple al inicio: la primera version debe ser pequena y validable.
- Escalable: debe poder crecer a nuevas fuentes y nuevos casos de uso.
- Trazable: el contexto recuperado debe indicar de donde sale.
- Facil de integrar: debe poder conectarse rapidamente a agentes, tools o servicios.
---
## Resultado esperado
Si el sistema esta bien planteado, deberiamos poder usarlo como una capa de contexto ampliado para agentes y servicios, logrando que consulten conocimiento relevante de forma rapida y util, sin depender de tener toda la informacion cargada manualmente en cada sesion.
---
## Alcance de este documento
Este documento define el que y el para que del sistema RAG base.
En documentos posteriores de esta carpeta se podran detallar, por separado:
- arquitectura propuesta
- flujo de ingesta
- estrategia de chunking
- embeddings y almacenamiento
- interfaz de consulta
- pruebas con `docs/`
- criterios de evaluacion de resultados

View file

@ -0,0 +1,221 @@
# Stack tecnico v1
**Proyecto:** Workspace de tools IA para empresas
**Modulo:** RAG
**Ultima actualizacion:** 2026-04-02
**Ultima modificacion por:** Agente tools IA para potenciar servicios empresariales
**Estado:** Acordado para v1
---
## Proposito
Definir el stack tecnico minimo con el que se construira la primera version funcional del RAG.
Las decisiones tomadas aqui deben entenderse como base estable del sistema, no como elecciones provisionales pensadas para ser sustituidas inmediatamente en una siguiente version.
---
## Stack acordado
### Runtime y lenguaje
- `Node.js`
- `TypeScript`
**Por que:**
- equilibrio entre velocidad de desarrollo y estructura
- buen encaje con APIs, tools y futuras integraciones
- facilita evolucion posterior hacia MCP u otras capas de consumo
---
### Backend HTTP
- `Express`
**Por que:**
- simplicidad de arranque
- ecosistema amplio
- suficiente para esta v1
- el rendimiento del framework no es el cuello de botella principal del sistema ahora mismo
**Encaje operativo:**
- compatible con despliegue local y en VPS
- adecuado para montarlo como servicio en EasyPanel dentro de un contenedor propio
---
### Vector store
- `Qdrant`
**Por que:**
- pensado especificamente para busqueda vectorial
- mas adecuado para un RAG real que usar solo una base de datos generalista
- deja una base mejor para evolucion posterior que una solucion demasiado provisional
**Matiz:**
- `SQLite` podria servir como apoyo auxiliar para ciertos metadatos o control local, pero no se toma como nucleo del retrieval vectorial
- en un escenario con EasyPanel, `Qdrant` encaja bien como servicio separado o contenedor adjunto al servicio principal del RAG
---
### Embeddings
- proveedor desacoplado mediante una interfaz interna del sistema
**Por que:**
- evita atarnos a un unico proveedor desde el inicio
- permite cambiar por coste, calidad o disponibilidad sin rehacer el nucleo del RAG
- facilita pruebas comparativas en el futuro
**Aclaracion importante:**
Esto debe entenderse como una capa de abstraccion o adaptador interno para proveedores de embeddings, no como una dependencia directa del resto del sistema hacia un proveedor concreto.
**Decision cerrada para la base estable del sistema:**
- `Qwen3 Embedding 8B`
**Por que se elige este modelo:**
- encaja con el objetivo de que la v1 ya sea una base estable y no una eleccion pensada para cambiar pronto
- sirve tanto para recuperacion textual como para recuperacion de codigo
- ofrece capacidades multilingues y manejo de texto largo
- esta disponible ahora en OpenRouter para poder arrancar
- puede desplegarse de forma local mas adelante, manteniendo coherencia con la arquitectura deseada
**Referencia de comparacion, no decision principal:**
- `OpenAI text-embedding-3-large` se considera una referencia valida de calidad comercial, pero no se adopta como base del sistema porque no encaja igual de bien con el objetivo de despliegue local futuro
**Consecuencia de cambiar de modelo de embeddings:**
- los vectores generados por modelos distintos no deben mezclarse como si vivieran en el mismo espacio semantico
- si se cambia de modelo de embeddings, normalmente hay que re-embeddear el contenido afectado
- esto no obliga a perder trazabilidad si el sistema guarda que modelo y que version genero cada embedding
**Criterio de diseño adoptado:**
- cada embedding debe registrar al menos su `embedding_provider`, `embedding_model` y `embedding_dimensions`
- el sistema debe poder saber con que modelo fue indexado cada documento o chunk
- el retrieval debe consultar dentro de un indice o coleccion coherente con el modelo de embeddings usado
**Configuracion base prevista:**
- modelo base: `Qwen3 Embedding 8B`
- acceso inicial: proveedor externo compatible
- acceso futuro previsto: despliegue local en red propia manteniendo la misma abstraccion del proveedor
**Direccion recomendada:**
- tratar cada combinacion relevante de proveedor y modelo como una version de indexacion
- permitir reindexacion controlada cuando se quiera migrar a otro modelo
- evitar mezclar embeddings de modelos distintos en la misma logica de recuperacion
---
### Parsing documental
Soporte inicial previsto para:
- `md`
- `txt`
- `pdf`
- codigo fuente del proyecto
**Por que:**
- cubre documentacion habitual del workspace y de escenarios reales de empresa
- el soporte de PDF es obligatorio desde la v1
**Matiz:**
- la v1 contempla PDFs con texto extraible
- OCR para PDFs escaneados o de baja calidad queda como ampliacion futura
Extensiones de codigo ya soportadas:
- `ts`
- `tsx`
- `js`
- `jsx`
- `mjs`
- `cjs`
- `py`
- `json`
- `yml`
- `yaml`
---
### Chunking y pipeline interno
- implementacion propia en TypeScript
**Por que:**
- el comportamiento del chunking forma parte central del diseño del sistema
- ya se han acordado reglas propias que conviene controlar directamente
- evita depender demasiado pronto de una abstraccion externa que no siga nuestras decisiones
---
### Forma de acceso
- API HTTP como base principal
**Por que:**
- permite que otras apps, agentes y sistemas consuman el RAG con facilidad
- permite pruebas inmediatas desde terminal
- encaja bien con una futura capa MCP
Endpoints funcionales actuales:
- `GET /health`
- `POST /ingest`
- `POST /retrieve`
- `POST /answer`
---
## Configuracion local de claves
Se deja previsto un archivo local `RAG/.env.local` para trabajar claves y configuraciones sensibles sin incluirlas en el repositorio.
Uso previsto:
- `EMBEDDING_API_KEY` para OpenRouter o proveedor compatible
- credenciales de `Qdrant` si aplican
- futuras claves de servicios asociados
Este archivo esta ignorado por Git.
---
## Encaje con EasyPanel
La estructura tecnica inicial se ha planteado para que el modulo pueda ejecutarse:
- en local durante desarrollo
- en un VPS Debian gestionado desde EasyPanel
Direccion operativa recomendada:
- un contenedor para la API del RAG
- un servicio de `Qdrant` separado o adjunto segun convenga
- variables de entorno gestionadas desde EasyPanel
Esto mantiene simple el despliegue y evita acoplar el codigo a una unica maquina o ruta local.
---
## Direccion general del stack
La v1 se construira con una idea clara:
- base simple de poner en marcha
- lo bastante seria para servir en casos reales
- preparada para evolucionar sin rehacer todo al cambiar proveedor o ampliar capacidades
---
## Decisiones ya cerradas que condicionan este stack
- el RAG se pensara como servicio
- PDFs entran desde la v1
- el modo `codigo` sigue siendo parte de la direccion del sistema aunque no sea la primera implementacion
- la salida del RAG debe servir tambien para contexto, no solo para respuestas finales
---
## Pendiente inmediato
Generar la imagen Docker final del servicio y dejarlo listo para desplegar en EasyPanel.

1836
RAG/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

26
RAG/package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "rag-service",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js",
"check": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@qdrant/js-client-rest": "^1.15.0",
"dotenv": "^16.4.5",
"express": "^4.21.2",
"openai": "^4.104.0",
"pdf-parse": "^1.1.1"
},
"devDependencies": {
"@types/express": "^5.0.1",
"@types/node": "^22.15.3",
"@types/pdf-parse": "^1.1.4",
"tsx": "^4.19.3",
"typescript": "^5.8.3"
}
}

86
RAG/src/app.ts Normal file
View file

@ -0,0 +1,86 @@
import express from "express";
import { env } from "./config/env.js";
import { AnswerService } from "./modules/answer/service.js";
import { IngestService } from "./modules/ingest/service.js";
import { OpenRouterEmbeddingProvider } from "./modules/embeddings/provider.js";
import { documentalChunkingPolicy } from "./modules/process/chunking.js";
import { RetrieveService } from "./modules/retrieve/service.js";
import { supportedParserExtensions } from "./modules/parsers/parser-registry.js";
import { QdrantVectorStoreClient } from "./modules/vectorstore/client.js";
import type { ChunkMode, RetrieveIntent, RetrieveScope } from "./shared/types/rag.js";
export function createApp() {
const app = express();
const embeddingProvider = new OpenRouterEmbeddingProvider();
const vectorStore = new QdrantVectorStoreClient();
const ingestService = new IngestService(embeddingProvider, vectorStore);
const retrieveService = new RetrieveService(embeddingProvider, vectorStore);
const answerService = new AnswerService(retrieveService);
app.use(express.json({ limit: "5mb" }));
app.get("/health", async (_req, res) => {
const vectorHealth = await vectorStore.healthcheck();
res.json({
ok: true,
service: "rag",
environment: env.nodeEnv,
embeddings: {
provider: embeddingProvider.providerName,
model: embeddingProvider.modelName
},
vectorStore: vectorHealth,
parsers: supportedParserExtensions(),
chunking: documentalChunkingPolicy
});
});
app.post("/ingest", async (req, res) => {
try {
const result = await ingestService.ingest(req.body);
res.status(202).json(result);
} catch (error) {
res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown ingest error" });
}
});
app.post("/retrieve", async (req, res) => {
try {
const mode = (req.body.mode ?? "auto") as ChunkMode;
const intent = (req.body.intent ?? "specific") as RetrieveIntent;
const query = String(req.body.query ?? "");
const scope: RetrieveScope | undefined = req.body.scope
? {
sourceId: req.body.scope.sourceId,
sourceRef: req.body.scope.sourceRef,
tags: Array.isArray(req.body.scope.tags) ? req.body.scope.tags.map(String) : undefined
}
: undefined;
const result = await retrieveService.retrieve(mode, intent, query, scope);
res.json(result);
} catch (error) {
res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown retrieve error" });
}
});
app.post("/answer", async (req, res) => {
try {
const mode = (req.body.mode ?? "auto") as ChunkMode;
const intent = (req.body.intent ?? "specific") as RetrieveIntent;
const query = String(req.body.query ?? "");
const scope: RetrieveScope | undefined = req.body.scope
? {
sourceId: req.body.scope.sourceId,
sourceRef: req.body.scope.sourceRef,
tags: Array.isArray(req.body.scope.tags) ? req.body.scope.tags.map(String) : undefined
}
: undefined;
const result = await answerService.answer(mode, intent, query, scope);
res.json(result);
} catch (error) {
res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown answer error" });
}
});
return app;
}

28
RAG/src/config/env.ts Normal file
View file

@ -0,0 +1,28 @@
import { config as loadEnv } from "dotenv";
loadEnv();
loadEnv({ path: ".env.local", override: true });
function requireEnv(name: string, fallback?: string): string {
const value = process.env[name] ?? fallback;
if (!value) {
throw new Error(`Missing environment variable: ${name}`);
}
return value;
}
export const env = {
nodeEnv: process.env.NODE_ENV ?? "development",
port: Number(process.env.PORT ?? 3000),
qdrantUrl: requireEnv("QDRANT_URL", "http://localhost:6333"),
qdrantApiKey: process.env.QDRANT_API_KEY ?? "",
qdrantCollection: requireEnv("QDRANT_COLLECTION", "rag_chunks"),
embeddingProvider: requireEnv("EMBEDDING_PROVIDER", "openrouter"),
embeddingModel: requireEnv("EMBEDDING_MODEL", "qwen/qwen3-embedding-8b"),
embeddingBaseUrl: requireEnv("EMBEDDING_BASE_URL", "https://openrouter.ai/api/v1"),
embeddingApiKey: process.env.EMBEDDING_API_KEY ?? "",
answerProvider: requireEnv("ANSWER_PROVIDER", "openrouter"),
answerModel: requireEnv("ANSWER_MODEL", "openai/gpt-4.1-mini"),
answerBaseUrl: requireEnv("ANSWER_BASE_URL", "https://openrouter.ai/api/v1"),
answerApiKey: process.env.ANSWER_API_KEY ?? process.env.EMBEDDING_API_KEY ?? ""
} as const;

View file

@ -0,0 +1,78 @@
import OpenAI from "openai";
import { env } from "../../config/env.js";
import type { AnswerResponse, ChunkMode, RetrieveIntent, RetrieveScope, RetrievedItem } from "../../shared/types/rag.js";
import { RetrieveService } from "../retrieve/service.js";
function buildPrompt(query: string, summary: string, items: RetrievedItem[]): string {
const context = items
.map((item, index) => [
`Fuente ${index + 1}: ${item.title}`,
item.sectionTitle ? `section_title: ${item.sectionTitle}` : undefined,
`chunk_id: ${item.chunkId}`,
`document_id: ${item.documentId}`,
item.startLine ? `line_range: ${item.startLine}-${item.endLine ?? item.startLine}` : undefined,
item.content
].filter(Boolean).join("\n"))
.join("\n\n---\n\n");
return [
"Responde usando solo el contexto recuperado.",
"Si el contexto no es suficiente, dilo claramente.",
"Prioriza exactitud y brevedad.",
`Resumen del contexto: ${summary}`,
`Pregunta: ${query}`,
"Contexto recuperado:",
context
].join("\n\n");
}
export class AnswerService {
private readonly client: OpenAI;
constructor(private readonly retrieveService: RetrieveService) {
this.client = new OpenAI({
apiKey: env.answerApiKey,
baseURL: env.answerBaseUrl
});
}
async answer(mode: ChunkMode, intent: RetrieveIntent, query: string, scope?: RetrieveScope): Promise<AnswerResponse> {
if (!env.answerApiKey) {
throw new Error("Missing ANSWER_API_KEY for answer provider");
}
const retrieved = await this.retrieveService.retrieve(mode, intent, query, scope);
const completion = await this.client.chat.completions.create({
model: env.answerModel,
temperature: 0.2,
messages: [
{
role: "system",
content: "Eres un asistente RAG. Respondes con precision usando solamente el contexto recuperado. Si falta informacion, lo indicas sin inventar datos."
},
{
role: "user",
content: buildPrompt(query, retrieved.summary, retrieved.items)
}
]
});
return {
mode: retrieved.mode,
intent: retrieved.intent,
answer: completion.choices[0]?.message?.content?.trim() || "No se pudo generar respuesta.",
summary: retrieved.summary,
topics: retrieved.topics,
criticalPoints: retrieved.criticalPoints,
citations: retrieved.items.map((item) => ({
chunkId: item.chunkId,
documentId: item.documentId,
title: item.title,
sectionTitle: item.sectionTitle,
startLine: item.startLine,
endLine: item.endLine
})),
scope: retrieved.scope
};
}
}

View file

@ -0,0 +1,39 @@
import OpenAI from "openai";
import { env } from "../../config/env.js";
export interface EmbeddingProvider {
readonly providerName: string;
readonly modelName: string;
readonly dimensions?: number;
embed(input: string[]): Promise<number[][]>;
}
export class OpenRouterEmbeddingProvider implements EmbeddingProvider {
readonly providerName: string;
readonly modelName: string;
readonly dimensions?: number;
private readonly client: OpenAI;
constructor() {
this.providerName = env.embeddingProvider;
this.modelName = env.embeddingModel;
this.client = new OpenAI({
apiKey: env.embeddingApiKey,
baseURL: env.embeddingBaseUrl
});
}
async embed(input: string[]): Promise<number[][]> {
if (!env.embeddingApiKey) {
throw new Error("Missing EMBEDDING_API_KEY for embedding provider");
}
const response = await this.client.embeddings.create({
model: this.modelName,
input,
encoding_format: "float"
});
return response.data.map((item) => item.embedding);
}
}

View file

@ -0,0 +1,91 @@
import path from "node:path";
import { parseDocument, isSupportedDocument } from "../parsers/parser-registry.js";
import { chunkDocument, codeChunkingPolicy, documentalChunkingPolicy } from "../process/chunking.js";
import type { EmbeddingProvider } from "../embeddings/provider.js";
import type { VectorStoreClient } from "../vectorstore/client.js";
import type { IngestResult, IngestSourceInput, IngestedChunk } from "../../shared/types/rag.js";
import { buildChunkId, buildDocumentId, buildQdrantPointId, buildSourceId, normalizeDocumentKey } from "../../shared/utils/ids.js";
import { listFilesRecursively } from "../../shared/utils/files.js";
import { env } from "../../config/env.js";
export class IngestService {
constructor(
private readonly embeddingProvider: EmbeddingProvider,
private readonly vectorStore: VectorStoreClient
) {}
async ingest(source: IngestSourceInput): Promise<IngestResult> {
const sourceId = source.sourceId ?? buildSourceId(source.sourceType, source.sourceRef);
const discoveredFiles = await this.resolveInputFiles(source);
const supportedFiles = discoveredFiles.filter(isSupportedDocument);
let documentsProcessed = 0;
let chunksStored = 0;
for (const filePath of supportedFiles) {
const parsed = await parseDocument(filePath);
if (!parsed.content.trim()) {
continue;
}
const documentKey = normalizeDocumentKey(
source.sourceType === "folder"
? path.relative(path.resolve(source.sourceRef), path.resolve(filePath))
: path.basename(filePath)
);
const documentId = buildDocumentId(sourceId, documentKey);
const chunkingPolicy = parsed.chunkMode === "codigo" ? codeChunkingPolicy : documentalChunkingPolicy;
const chunks = chunkDocument(parsed.title, parsed.content, chunkingPolicy);
const embeddings = await this.embeddingProvider.embed(chunks.map((chunk) => chunk.content));
const qdrantChunks: IngestedChunk[] = chunks.map((chunk, index) => {
const chunkId = buildChunkId(documentId, chunkingPolicy.mode, chunk.index);
return {
id: buildQdrantPointId(chunkId),
vector: embeddings[index],
payload: {
chunk_id: chunkId,
source_id: sourceId,
source_type: source.sourceType,
source_ref: source.sourceRef,
document_id: documentId,
document_key: documentKey,
title: parsed.title,
mime_type: parsed.mimeType,
section_title: chunk.sectionTitle,
chunk_mode: chunkingPolicy.mode,
chunk_index: chunk.index,
start_line: chunk.startLine,
end_line: chunk.endLine,
content: chunk.content,
embedding_provider: this.embeddingProvider.providerName,
embedding_model: this.embeddingProvider.modelName,
embedding_dimensions: embeddings[index]?.length ?? 0,
status: "active",
updated_at: new Date().toISOString(),
tags: source.tags ?? []
}
};
});
await this.vectorStore.upsert(qdrantChunks);
documentsProcessed += 1;
chunksStored += qdrantChunks.length;
}
return {
accepted: true,
source,
filesDiscovered: supportedFiles.length,
documentsProcessed,
chunksStored,
collectionName: env.qdrantCollection
};
}
private async resolveInputFiles(source: IngestSourceInput): Promise<string[]> {
if (source.sourceType === "file") {
return [source.sourceRef];
}
return listFilesRecursively(source.sourceRef);
}
}

View file

@ -0,0 +1,68 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import pdf from "pdf-parse";
import type { ChunkingMode } from "../process/chunking.js";
export interface ParsedDocument {
title: string;
content: string;
mimeType: string;
chunkMode: ChunkingMode;
}
const documentalExtensions = [".md", ".txt", ".pdf"] as const;
const codeExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".json", ".yml", ".yaml"] as const;
const parserExtensions = [...documentalExtensions, ...codeExtensions] as const;
export function supportedParserExtensions(): string[] {
return [...parserExtensions];
}
export function isSupportedDocument(filePath: string): boolean {
return parserExtensions.includes(path.extname(filePath).toLowerCase() as (typeof parserExtensions)[number]);
}
export function inferChunkMode(filePath: string): ChunkingMode {
const extension = path.extname(filePath).toLowerCase();
if (codeExtensions.includes(extension as (typeof codeExtensions)[number])) {
return "codigo";
}
return "documental";
}
function inferMimeType(extension: string, chunkMode: ChunkingMode): string {
if (extension === ".pdf") {
return "application/pdf";
}
if (extension === ".md") {
return "text/markdown";
}
if (chunkMode === "codigo") {
return "text/x-code";
}
return "text/plain";
}
export async function parseDocument(filePath: string): Promise<ParsedDocument> {
const extension = path.extname(filePath).toLowerCase();
const chunkMode = inferChunkMode(filePath);
if (extension === ".pdf") {
const buffer = await readFile(filePath);
const result = await pdf(buffer);
return {
title: path.basename(filePath),
content: result.text.trim(),
mimeType: inferMimeType(extension, chunkMode),
chunkMode
};
}
const content = await readFile(filePath, "utf8");
return {
title: path.basename(filePath),
content,
mimeType: inferMimeType(extension, chunkMode),
chunkMode
};
}

View file

@ -0,0 +1,325 @@
export type ChunkingMode = "documental" | "codigo";
export interface ChunkingPolicy {
mode: ChunkingMode;
targetCharacters: number;
maxCharacters: number;
overlapCharacters: number;
}
export interface ChunkedDocument {
index: number;
title: string;
content: string;
sectionTitle: string;
startLine: number;
endLine: number;
}
export const documentalChunkingPolicy: ChunkingPolicy = {
mode: "documental",
targetCharacters: 1500,
maxCharacters: 2200,
overlapCharacters: 200
};
export const codeChunkingPolicy: ChunkingPolicy = {
mode: "codigo",
targetCharacters: 1800,
maxCharacters: 2600,
overlapCharacters: 160
};
function normalizeLineBreaks(content: string): string {
return content.replace(/\r\n/g, "\n").trim();
}
function splitIntoSections(content: string): Array<{ sectionTitle: string; body: string; startLine: number }> {
const normalized = normalizeLineBreaks(content);
const lines = normalized.split("\n");
const sections: Array<{ sectionTitle: string; body: string; startLine: number }> = [];
let currentTitle = "General";
let currentLines: string[] = [];
let currentStartLine = 1;
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
const trimmed = line.trim();
if (/^#{1,6}\s+/.test(trimmed)) {
if (currentLines.length > 0) {
sections.push({ sectionTitle: currentTitle, body: currentLines.join("\n").trim(), startLine: currentStartLine });
}
currentTitle = trimmed.replace(/^#{1,6}\s+/, "").trim() || "General";
currentLines = [];
currentStartLine = i + 1;
continue;
}
currentLines.push(line);
}
if (currentLines.length > 0) {
sections.push({ sectionTitle: currentTitle, body: currentLines.join("\n").trim(), startLine: currentStartLine });
}
if (sections.length === 0 && normalized) {
sections.push({ sectionTitle: "General", body: normalized, startLine: 1 });
}
return sections.filter((section) => section.body.trim().length > 0);
}
function splitSectionIntoParagraphs(sectionBody: string): string[] {
return sectionBody
.split(/\n\s*\n/g)
.map((paragraph) => paragraph.trim())
.filter(Boolean);
}
function chunkOversizedText(text: string, maxCharacters: number, overlapCharacters: number): string[] {
const chunks: string[] = [];
let start = 0;
const cleaned = text.trim();
while (start < cleaned.length) {
const end = Math.min(start + maxCharacters, cleaned.length);
const slice = cleaned.slice(start, end).trim();
if (slice) {
chunks.push(slice);
}
if (end >= cleaned.length) {
break;
}
start = Math.max(end - overlapCharacters, start + 1);
}
return chunks;
}
function buildDocumentalChunks(title: string, content: string, policy: ChunkingPolicy): ChunkedDocument[] {
const sections = splitIntoSections(content);
const chunks: ChunkedDocument[] = [];
let index = 0;
for (const section of sections) {
const sectionLineCount = section.body.split("\n").length;
if (section.body.length <= policy.maxCharacters) {
chunks.push({
index: index++,
title,
sectionTitle: section.sectionTitle,
content: section.body,
startLine: section.startLine,
endLine: section.startLine + sectionLineCount - 1
});
continue;
}
const paragraphs = splitSectionIntoParagraphs(section.body);
let buffer = "";
let bufferStartLine = section.startLine;
let consumedLines = 0;
for (const paragraph of paragraphs) {
const paragraphLines = paragraph.split("\n").length;
const candidate = buffer ? `${buffer}\n\n${paragraph}` : paragraph;
if (candidate.length <= policy.targetCharacters) {
buffer = candidate;
consumedLines += paragraphLines + (buffer ? 1 : 0);
continue;
}
if (buffer) {
chunks.push({
index: index++,
title,
sectionTitle: section.sectionTitle,
content: buffer,
startLine: bufferStartLine,
endLine: bufferStartLine + buffer.split("\n").length - 1
});
bufferStartLine = bufferStartLine + buffer.split("\n").length + 1;
buffer = paragraph;
consumedLines = paragraphLines;
} else {
for (const piece of chunkOversizedText(paragraph, policy.maxCharacters, policy.overlapCharacters)) {
chunks.push({
index: index++,
title,
sectionTitle: section.sectionTitle,
content: piece,
startLine: bufferStartLine,
endLine: bufferStartLine + piece.split("\n").length - 1
});
}
bufferStartLine += paragraphLines;
}
}
if (buffer) {
if (buffer.length > policy.maxCharacters) {
for (const piece of chunkOversizedText(buffer, policy.maxCharacters, policy.overlapCharacters)) {
chunks.push({
index: index++,
title,
sectionTitle: section.sectionTitle,
content: piece,
startLine: bufferStartLine,
endLine: bufferStartLine + piece.split("\n").length - 1
});
}
} else {
chunks.push({
index: index++,
title,
sectionTitle: section.sectionTitle,
content: buffer,
startLine: bufferStartLine,
endLine: bufferStartLine + buffer.split("\n").length - 1
});
}
}
}
return chunks;
}
function looksLikeTopLevelCodeStart(line: string): boolean {
const trimmed = line.trim();
if (!trimmed) {
return false;
}
return [
/^export\s+(async\s+)?function\s+/,
/^(async\s+)?function\s+/,
/^export\s+class\s+/,
/^class\s+/,
/^export\s+(const|let|var)\s+[A-Za-z0-9_$]+\s*=\s*(async\s*)?(\([^)]*\)|[A-Za-z0-9_$]+)\s*=>/,
/^(const|let|var)\s+[A-Za-z0-9_$]+\s*=\s*(async\s*)?(\([^)]*\)|[A-Za-z0-9_$]+)\s*=>/,
/^export\s+default\s+/,
/^interface\s+/,
/^type\s+/,
/^enum\s+/,
/^def\s+/,
/^class\s+/,
/^@/,
/^if\s+__name__\s*==\s*["']__main__["']:/
].some((pattern) => pattern.test(trimmed));
}
function inferCodeSectionTitle(block: string): string {
const firstMeaningful = block
.split("\n")
.map((line) => line.trim())
.find(Boolean);
return firstMeaningful?.slice(0, 120) || "code_block";
}
function splitCodeIntoBlocks(content: string): Array<{ sectionTitle: string; body: string; startLine: number }> {
const normalized = content.replace(/\r\n/g, "\n");
const lines = normalized.split("\n");
const blocks: Array<{ sectionTitle: string; body: string; startLine: number }> = [];
let currentLines: string[] = [];
let currentStartLine = 1;
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
const isNewBlock = currentLines.length > 0 && looksLikeTopLevelCodeStart(line);
if (isNewBlock) {
const body = currentLines.join("\n").trim();
if (body) {
blocks.push({
sectionTitle: inferCodeSectionTitle(body),
body,
startLine: currentStartLine
});
}
currentLines = [line];
currentStartLine = i + 1;
continue;
}
currentLines.push(line);
}
const finalBody = currentLines.join("\n").trim();
if (finalBody) {
blocks.push({
sectionTitle: inferCodeSectionTitle(finalBody),
body: finalBody,
startLine: currentStartLine
});
}
return blocks;
}
function splitCodeBlockByLength(block: string, policy: ChunkingPolicy): string[] {
const lines = block.split("\n");
const chunks: string[] = [];
let buffer: string[] = [];
let bufferLength = 0;
for (const line of lines) {
const nextLength = bufferLength + line.length + 1;
if (buffer.length > 0 && nextLength > policy.targetCharacters) {
chunks.push(buffer.join("\n").trim());
const overlapLines = buffer.join("\n").slice(-policy.overlapCharacters).split("\n").filter(Boolean);
buffer = overlapLines.length > 0 ? overlapLines : [];
bufferLength = buffer.join("\n").length;
}
buffer.push(line);
bufferLength += line.length + 1;
}
if (buffer.length > 0) {
chunks.push(buffer.join("\n").trim());
}
return chunks.filter(Boolean);
}
function buildCodeChunks(title: string, content: string, policy: ChunkingPolicy): ChunkedDocument[] {
const blocks = splitCodeIntoBlocks(content);
const chunks: ChunkedDocument[] = [];
let index = 0;
for (const block of blocks) {
if (block.body.length <= policy.maxCharacters) {
chunks.push({
index: index++,
title,
sectionTitle: block.sectionTitle,
content: block.body,
startLine: block.startLine,
endLine: block.startLine + block.body.split("\n").length - 1
});
continue;
}
let currentStartLine = block.startLine;
for (const piece of splitCodeBlockByLength(block.body, policy)) {
const lineCount = piece.split("\n").length;
chunks.push({
index: index++,
title,
sectionTitle: block.sectionTitle,
content: piece,
startLine: currentStartLine,
endLine: currentStartLine + lineCount - 1
});
currentStartLine += lineCount;
}
}
return chunks;
}
export function chunkDocument(title: string, content: string, policy: ChunkingPolicy): ChunkedDocument[] {
if (policy.mode === "codigo") {
return buildCodeChunks(title, content, policy);
}
return buildDocumentalChunks(title, content, policy);
}

View file

@ -0,0 +1,238 @@
import type { EmbeddingProvider } from "../embeddings/provider.js";
import type { VectorStoreClient } from "../vectorstore/client.js";
import type { ChunkMode, RetrieveIntent, RetrieveResponse, RetrieveScope } from "../../shared/types/rag.js";
function unique(values: string[]): string[] {
return [...new Set(values.filter(Boolean))];
}
function normalizeQuery(value: string): string {
return value.trim().replace(/\s+/g, " ");
}
function scoreBoostFromContent(content: string): number {
let boost = 0;
if (/regla|importante|critico|critica/i.test(content)) {
boost += 0.08;
}
if (/pendiente|backlog|task|objetivo/i.test(content)) {
boost += 0.06;
}
if (/workspace|proyecto|documentacion/i.test(content)) {
boost += 0.04;
}
if (/objetivo|objetivos principales|principios del sistema|resultado esperado|vision del sistema/i.test(content)) {
boost += 0.16;
}
if (/\*\*proyecto:\*\*|\*\*modulo:\*\*|\*\*ultima actualizacion:\*\*/i.test(content)) {
boost -= 0.18;
}
return boost;
}
function scoreBoostFromTitle(title: string): number {
let boost = 0;
if (/pendientes_generales/i.test(title)) {
boost += 0.1;
}
if (/ids/i.test(title)) {
boost += 0.12;
}
if (/service|client|chunking|provider|server|app/i.test(title)) {
boost += 0.05;
}
if (/readme|indice_documentacion|historial_sesiones|task/i.test(title)) {
boost += 0.03;
}
return boost;
}
function scoreBoostFromQueryAlignment(query: string, item: RetrieveResponse["items"][number]): number {
const source = `${item.title} ${item.content}`.toLowerCase();
const normalizedQuery = query.toLowerCase();
let boost = 0;
if (/pendiente|backlog|por hacer|siguiente/i.test(normalizedQuery) && /pendiente|backlog|lineas de trabajo|proximos pasos/i.test(source)) {
boost += 0.18;
}
if (/regla|norma|protocolo|instruccion/i.test(normalizedQuery) && /regla|norma|protocolo|instruccion/i.test(source)) {
boost += 0.18;
}
if (/sesion|historial|registro/i.test(normalizedQuery) && /sesion|historial|registro/i.test(source)) {
boost += 0.16;
}
if (/indice|documentacion|mapa/i.test(normalizedQuery) && /indice|documentacion|mapa/i.test(source)) {
boost += 0.14;
}
if (/funcion|metodo|clase|endpoint|service|client|chunk|codigo|linea|source_id|document_id|qdrant|embedding/i.test(normalizedQuery) && /function|class|endpoint|service|client|chunk|codigo|source_id|document_id|qdrant|embedding|def\s+/i.test(source)) {
boost += 0.2;
}
if (/source_id|document_id|chunk_id|ids/i.test(normalizedQuery) && /buildsourceid|builddocumentid|buildchunkid|ids\.ts|source_id|document_id|chunk_id/i.test(source)) {
boost += 0.28;
}
if (/source_id/i.test(normalizedQuery) && /buildsourceid|source_id/i.test(source)) {
boost += 0.22;
}
if (/document_id/i.test(normalizedQuery) && /builddocumentid|document_id/i.test(source)) {
boost += 0.22;
}
if (/chunk_id/i.test(normalizedQuery) && /buildchunkid|chunk_id/i.test(source)) {
boost += 0.22;
}
return boost;
}
function rankItems(query: string, items: RetrieveResponse["items"]): RetrieveResponse["items"] {
return [...items].sort((left, right) => {
const leftScore = left.score + scoreBoostFromTitle(left.title) + scoreBoostFromContent(left.content) + scoreBoostFromQueryAlignment(query, left);
const rightScore = right.score + scoreBoostFromTitle(right.title) + scoreBoostFromContent(right.content) + scoreBoostFromQueryAlignment(query, right);
return rightScore - leftScore;
});
}
function buildBootstrapQueries(query: string): string[] {
const cleaned = normalizeQuery(query);
return unique([
cleaned,
`${cleaned} mapa general`,
`${cleaned} documentacion principal`,
`${cleaned} pendientes y lineas de trabajo`,
`${cleaned} reglas y estructura del workspace`
]);
}
function buildSpecificQueries(query: string): string[] {
const cleaned = normalizeQuery(query);
const queries = [cleaned];
if (/pendiente|backlog|por hacer|siguiente/i.test(cleaned)) {
queries.push(`${cleaned} pendientes backlog proximos pasos`);
queries.push(`${cleaned} lineas de trabajo prioritarias`);
queries.push(`${cleaned} sistema basico RAG estructura MCP Retell`);
}
if (/regla|norma|protocolo|instruccion/i.test(cleaned)) {
queries.push(`${cleaned} reglas protocolo instrucciones`);
}
if (/sesion|historial|registro/i.test(cleaned)) {
queries.push(`${cleaned} historial sesiones registro`);
}
if (/indice|documentacion|mapa/i.test(cleaned)) {
queries.push(`${cleaned} indice documentacion mapa`);
}
if (/caracteristica|como funciona|objetivo|modulo|arquitectura|stack/i.test(cleaned)) {
queries.push(`${cleaned} sistema rag base arquitectura stack tecnico`);
queries.push(`${cleaned} ingesta procesado salida embeddings qdrant`);
queries.push(`${cleaned} objetivo principios casos de uso`);
queries.push(`${cleaned} vision del sistema objetivos principales resultado esperado`);
}
if (/funcion|metodo|clase|endpoint|service|client|source_id|document_id|chunk|qdrant|embedding|codigo/i.test(cleaned)) {
queries.push(`${cleaned} funcion metodo clase service client`);
queries.push(`${cleaned} endpoint api qdrant embeddings retrieve answer ingest`);
queries.push(`${cleaned} source_id document_id chunk_id codigo`);
queries.push(`${cleaned} buildSourceId buildDocumentId buildChunkId ids.ts`);
}
return unique(queries);
}
function buildSpecificSummary(topics: string[], itemsCount: number): string {
if (itemsCount === 0) {
return "No se recupero contexto relevante para la consulta.";
}
return `Se recuperaron ${itemsCount} fragmentos relevantes sobre: ${topics.slice(0, 4).join(", ") || "tema consultado"}.`;
}
function buildBootstrapSummary(topics: string[], criticalPoints: string[], followUpRefs: string[], itemsCount: number): string {
if (itemsCount === 0) {
return "No se recupero contexto suficiente para construir el mapa inicial del dominio.";
}
const themeText = topics.slice(0, 4).join(", ") || "los documentos principales";
const criticalText = criticalPoints.slice(0, 3).join(", ") || "sin puntos criticos destacados";
return `Mapa inicial construido con ${itemsCount} fragmentos. Temas base: ${themeText}. Puntos a tener presentes: ${criticalText}. Referencias principales para profundizar: ${followUpRefs.slice(0, 4).join(", ") || "no disponibles"}.`;
}
async function embedQuery(provider: EmbeddingProvider, query: string): Promise<number[]> {
const [vector] = await provider.embed([query]);
return vector;
}
export class RetrieveService {
constructor(
private readonly embeddingProvider: EmbeddingProvider,
private readonly vectorStore: VectorStoreClient
) {}
async retrieve(mode: ChunkMode, intent: RetrieveIntent, query: string, scope?: RetrieveScope): Promise<RetrieveResponse> {
const items = intent === "bootstrap"
? await this.retrieveBootstrap(mode, query, scope)
: await this.retrieveSpecific(mode, query, scope);
const topics = unique(items.map((item) => item.title)).slice(0, 8);
const criticalPoints = items
.filter((item) => /critico|critical|importante|pendiente/i.test(item.content))
.map((item) => item.title)
.filter(Boolean)
.slice(0, 5);
const followUpRefs = unique(items.map((item) => item.documentId)).slice(0, 8);
const summary = intent === "bootstrap"
? buildBootstrapSummary(topics, criticalPoints, followUpRefs, items.length)
: buildSpecificSummary(topics, items.length);
return {
mode,
intent,
summary,
topics,
criticalPoints,
items,
followUpRefs,
scope
};
}
private async retrieveSpecific(mode: ChunkMode, query: string, scope?: RetrieveScope) {
const queries = buildSpecificQueries(query);
const merged = new Map<string, RetrieveResponse["items"][number]>();
for (const subquery of queries) {
const queryVector = await embedQuery(this.embeddingProvider, subquery);
const results = await this.vectorStore.search(queryVector, 6, mode, scope);
for (const item of results) {
const existing = merged.get(item.chunkId);
if (!existing || item.score > existing.score) {
merged.set(item.chunkId, item);
}
}
}
return rankItems(query, [...merged.values()]).slice(0, 8);
}
private async retrieveBootstrap(mode: ChunkMode, query: string, scope?: RetrieveScope) {
const queries = buildBootstrapQueries(query);
const merged = new Map<string, RetrieveResponse["items"][number]>();
for (const subquery of queries) {
const queryVector = await embedQuery(this.embeddingProvider, subquery);
const results = await this.vectorStore.search(queryVector, 6, mode, scope);
for (const item of results) {
const existing = merged.get(item.chunkId);
if (!existing || item.score > existing.score) {
merged.set(item.chunkId, item);
}
}
}
return rankItems(query, [...merged.values()]).slice(0, 12);
}
}

View file

@ -0,0 +1,136 @@
import { QdrantClient } from "@qdrant/js-client-rest";
import { env } from "../../config/env.js";
import type { IngestedChunk, RetrieveScope, RetrievedItem } from "../../shared/types/rag.js";
function buildQdrantClient(): QdrantClient {
const url = new URL(env.qdrantUrl);
return new QdrantClient({
host: url.hostname,
port: Number(url.port || (url.protocol === "https:" ? 443 : 6333)),
https: url.protocol === "https:",
apiKey: env.qdrantApiKey || undefined,
checkCompatibility: false
});
}
export interface VectorStoreClient {
readonly kind: string;
healthcheck(): Promise<{ ok: boolean; kind: string }>;
ensureCollection(vectorSize: number): Promise<void>;
upsert(chunks: IngestedChunk[]): Promise<void>;
search(queryVector: number[], limit: number, mode?: string, scope?: RetrieveScope): Promise<RetrievedItem[]>;
}
function buildSearchFilter(mode?: string, scope?: RetrieveScope) {
const must: Array<Record<string, unknown>> = [];
if (mode && mode !== "auto") {
must.push({
key: "chunk_mode",
match: { value: mode }
});
}
if (scope?.sourceId) {
must.push({
key: "source_id",
match: { value: scope.sourceId }
});
}
if (scope?.sourceRef) {
must.push({
key: "source_ref",
match: { value: scope.sourceRef }
});
}
if (scope?.tags?.length) {
for (const tag of scope.tags) {
must.push({
key: "tags",
match: { any: [tag] }
});
}
}
return must.length > 0 ? { must } : undefined;
}
export class QdrantVectorStoreClient implements VectorStoreClient {
readonly kind = "qdrant";
private readonly client: QdrantClient;
private collectionReady = false;
constructor() {
this.client = buildQdrantClient();
}
async healthcheck(): Promise<{ ok: boolean; kind: string }> {
try {
await this.client.getCollections();
return { ok: true, kind: this.kind };
} catch {
return { ok: false, kind: this.kind };
}
}
async ensureCollection(vectorSize: number): Promise<void> {
if (this.collectionReady) {
return;
}
const collections = await this.client.getCollections();
const exists = collections.collections.some((collection) => collection.name === env.qdrantCollection);
if (!exists) {
await this.client.createCollection(env.qdrantCollection, {
vectors: {
size: vectorSize,
distance: "Cosine"
}
});
}
this.collectionReady = true;
}
async upsert(chunks: IngestedChunk[]): Promise<void> {
if (chunks.length === 0) {
return;
}
await this.ensureCollection(chunks[0].vector.length);
await this.client.upsert(env.qdrantCollection, {
wait: true,
points: chunks.map((chunk) => ({
id: chunk.id,
vector: chunk.vector,
payload: chunk.payload
}))
});
}
async search(queryVector: number[], limit: number, mode?: string, scope?: RetrieveScope): Promise<RetrievedItem[]> {
await this.ensureCollection(queryVector.length);
const result = await this.client.search(env.qdrantCollection, {
vector: queryVector,
limit,
with_payload: true,
filter: buildSearchFilter(mode, scope)
});
return result.map((point) => ({
chunkId: String(point.payload?.chunk_id ?? point.id),
documentId: String(point.payload?.document_id ?? ""),
sourceId: String(point.payload?.source_id ?? ""),
title: String(point.payload?.title ?? ""),
sectionTitle: point.payload?.section_title ? String(point.payload.section_title) : undefined,
content: String(point.payload?.content ?? ""),
score: Number(point.score ?? 0),
chunkMode: point.payload?.chunk_mode ? String(point.payload.chunk_mode) as "documental" | "codigo" : undefined,
startLine: point.payload?.start_line ? Number(point.payload.start_line) : undefined,
endLine: point.payload?.end_line ? Number(point.payload.end_line) : undefined
}));
}
}

8
RAG/src/server.ts Normal file
View file

@ -0,0 +1,8 @@
import { createApp } from "./app.js";
import { env } from "./config/env.js";
const app = createApp();
app.listen(env.port, () => {
console.log(`RAG service listening on port ${env.port}`);
});

View file

@ -0,0 +1,78 @@
export type ChunkMode = "documental" | "codigo" | "auto";
export type RetrieveIntent = "bootstrap" | "specific";
export type IngestMode = "mechanical" | "interactive";
export type SourceType = "file" | "folder";
export interface IngestSourceInput {
sourceId?: string;
sourceType: SourceType;
sourceRef: string;
mode?: IngestMode;
tags?: string[];
}
export interface IngestedChunk {
id: string;
vector: number[];
payload: Record<string, unknown>;
}
export interface IngestResult {
accepted: true;
source: IngestSourceInput;
filesDiscovered: number;
documentsProcessed: number;
chunksStored: number;
collectionName: string;
}
export interface RetrievedItem {
chunkId: string;
documentId: string;
sourceId: string;
title: string;
sectionTitle?: string;
content: string;
score: number;
chunkMode?: ChunkMode;
startLine?: number;
endLine?: number;
}
export interface RetrieveScope {
sourceId?: string;
sourceRef?: string;
tags?: string[];
}
export interface RetrieveResponse {
mode: ChunkMode;
intent: RetrieveIntent;
summary: string;
topics: string[];
criticalPoints: string[];
items: RetrievedItem[];
followUpRefs: string[];
scope?: RetrieveScope;
}
export interface AnswerResponse {
mode: ChunkMode;
intent: RetrieveIntent;
answer: string;
summary: string;
topics: string[];
criticalPoints: string[];
citations: Array<{
chunkId: string;
documentId: string;
title: string;
sectionTitle?: string;
startLine?: number;
endLine?: number;
}>;
scope?: RetrieveScope;
}

View file

@ -0,0 +1,17 @@
import { readdir } from "node:fs/promises";
import path from "node:path";
export async function listFilesRecursively(inputPath: string): Promise<string[]> {
const entries = await readdir(inputPath, { withFileTypes: true });
const files = await Promise.all(
entries.map(async (entry) => {
const resolved = path.join(inputPath, entry.name);
if (entry.isDirectory()) {
return listFilesRecursively(resolved);
}
return [resolved];
})
);
return files.flat();
}

View file

@ -0,0 +1,38 @@
import path from "node:path";
import { createHash } from "node:crypto";
function asciiNormalize(value: string): string {
return value
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/ñ/gi, (char) => (char === "Ñ" ? "N" : "n"));
}
export function normalizeDocumentKey(value: string): string {
return asciiNormalize(value)
.replace(/\\/g, "/")
.replace(/\s+/g, "_")
.replace(/_+/g, "_")
.replace(/[^a-zA-Z0-9_./-]/g, "")
.toLowerCase();
}
export function buildSourceId(sourceType: string, sourceRef: string): string {
const readable = normalizeDocumentKey(path.basename(sourceRef) || sourceRef).replace(/\.[^.]+$/, "") || "source";
const suffix = createHash("sha1").update(sourceRef).digest("hex").slice(0, 8);
const sourceName = `${readable}_${suffix}`;
return `src:default:${sourceType}:${sourceName}`;
}
export function buildDocumentId(sourceId: string, documentKey: string): string {
return `doc:${sourceId}:${documentKey}`;
}
export function buildChunkId(documentId: string, chunkMode: string, chunkIndex: number): string {
return `chk:${documentId}:${chunkMode}:${String(chunkIndex + 1).padStart(4, "0")}`;
}
export function buildQdrantPointId(value: string): string {
const hex = createHash("sha1").update(value).digest("hex").slice(0, 32);
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
}

14
RAG/tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}

View file

@ -0,0 +1,74 @@
# Historial de sesiones
## Proyecto: Workspace de tools IA para empresas
Este archivo registra agentes y sesiones de trabajo de este workspace.
---
## Indice de agentes
| Agente | Responsabilidad | Identificador |
|--------|-----------------|---------------|
| **Agente tools IA para potenciar servicios empresariales** | Desarrollo de tools, herramientas, skills, RAGs, MCPs y utilidades para potenciar soluciones con IA para empresas | `session_id OpenCode por workspace cuando aplique` |
---
## Sesiones de trabajo
### Sesion 1 (2026-04-02) - Agente tools IA para potenciar servicios empresariales
**Agente:** **Agente tools IA para potenciar servicios empresariales**
**Modelo:** gpt-5.4
**Conversation ID:** `N/D (OpenCode no lo expone en este entorno)`
**Session ID OpenCode:** `ses_2b208e826ffeqyzvUVG7Tal0r0`
**Titulo de sesion:** `Registro de agente para tools IA empresariales`
**Directorio:** `/home/pancho/Documentos/Empresa/Desarrollo/IA`
#### Trabajo realizado:
- Registro del agente para este workspace.
- Lectura y alineacion inicial de la documentacion base.
- Creacion de `docs/PENDIENTES_GENERALES.md` con las primeras lineas de trabajo.
- Limpieza de documentacion heredada de otros proyectos en archivos base del workspace.
- Creacion inicial del documento marco del sistema RAG reutilizable.
- Definicion inicial del modulo de ingesta en `RAG/docs/INGESTA.md`.
- Definicion inicial del modulo de procesado en `RAG/docs/PROCESADO.md`, incluyendo la decision de una estrategia de chunking comun para la v1.
- Creacion de `RAG/docs/BITACORA_DISENO_RAG.md` para conservar el camino de decisiones, razones y posibilidades abiertas del diseño del sistema.
- Definicion inicial del modulo de salida en `RAG/docs/SALIDA.md`, incluyendo la estructura acordada para el `retrieve` inicial como paquete de contexto de arranque.
- Ajuste del alcance de la v1 para dejar explicito que los PDFs deben estar soportados desde el inicio y que el modo `codigo` sigue siendo parte de la direccion del sistema.
- Definicion de `RAG/docs/STACK_TECNICO_V1.md` con el stack tecnico minimo acordado para construir la primera version funcional del sistema.
- Aclaracion del diseño de embeddings desacoplados, dejando documentado que los cambios de modelo requieren reindexacion coherente y trazabilidad por proveedor/modelo.
- Cierre de la decision de embeddings fijando `Qwen3 Embedding 8B` como modelo base estable del sistema, con arranque via proveedor compatible y objetivo futuro de despliegue local.
- Creacion del scaffold inicial de `RAG/` con API Express, modulos base, Dockerfile, configuracion por entorno y orientacion de despliegue compatible con EasyPanel.
- Implementacion funcional inicial del pipeline con parsers `md/txt/pdf`, chunking documental, proveedor real de embeddings via OpenRouter, cliente Qdrant y endpoints operativos de ingesta y retrieve.
- Creacion de `RAG/.env.local` como fichero local de claves y configuracion sensible fuera del control de versiones.
- Prueba real completada contra `Qdrant` remoto en EasyPanel, con ingesta funcional de `docs/` y retrieves reales en modos `specific` y `bootstrap`.
- Ampliacion del retrieve para aceptar `scope` por fuente, referencia o tags, dejando preparado el bootstrap enfocado por workspace o proyecto.
- Mejora del `retrieve bootstrap` mediante subconsultas internas y sintesis orientada a mapa inicial del dominio.
- Implementacion funcional de `POST /answer`, apoyado en `retrieve` y con respuesta generada por modelo usando solo el contexto recuperado.
- Mejora de `retrieve specific` para preguntas operativas frecuentes, logrando respuestas mas concretas sobre backlog y estado del workspace.
- Limpieza de la coleccion `rag_chunks`, reingesta separada de `docs/` y `RAG/docs/`, y prueba satisfactoria de consultas acotadas por `scope` a cada fuente.
- Implementacion completa del modo `codigo`, incluyendo ingesta de `RAG/src/`, chunking semantico por bloques top-level, retrieve filtrado y respuesta con referencias de lineas.
- Prueba satisfactoria del modo `codigo` con una consulta real sobre la construccion de `source_id` en el sistema.
- Reorganizacion de RAG como modulo raiz independiente con documentacion propia en `RAG/docs/`.
- Ajuste del indice documental global para reflejar la separacion entre documentacion global y documentacion por tool.
- Creacion de `docs/TASK.md` para descomponer lineas de trabajo amplias en puntos de analisis y acuerdos.
#### Estado final:
- Documentacion base alineada con este workspace.
- Agente registrado.
- Backlog inicial disponible para continuar el trabajo.
- Carpeta `RAG/docs/` creada y primera documentacion RAG registrada con la estructura correcta del workspace.
- Existe ya un documento global para partir planificaciones amplias en tasks de trabajo.
- Existe ya una base tecnica compilable del modulo `RAG/` lista para continuar con implementacion funcional.
- Existe ya una primera implementacion funcional compilable del servicio RAG, pendiente de probar con claves reales y Qdrant activo.
- El servicio ya ha sido probado con embeddings reales y vector store remoto operativo.
- El servicio soporta ya modo documental y modo codigo con pruebas reales sobre documentacion y codigo del propio modulo.
---
## Agentes activos
### Agente tools IA para potenciar servicios empresariales
- **Responsabilidad:** Desarrollo de tools, herramientas, skills, RAGs, MCPs y utilidades para potenciar soluciones con IA para empresas.
- **Estado:** Activo
- **Trabajo principal:** Desarrollo de herramientas reutilizables e integraciones para potenciar otros servicios de IA empresariales.

View file

@ -0,0 +1,250 @@
# Indice de documentacion
**Proyecto:** Workspace de tools IA para empresas
**Ultima actualizacion:** 2026-04-02
**Ultima modificacion por:** Agente tools IA para potenciar servicios empresariales
---
## Proposito
Este documento es el mapa de la documentacion activa del workspace.
Usalo para:
- saber que documentos existen
- evitar documentacion duplicada
- identificar donde registrar cambios del workspace
- mantener limpia la documentacion de este proyecto
---
## Protocolo rapido
1. Lee `docs/README.md` antes de crear o reorganizar documentacion.
2. Verifica aqui si el documento ya existe o si su contenido pertenece a otro archivo.
3. Si creas un documento nuevo, anadelo aqui en la misma sesion.
4. Si limpias documentacion heredada, actualiza este indice para reflejar el estado real.
Convencion de estructura:
- `docs/` en raiz contiene solo documentacion global del workspace.
- cada tool o modulo independiente debe vivir en su propia carpeta raiz.
- la documentacion propia de cada tool debe vivir dentro de su carpeta, por ejemplo `RAG/docs/`.
---
## Documentos activos
### `README.md`
**Ubicacion:** `docs/README.md`
**Proposito:**
Reglas base para agentes y criterios de trabajo del workspace.
**Cuando leerlo:**
- al empezar a trabajar en este workspace
- antes de crear o modificar documentacion
**Cuando actualizarlo:**
- si cambian las reglas del workspace
- si se incorporan nuevas normas de documentacion o trabajo
---
### `INDICE_DOCUMENTACION.md`
**Ubicacion:** `docs/INDICE_DOCUMENTACION.md`
**Proposito:**
Inventario maestro de la documentacion vigente.
**Cuando leerlo:**
- antes de crear un nuevo documento
- al revisar estructura documental del workspace
**Cuando actualizarlo:**
- cuando se crea, elimina o renombra un documento en `docs/`
---
### `HISTORIAL_SESIONES.md`
**Ubicacion:** `docs/HISTORIAL_SESIONES.md`
**Proposito:**
Registro de agentes y sesiones de trabajo de este workspace.
**Cuando leerlo:**
- al identificar un agente
- al revisar continuidad del trabajo anterior
**Cuando actualizarlo:**
- al registrar una nueva sesion relevante
- al dar de alta o ajustar un agente activo
---
### `PENDIENTES_GENERALES.md`
**Ubicacion:** `docs/PENDIENTES_GENERALES.md`
**Proposito:**
Backlog principal de tools, experimentos, investigaciones e integraciones del workspace.
**Cuando leerlo:**
- al decidir en que trabajar a continuacion
- al revisar prioridades del workspace
**Cuando actualizarlo:**
- cuando aparece una nueva linea de trabajo
- cuando cambia el enfoque o estado de una iniciativa
---
### `TASK.md`
**Ubicacion:** `docs/TASK.md`
**Proposito:**
Descomponer lineas de trabajo amplias en bloques de analisis, decisiones y acuerdos para avanzar sin dejar puntos importantes fuera.
**Cuando leerlo:**
- cuando una linea de trabajo requiera varias conversaciones o decisiones encadenadas
- al continuar una planificacion tecnica ya iniciada
**Cuando actualizarlo:**
- cuando se abra una nueva task de analisis relevante
- cuando cambien los puntos a convenir de una task activa
---
### `RAG/docs/SISTEMA_RAG_BASE.md`
**Ubicacion:** `RAG/docs/SISTEMA_RAG_BASE.md`
**Proposito:**
Documento marco del sistema RAG base reutilizable que se quiere construir para este workspace y para futuros proyectos.
**Cuando leerlo:**
- al iniciar el trabajo sobre la linea RAG
- al necesitar recordar objetivos, alcance y enfoque del sistema
**Cuando actualizarlo:**
- cuando cambie la vision del sistema RAG
- cuando se definan o ajusten objetivos clave del diseño base
---
### `RAG/docs/INGESTA.md`
**Ubicacion:** `RAG/docs/INGESTA.md`
**Proposito:**
Definir el modulo de ingesta del sistema RAG, su alcance, responsabilidades y preparacion para futuras actualizaciones.
**Cuando leerlo:**
- al diseñar la entrada de conocimiento al RAG
- al decidir como se incorporan y registran las fuentes
**Cuando actualizarlo:**
- cuando cambie el alcance de la ingesta
- cuando se acuerden metadatos, tipos de fuente o comportamiento base
---
### `RAG/docs/PROCESADO.md`
**Ubicacion:** `RAG/docs/PROCESADO.md`
**Proposito:**
Definir el modulo de procesado del RAG, especialmente la preparacion del contenido y la estrategia de chunking.
**Cuando leerlo:**
- al definir como se transforma el contenido ingerido antes de indexarlo
- al trabajar decisiones sobre chunking y estructura recuperable
**Cuando actualizarlo:**
- cuando se acuerde o cambie la estrategia de chunking
- cuando se amplie el tratamiento por tipo de documento o fuente
---
### `RAG/docs/BITACORA_DISENO_RAG.md`
**Ubicacion:** `RAG/docs/BITACORA_DISENO_RAG.md`
**Proposito:**
Mantener una vision panoramica del camino de diseño del RAG, incluyendo decisiones, razonamiento, contexto y opciones abiertas.
**Cuando leerlo:**
- al retomar la conversacion de diseño del RAG
- al necesitar entender por que se eligio una direccion concreta
- al querer ver la vision general que da sentido al resto de documentos del modulo
**Cuando actualizarlo:**
- cuando se cierre una decision relevante de diseño
- cuando aparezca un cambio de rumbo, matiz importante u opcion abierta que merezca conservarse
---
### `RAG/docs/SALIDA.md`
**Ubicacion:** `RAG/docs/SALIDA.md`
**Proposito:**
Definir la salida del sistema RAG, especialmente la recuperacion de contexto y la estructura del retrieve inicial.
**Cuando leerlo:**
- al diseñar como consumiran el RAG agentes, apps o herramientas
- al revisar que debe devolver `retrieve` y como debe servir de contexto de arranque
**Cuando actualizarlo:**
- cuando cambie la estructura de salida del RAG
- cuando se ajusten los modos de retrieve, answer o sintesis
---
### `RAG/docs/STACK_TECNICO_V1.md`
**Ubicacion:** `RAG/docs/STACK_TECNICO_V1.md`
**Proposito:**
Documentar el stack tecnico minimo acordado para construir la primera version funcional del RAG.
**Cuando leerlo:**
- al iniciar la implementacion tecnica del RAG
- al revisar decisiones de backend, vector store, parsing y forma de acceso
**Cuando actualizarlo:**
- cuando cambie alguna decision tecnica base del stack
- cuando se cierre o cambie el proveedor inicial de embeddings
---
### `sesion_actual_opencode.md`
**Ubicacion:** `docs/sesion_actual_opencode.md`
**Proposito:**
Instruccion universal para detectar la sesion activa de OpenCode del workspace actual.
**Cuando leerlo:**
- al necesitar identificar `session_id`, titulo y directorio de la sesion actual
**Cuando actualizarlo:**
- solo si el usuario lo pide expresamente o cambia el mecanismo canonico de deteccion
---
## Estado actual de la documentacion
- La documentacion global del workspace esta concentrada en `docs/`.
- La documentacion especifica de cada tool debe vivir dentro de su propio modulo raiz.
- Se ha eliminado el arrastre documental de proyectos anteriores en los archivos base.
- Cualquier documento nuevo debe responder al objetivo de tools IA para empresas.
---
## Estadistica global
**Total de documentos indexados:** 12

View file

@ -0,0 +1,139 @@
# Pendientes Generales
**Proyecto:** Workspace de tools IA para empresas
**Ultima actualizacion:** 2026-04-02
**Ultima modificacion por:** Agente tools IA para potenciar servicios empresariales
**Estado:** Activo
---
## Proposito
Este documento centraliza las lineas de trabajo, experimentos, investigaciones y tools pendientes del workspace para dar continuidad al objetivo principal:
- Desarrollar tools, herramientas, skills y componentes reutilizables para potenciar soluciones con IA para empresas.
- Priorizar piezas que puedan integrarse facilmente con servicios ya en funcionamiento.
- Mantener visible el estado de las iniciativas activas y sus siguientes pasos.
---
## Como usar este documento
Este archivo funciona como listado rapido de ideas, lineas de trabajo y frentes pendientes del workspace.
Cuando el usuario pregunte que hay pendiente por hacer, este documento debe permitir responder con un panorama rapido y claro.
No debe convertirse en un gestor detallado de tareas ni duplicar informacion que pertenezca a otros documentos.
---
## Lineas de trabajo prioritarias
### 1. Sistema basico RAG reutilizable
**Objetivo:**
Diseñar una base RAG simple, modular y facil de conectar a otros servicios ya existentes.
**Enfoque inicial:**
- Definir una arquitectura minima reutilizable.
- Permitir conexion sencilla desde otros servicios o agentes.
- Separar claramente ingesta, almacenamiento, recuperacion y consumo.
- Evaluar un formato de configuracion que permita adaptar el RAG a distintos clientes o casos de uso.
**Pendientes iniciales:**
- Definir estructura base del proyecto o modulo.
- Elegir estrategia inicial de almacenamiento y vectorizacion.
- Definir interfaz de integracion con servicios externos.
- Identificar un primer caso real de uso para validacion.
**Estado:** Pendiente
---
### 2. Estructura MCP para integracion de tools
**Objetivo:**
Preparar una estructura MCP que permita ir conectando nuestras tools con otros servicios que vayamos desarrollando y poniendo en funcionamiento.
**Enfoque inicial:**
- Dejar una base de organizacion clara para futuras tools.
- Definir criterios para exponer capacidades de forma consistente.
- Diseñar una estructura que facilite escalar nuevas integraciones.
- Mantener separacion entre tools independientes y tools de soporte para otros servicios.
**Pendientes iniciales:**
- Definir estructura minima MCP del workspace.
- Establecer convenciones de organizacion para nuevas tools.
- Identificar primeras capacidades a exponer.
- Documentar como se conectara con servicios internos y externos.
**Estado:** Pendiente
---
### 3. Potenciar agentes de Retell con herramientas y flujos externos
**Objetivo:**
Preparar una integracion para que Retell pueda conectarse con flujos y servicios externos, como n8n y otros sistemas, de manera que el agente telefonico pueda apoyarse en herramientas adicionales y fuentes externas de informacion.
**Contexto actual:**
- Existe un flujo en n8n para atencion al cliente.
- Ese flujo ya dispone de varias herramientas para atender clientes de una empresa concreta.
- Actualmente las peticiones suelen llegar desde WhatsApp.
- Se quiere que Retell pueda aprovechar ese tipo de flujos externos y tambien futuras integraciones similares.
**Enfoque inicial:**
- Analizar como Retell puede invocar herramientas o endpoints externos.
- Diseñar una capa de integracion reutilizable para conectar Retell con n8n u otros servicios.
- Reutilizar herramientas ya existentes sin acoplarlas a un unico canal.
- Dejar preparada una via estandar para futuras integraciones de voz con fuentes externas.
**Pendientes iniciales:**
- Definir payloads de entrada y salida entre Retell y servicios externos.
- Identificar endpoint, adaptador o capa intermedia de integracion.
- Mapear como reutilizar el flujo actual de n8n desde Retell.
- Detectar necesidades de contexto, autenticacion, trazabilidad y control de errores.
**Estado:** Pendiente
---
### 4. Explorar posible potenciacion del RAG con Obsidian
**Objetivo:**
Evaluar si Obsidian puede aportar valor como capa de organizacion, fuente de conocimiento o apoyo a futuras capacidades del sistema RAG.
**Pendientes iniciales:**
- Revisar en que puntos podria integrarse con el RAG.
- Valorar si aporta ventajas reales para documentacion, enlaces entre conceptos o gestion de conocimiento.
- Dejarlo como linea futura, sin afectar a la salida de la v1.
**Estado:** Pendiente
---
## Criterios generales de desarrollo
- Priorizar soluciones simples y reutilizables.
- Favorecer integraciones desacopladas de un unico canal o proveedor.
- Diseñar cada tool pensando en reutilizacion por otros servicios.
- Documentar decisiones tecnicas y aprendizajes a medida que avancemos.
---
## Proximos pasos sugeridos
1. Definir el alcance minimo viable del sistema RAG base.
2. Diseñar la estructura inicial MCP del workspace.
3. Documentar el punto de integracion entre Retell y servicios externos como n8n.
---
## Resumen rapido
| Linea | Estado |
|-------|--------|
| Sistema basico RAG reutilizable | Pendiente |
| Estructura MCP para integracion de tools | Pendiente |
| Retell conectado con herramientas externas | Pendiente |
| Posible potenciacion del RAG con Obsidian | Pendiente |

75
docs/README.md Normal file
View file

@ -0,0 +1,75 @@
# README para agentes del workspace
## Proyecto: Workspace de tools IA para empresas
Lee este archivo antes de crear documentos, editar documentacion o registrar sesiones.
---
## Objetivo del workspace
En este workspace desarrollamos tools, herramientas, skills, integraciones y componentes reutilizables para potenciar implementaciones de IA en empresas.
Esto incluye, entre otros:
- sistemas RAG
- servidores o estructuras MCP
- integraciones con servicios externos
- herramientas reutilizables para otros servicios de IA
- pruebas tecnicas, experimentos e investigaciones aplicadas
---
## Documentos base
Consulta siempre estos documentos:
1. `docs/INDICE_DOCUMENTACION.md`
2. `docs/README.md`
3. `docs/HISTORIAL_SESIONES.md`
4. `docs/PENDIENTES_GENERALES.md`
5. `docs/sesion_actual_opencode.md`
---
## Reglas de documentacion
1. Toda la documentacion del workspace va en `docs/`.
2. Antes de crear un documento nuevo, revisa `docs/INDICE_DOCUMENTACION.md` para evitar duplicados.
3. Si creas un documento nuevo, indexalo en `docs/INDICE_DOCUMENTACION.md` en la misma sesion.
4. Si haces un cambio relevante en el workspace, revisa si hay que actualizar `docs/PENDIENTES_GENERALES.md` o `docs/HISTORIAL_SESIONES.md`.
5. `docs/sesion_actual_opencode.md` es una instruccion universal del workspace y no debe modificarse salvo peticion explicita del usuario.
---
## Identificacion del agente
Si el usuario pide identificacion, revisa `docs/HISTORIAL_SESIONES.md` y responde con:
```text
Soy el [Nombre del Agente].
Trabajo en el proyecto: [Nombre del Proyecto].
Mi Conversation ID es: [valor disponible o N/D].
Me encargo de: [responsabilidad].
```
En este entorno OpenCode puede no exponer `Conversation ID`, por lo que puede usarse `session_id` del workspace como referencia operativa cuando aplique.
---
## Registro de sesiones
Al finalizar una sesion de trabajo relevante:
1. Actualiza `docs/HISTORIAL_SESIONES.md`.
2. Registra el agente, modelo y trabajo realizado.
3. Incluye `session_id` de OpenCode cuando sea util para identificar la sesion del workspace.
---
## Criterios de trabajo
1. Priorizar soluciones simples, reutilizables y desacopladas.
2. Pensar cada tool como pieza aprovechable por otros servicios.
3. Mantener foco en integraciones utiles para IA empresarial.
4. Evitar arrastrar documentacion de otros proyectos a este workspace.
5. Mantener la documentacion alineada con el estado real del workspace.

73
docs/TASK.md Normal file
View file

@ -0,0 +1,73 @@
# TASK
**Proyecto:** Workspace de tools IA para empresas
**Ultima actualizacion:** 2026-04-02
**Ultima modificacion por:** Agente tools IA para potenciar servicios empresariales
---
## Proposito
Este documento sirve para partir lineas de trabajo amplias en bloques de analisis y acuerdos, de forma que podamos avanzar sin dejarnos puntos importantes por el camino.
No sustituye a `docs/PENDIENTES_GENERALES.md`.
- `PENDIENTES_GENERALES.md` mantiene el panorama rapido de ideas y lineas pendientes.
- `TASK.md` descompone una linea concreta cuando requiere varias conversaciones, decisiones o pasos.
---
## Task principal
### Diseño y planeacion del tipo de RAG a crear
**Objetivo de esta task:**
Definir con claridad que tipo de sistema RAG queremos construir para que sea util, reutilizable, robusto desde una primera version sencilla y preparado para crecer hacia proyectos de clientes.
**Puntos a analizar y convenir:**
1. Diseño de fuentes
- Que tipos de fuentes debe soportar el sistema.
- Si empezamos solo con documentos o dejamos preparada entrada para APIs, bases de datos u otras fuentes.
- Como desacoplar conectores para que el sistema se adapte a distintos clientes.
2. Modelo de conocimiento
- Como representar la informacion de forma util para recuperacion.
- Que metadatos minimos conviene guardar desde el inicio.
- Como preparar el sistema para distintas clases de contenido.
3. Estrategia de chunking
- Como dividir la informacion en fragmentos utiles.
- Que nivel de contexto debe conservar cada fragmento.
- Como evitar fragmentos demasiado pequenos, demasiado grandes o sin sentido aislado.
4. Recuperacion
- Como encontrar el contexto mas relevante para una consulta.
- Que enfoque minimo usar en la primera version.
- Como dejar abierta la puerta a mejoras posteriores.
5. Multi-tenant y reutilizacion por cliente
- Como dar servicio a distintos clientes o proyectos con una misma base RAG.
- Como aislar informacion por cliente, app o entorno.
- Como preparar el sistema para que mejoras internas beneficien a todos los consumidores del servicio.
6. Interfaz de integracion
- Como consumiran el RAG otros agentes, tools o servicios.
- Si la primera interfaz sera API, libreria, CLI, MCP o una combinacion.
- Como evitar acoplar el RAG a un unico tipo de consumidor.
7. Observabilidad
- Que informacion necesitaremos para entender que esta recuperando el sistema.
- Como sabremos de donde sale el contexto devuelto.
- Que datos son utiles para depurar y mejorar el RAG.
8. Evaluacion
- Como validar si recupera bien.
- Como medir si la respuesta mejora con el contexto recuperado.
- Como detectar errores, ruido o falta de cobertura.
**Criterio de trabajo para esta task:**
- Empezar simple.
- Diseñar con vision de crecimiento.
- Evitar complejidad innecesaria en la primera version.
- Dejar preparadas las decisiones que condicionan una evolucion futura sana.

View file

@ -0,0 +1,41 @@
# Sesion actual de OpenCode (instruccion universal para cualquier agente)
Objetivo:
- Que cualquier agente, incluso uno simple, pueda identificar su sesion del workspace actual sin ambiguedad.
## Mensaje exacto para dar al agente
Copiar y pegar tal cual:
```text
Lee `docs/sesion_actual_opencode.md` y ejecuta exactamente este comando para identificar tu sesion del workspace actual:
opencode db "SELECT id,title,directory,datetime(time_updated/1000,'unixepoch','localtime') AS updated FROM session WHERE directory='$(pwd)' ORDER BY time_updated DESC LIMIT 1" --format tsv
Luego responde SOLO con:
1) `session_id`
2) `title`
3) `directory`
4) `updated`
Si no hay filas, responde exactamente: `SIN_SESION_EN_ESTE_WORKSPACE`.
No inventes datos.
```
## Comando canonico (fuente de verdad)
```bash
opencode db "SELECT id,title,directory,datetime(time_updated/1000,'unixepoch','localtime') AS updated FROM session WHERE directory='$(pwd)' ORDER BY time_updated DESC LIMIT 1" --format tsv
```
## Continuar automaticamente la sesion detectada
```bash
opencode -s "$(opencode db \"SELECT id FROM session WHERE directory='$(pwd)' ORDER BY time_updated DESC LIMIT 1\" --format tsv | awk 'NR==2{print $1}')"
```
## Ver historial corto del workspace actual (ultimas 10)
```bash
opencode db "SELECT id,title,datetime(time_updated/1000,'unixepoch','localtime') AS updated FROM session WHERE directory='$(pwd)' ORDER BY time_updated DESC LIMIT 10" --format tsv
```