commit 7600f36f4878e3cc6e2b79150031ce922ba8db7b Author: OpenCode Date: Sun Apr 5 17:49:35 2026 +0200 Add functional RAG service with code mode diff --git a/RAG/.dockerignore b/RAG/.dockerignore new file mode 100644 index 0000000..508c2e3 --- /dev/null +++ b/RAG/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +.env +.env.local +npm-debug.log* +tests +docs diff --git a/RAG/.env.example b/RAG/.env.example new file mode 100644 index 0000000..6f561ee --- /dev/null +++ b/RAG/.env.example @@ -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= diff --git a/RAG/.gitignore b/RAG/.gitignore new file mode 100644 index 0000000..3ac57e1 --- /dev/null +++ b/RAG/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.env.local +npm-debug.log* diff --git a/RAG/Dockerfile b/RAG/Dockerfile new file mode 100644 index 0000000..d20a6b5 --- /dev/null +++ b/RAG/Dockerfile @@ -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"] diff --git a/RAG/docs/BITACORA_DISENO_RAG.md b/RAG/docs/BITACORA_DISENO_RAG.md new file mode 100644 index 0000000..4ab245c --- /dev/null +++ b/RAG/docs/BITACORA_DISENO_RAG.md @@ -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:::` +- `document_id`: `doc::` +- `chunk_id`: `chk:::` + +**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 diff --git a/RAG/docs/INGESTA.md b/RAG/docs/INGESTA.md new file mode 100644 index 0000000..0e6ba6e --- /dev/null +++ b/RAG/docs/INGESTA.md @@ -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:::` + +**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::` + +**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:::` + +**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. diff --git a/RAG/docs/PROCESADO.md b/RAG/docs/PROCESADO.md new file mode 100644 index 0000000..1073ac7 --- /dev/null +++ b/RAG/docs/PROCESADO.md @@ -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. diff --git a/RAG/docs/SALIDA.md b/RAG/docs/SALIDA.md new file mode 100644 index 0000000..0c61a70 --- /dev/null +++ b/RAG/docs/SALIDA.md @@ -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. diff --git a/RAG/docs/SISTEMA_RAG_BASE.md b/RAG/docs/SISTEMA_RAG_BASE.md new file mode 100644 index 0000000..2f1b35d --- /dev/null +++ b/RAG/docs/SISTEMA_RAG_BASE.md @@ -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 diff --git a/RAG/docs/STACK_TECNICO_V1.md b/RAG/docs/STACK_TECNICO_V1.md new file mode 100644 index 0000000..263a54f --- /dev/null +++ b/RAG/docs/STACK_TECNICO_V1.md @@ -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. diff --git a/RAG/package-lock.json b/RAG/package-lock.json new file mode 100644 index 0000000..ad697ea --- /dev/null +++ b/RAG/package-lock.json @@ -0,0 +1,1836 @@ +{ + "name": "rag-service", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rag-service", + "version": "0.1.0", + "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" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@qdrant/js-client-rest": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@qdrant/js-client-rest/-/js-client-rest-1.17.0.tgz", + "integrity": "sha512-aZFQeirWVqWAa1a8vJ957LMzcXkFHGbsoRhzc8AkGfg6V0jtK8PlG8/eyyc2xhYsR961FDDx1Tx6nyE0K7lS+A==", + "license": "Apache-2.0", + "dependencies": { + "@qdrant/openapi-typescript-fetch": "1.2.6", + "undici": "^6.23.0" + }, + "engines": { + "node": ">=18.17.0", + "pnpm": ">=8" + }, + "peerDependencies": { + "typescript": ">=4.7" + } + }, + "node_modules/@qdrant/openapi-typescript-fetch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@qdrant/openapi-typescript-fetch/-/openapi-typescript-fetch-1.2.6.tgz", + "integrity": "sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "pnpm": ">=8" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/pdf-parse": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.5.tgz", + "integrity": "sha512-kBfrSXsloMnUJOKi25s3+hRmkycHfLK6A09eRGqF/N8BkQoPUmaCr+q8Cli5FnfohEz/rsv82zAiPz/LXtOGhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-ensure": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz", + "integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pdf-parse": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.4.tgz", + "integrity": "sha512-XRIRcLgk6ZnUbsHsYXExMw+krrPE81hJ6FQPLdBNhhBefqIQKXu/WeTgNBGSwPrfU0v+UCEwn7AoAUOsVKHFvQ==", + "license": "MIT", + "dependencies": { + "node-ensure": "^0.0.0" + }, + "engines": { + "node": ">=6.8.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/mehmet-kozan" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/RAG/package.json b/RAG/package.json new file mode 100644 index 0000000..2967b05 --- /dev/null +++ b/RAG/package.json @@ -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" + } +} diff --git a/RAG/src/app.ts b/RAG/src/app.ts new file mode 100644 index 0000000..ec54bc6 --- /dev/null +++ b/RAG/src/app.ts @@ -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; +} diff --git a/RAG/src/config/env.ts b/RAG/src/config/env.ts new file mode 100644 index 0000000..dd282a7 --- /dev/null +++ b/RAG/src/config/env.ts @@ -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; diff --git a/RAG/src/modules/answer/service.ts b/RAG/src/modules/answer/service.ts new file mode 100644 index 0000000..1f05901 --- /dev/null +++ b/RAG/src/modules/answer/service.ts @@ -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 { + 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 + }; + } +} diff --git a/RAG/src/modules/embeddings/provider.ts b/RAG/src/modules/embeddings/provider.ts new file mode 100644 index 0000000..c972f10 --- /dev/null +++ b/RAG/src/modules/embeddings/provider.ts @@ -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; +} + +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 { + 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); + } +} diff --git a/RAG/src/modules/ingest/service.ts b/RAG/src/modules/ingest/service.ts new file mode 100644 index 0000000..a6461fd --- /dev/null +++ b/RAG/src/modules/ingest/service.ts @@ -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 { + 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 { + if (source.sourceType === "file") { + return [source.sourceRef]; + } + return listFilesRecursively(source.sourceRef); + } +} diff --git a/RAG/src/modules/parsers/parser-registry.ts b/RAG/src/modules/parsers/parser-registry.ts new file mode 100644 index 0000000..be9268c --- /dev/null +++ b/RAG/src/modules/parsers/parser-registry.ts @@ -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 { + 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 + }; +} diff --git a/RAG/src/modules/process/chunking.ts b/RAG/src/modules/process/chunking.ts new file mode 100644 index 0000000..7a0f31b --- /dev/null +++ b/RAG/src/modules/process/chunking.ts @@ -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); +} diff --git a/RAG/src/modules/retrieve/service.ts b/RAG/src/modules/retrieve/service.ts new file mode 100644 index 0000000..e6407cd --- /dev/null +++ b/RAG/src/modules/retrieve/service.ts @@ -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 { + 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 { + 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(); + + 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(); + + 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); + } +} diff --git a/RAG/src/modules/vectorstore/client.ts b/RAG/src/modules/vectorstore/client.ts new file mode 100644 index 0000000..b67f717 --- /dev/null +++ b/RAG/src/modules/vectorstore/client.ts @@ -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; + upsert(chunks: IngestedChunk[]): Promise; + search(queryVector: number[], limit: number, mode?: string, scope?: RetrieveScope): Promise; +} + +function buildSearchFilter(mode?: string, scope?: RetrieveScope) { + const must: Array> = []; + + 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 { + 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 { + 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 { + 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 + })); + } +} diff --git a/RAG/src/server.ts b/RAG/src/server.ts new file mode 100644 index 0000000..31299a6 --- /dev/null +++ b/RAG/src/server.ts @@ -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}`); +}); diff --git a/RAG/src/shared/types/rag.ts b/RAG/src/shared/types/rag.ts new file mode 100644 index 0000000..3fd7b42 --- /dev/null +++ b/RAG/src/shared/types/rag.ts @@ -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; +} + +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; +} diff --git a/RAG/src/shared/utils/files.ts b/RAG/src/shared/utils/files.ts new file mode 100644 index 0000000..c6f8a49 --- /dev/null +++ b/RAG/src/shared/utils/files.ts @@ -0,0 +1,17 @@ +import { readdir } from "node:fs/promises"; +import path from "node:path"; + +export async function listFilesRecursively(inputPath: string): Promise { + 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(); +} diff --git a/RAG/src/shared/utils/ids.ts b/RAG/src/shared/utils/ids.ts new file mode 100644 index 0000000..5b336b3 --- /dev/null +++ b/RAG/src/shared/utils/ids.ts @@ -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)}`; +} diff --git a/RAG/tsconfig.json b/RAG/tsconfig.json new file mode 100644 index 0000000..af62570 --- /dev/null +++ b/RAG/tsconfig.json @@ -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"] +} diff --git a/docs/HISTORIAL_SESIONES.md b/docs/HISTORIAL_SESIONES.md new file mode 100644 index 0000000..a122218 --- /dev/null +++ b/docs/HISTORIAL_SESIONES.md @@ -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. diff --git a/docs/INDICE_DOCUMENTACION.md b/docs/INDICE_DOCUMENTACION.md new file mode 100644 index 0000000..1598f74 --- /dev/null +++ b/docs/INDICE_DOCUMENTACION.md @@ -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 diff --git a/docs/PENDIENTES_GENERALES.md b/docs/PENDIENTES_GENERALES.md new file mode 100644 index 0000000..d71a9e5 --- /dev/null +++ b/docs/PENDIENTES_GENERALES.md @@ -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 | diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..cb63b10 --- /dev/null +++ b/docs/README.md @@ -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. diff --git a/docs/TASK.md b/docs/TASK.md new file mode 100644 index 0000000..1490a14 --- /dev/null +++ b/docs/TASK.md @@ -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. diff --git a/docs/sesion_actual_opencode.md b/docs/sesion_actual_opencode.md new file mode 100644 index 0000000..d7469a4 --- /dev/null +++ b/docs/sesion_actual_opencode.md @@ -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 +```