Add upload and bootstrap chat workflow to playground

This commit is contained in:
Paco POR-CORREO 2026-04-06 15:51:11 +02:00
parent 150aa818ad
commit b7846d89ed
11 changed files with 259 additions and 7 deletions

View file

@ -81,8 +81,10 @@ Payload base:
Campos: Campos:
- `sourceId`: opcional, permite aislar la fuente en un scope propio
- `sourceType`: `file` o `folder` - `sourceType`: `file` o `folder`
- `sourceRef`: ruta de la fuente - `sourceRef`: ruta de la fuente
- `readPath`: uso interno cuando se sube un archivo y el backend lo procesa desde una ruta temporal
- `mode`: `mechanical` o `interactive` - `mode`: `mechanical` o `interactive`
- `tags`: etiquetas opcionales para clasificar la fuente - `tags`: etiquetas opcionales para clasificar la fuente
@ -124,6 +126,20 @@ Respuesta esperada aproximada:
} }
``` ```
Ejemplo de aislamiento en un scope propio:
```bash
curl -sS -X POST "https://rag.por-correo.com/ingest" \
-H "Content-Type: application/json" \
-d '{
"sourceId": "src:default:manual:pdf-tecnico-cliente-a",
"sourceType": "file",
"sourceRef": "/ruta/a/documento.pdf",
"mode": "mechanical",
"tags": ["cliente-a", "pdf-tecnico"]
}'
```
--- ---
### 3. `POST /retrieve` ### 3. `POST /retrieve`
@ -164,6 +180,9 @@ Campos:
- `sourceRef` - `sourceRef`
- `tags` - `tags`
Nota util:
- si una ingesta se hizo con `sourceId` propio, despues puedes consultar solo ese material usando `scope.sourceId`
#### Ejemplo `retrieve` documental #### Ejemplo `retrieve` documental
```bash ```bash
@ -368,6 +387,26 @@ Payload base:
} }
``` ```
---
### 7. `POST /ingest/upload`
Sirve para subir un archivo directamente al backend desde una interfaz web o cliente compatible con `multipart/form-data`.
Es util para:
- PDFs o documentos sueltos
- pruebas rapidas desde el playground
- crear un scope aislado para una carga concreta
Campos esperados del formulario:
- `file`
- `sourceId` opcional
- `mode`
- `tags`
Si se usa `sourceId`, el archivo subido no se mezcla con otros scopes salvo que elijas reutilizar ese mismo identificador.
Respuesta esperada resumida: Respuesta esperada resumida:
```json ```json

View file

@ -70,6 +70,8 @@ El playground ya no funciona como una sola caja de consulta tecnica. Ahora se or
1. `Ingesta` 1. `Ingesta`
- lanzar ingesta documental o de codigo - lanzar ingesta documental o de codigo
- subir archivos directamente desde el navegador
- definir un `sourceId` propio para aislar una carga concreta
2. `Bootstrap` 2. `Bootstrap`
- elegir scope - elegir scope
@ -93,6 +95,15 @@ En la pestaña `Chat` hay un indicador visual:
Tambien se muestra el `scope` actualmente cargado. Tambien se muestra el `scope` actualmente cargado.
### Aislamiento de scopes en ingesta
En la pestaña `Ingesta` ya se puede:
- indicar un `sourceId` propio
- subir un archivo local directamente
Esto permite probar documentos o PDFs que no tengan nada que ver con el RAG sin mezclarlos con el resto de scopes si eliges un identificador especifico para esa carga.
### Chat con consultas adicionales al RAG ### Chat con consultas adicionales al RAG
El chat ya soporta dos niveles: El chat ya soporta dos niveles:

112
RAG/package-lock.json generated
View file

@ -11,11 +11,13 @@
"@qdrant/js-client-rest": "^1.15.0", "@qdrant/js-client-rest": "^1.15.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.2", "express": "^4.21.2",
"multer": "^2.0.0",
"openai": "^4.104.0", "openai": "^4.104.0",
"pdf-parse": "^1.1.1" "pdf-parse": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/multer": "^1.4.12",
"@types/node": "^22.15.3", "@types/node": "^22.15.3",
"@types/pdf-parse": "^1.1.4", "@types/pdf-parse": "^1.1.4",
"tsx": "^4.19.3", "tsx": "^4.19.3",
@ -544,6 +546,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/multer": {
"version": "1.4.13",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz",
"integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.19.17", "version": "22.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
@ -645,6 +657,12 @@
"node": ">= 8.0.0" "node": ">= 8.0.0"
} }
}, },
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/array-flatten": { "node_modules/array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@ -681,6 +699,23 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -731,6 +766,21 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -1329,6 +1379,25 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/multer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"type-is": "^1.6.18"
},
"engines": {
"node": ">= 10.16.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -1536,6 +1605,20 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/resolve-pkg-maps": { "node_modules/resolve-pkg-maps": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@ -1704,6 +1787,23 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -1752,6 +1852,12 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@ -1789,6 +1895,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View file

@ -13,11 +13,13 @@
"@qdrant/js-client-rest": "^1.15.0", "@qdrant/js-client-rest": "^1.15.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.2", "express": "^4.21.2",
"multer": "^2.0.0",
"openai": "^4.104.0", "openai": "^4.104.0",
"pdf-parse": "^1.1.1" "pdf-parse": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/multer": "^1.4.12",
"@types/node": "^22.15.3", "@types/node": "^22.15.3",
"@types/pdf-parse": "^1.1.4", "@types/pdf-parse": "^1.1.4",
"tsx": "^4.19.3", "tsx": "^4.19.3",

View file

@ -21,7 +21,9 @@ const contextStatusText = document.getElementById("contextStatusText");
const contextScopeText = document.getElementById("contextScopeText"); const contextScopeText = document.getElementById("contextScopeText");
const ingestSourceType = document.getElementById("ingestSourceType"); const ingestSourceType = document.getElementById("ingestSourceType");
const ingestSourceId = document.getElementById("ingestSourceId");
const ingestSourceRef = document.getElementById("ingestSourceRef"); const ingestSourceRef = document.getElementById("ingestSourceRef");
const ingestUploadFile = document.getElementById("ingestUploadFile");
const ingestMode = document.getElementById("ingestMode"); const ingestMode = document.getElementById("ingestMode");
const ingestTags = document.getElementById("ingestTags"); const ingestTags = document.getElementById("ingestTags");
@ -199,12 +201,35 @@ healthButton.addEventListener("click", async () => {
ingestButton.addEventListener("click", async () => { ingestButton.addEventListener("click", async () => {
ingestResult.textContent = "Ejecutando ingesta..."; ingestResult.textContent = "Ejecutando ingesta...";
try { try {
const data = await request("/ingest", { let data;
sourceType: ingestSourceType.value,
sourceRef: ingestSourceRef.value, if (ingestUploadFile.files && ingestUploadFile.files[0]) {
mode: ingestMode.value, const formData = new FormData();
tags: splitTags(ingestTags.value) formData.append("file", ingestUploadFile.files[0]);
}); formData.append("mode", ingestMode.value);
formData.append("tags", splitTags(ingestTags.value).join(","));
if (ingestSourceId.value.trim()) {
formData.append("sourceId", ingestSourceId.value.trim());
}
const response = await fetch("/ingest/upload", {
method: "POST",
body: formData
});
data = await response.json();
if (!response.ok) {
throw new Error(data.error || `HTTP ${response.status}`);
}
} else {
data = await request("/ingest", {
sourceId: ingestSourceId.value.trim() || undefined,
sourceType: ingestSourceType.value,
sourceRef: ingestSourceRef.value,
mode: ingestMode.value,
tags: splitTags(ingestTags.value)
});
}
ingestResult.textContent = format(data); ingestResult.textContent = format(data);
await loadScopes(); await loadScopes();
} catch (error) { } catch (error) {

View file

@ -27,6 +27,9 @@
<article class="panel"> <article class="panel">
<h2>Ingesta</h2> <h2>Ingesta</h2>
<div class="grid single-grid"> <div class="grid single-grid">
<label>Source ID / scope custom
<input id="ingestSourceId" placeholder="ej: src:cliente-a:manual:pdf-tecnico" />
</label>
<label>Tipo de fuente <label>Tipo de fuente
<select id="ingestSourceType"> <select id="ingestSourceType">
<option value="folder">folder</option> <option value="folder">folder</option>
@ -36,6 +39,9 @@
<label>Ruta de la fuente <label>Ruta de la fuente
<input id="ingestSourceRef" value="/home/pancho/Documentos/Empresa/Desarrollo/IA/docs" /> <input id="ingestSourceRef" value="/home/pancho/Documentos/Empresa/Desarrollo/IA/docs" />
</label> </label>
<label>Archivo local a subir
<input id="ingestUploadFile" type="file" accept=".pdf,.md,.txt,.ts,.tsx,.js,.jsx,.mjs,.cjs,.py,.json,.yml,.yaml" />
</label>
<label>Modo de ingesta <label>Modo de ingesta
<select id="ingestMode"> <select id="ingestMode">
<option value="mechanical">mechanical</option> <option value="mechanical">mechanical</option>
@ -46,6 +52,7 @@
<input id="ingestTags" value="workspace,global-docs" /> <input id="ingestTags" value="workspace,global-docs" />
</label> </label>
</div> </div>
<p class="helper">Si seleccionas un archivo local, el playground usara upload directo y podras aislarlo con un `sourceId` propio para no mezclarlo con otros scopes.</p>
<div class="actions"> <div class="actions">
<button id="ingestButton">Lanzar ingesta</button> <button id="ingestButton">Lanzar ingesta</button>
</div> </div>

View file

@ -79,6 +79,12 @@ body {
h1, h2, h3 { margin: 0 0 12px; } h1, h2, h3 { margin: 0 0 12px; }
.lead { margin: 0; color: var(--muted); max-width: 760px; } .lead { margin: 0; color: var(--muted); max-width: 760px; }
.helper {
color: var(--muted);
font-size: 13px;
margin: 0 0 16px;
}
.grid { .grid {
display: grid; display: grid;
gap: 24px; gap: 24px;

View file

@ -1,4 +1,8 @@
import express from "express"; import express from "express";
import multer from "multer";
import type { Request } from "express";
import { writeFile, unlink } from "node:fs/promises";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { env } from "./config/env.js"; import { env } from "./config/env.js";
@ -11,11 +15,16 @@ import { supportedParserExtensions } from "./modules/parsers/parser-registry.js"
import { QdrantVectorStoreClient } from "./modules/vectorstore/client.js"; import { QdrantVectorStoreClient } from "./modules/vectorstore/client.js";
import type { ChatMessage, ChunkMode, RetrieveIntent, RetrieveScope } from "./shared/types/rag.js"; import type { ChatMessage, ChunkMode, RetrieveIntent, RetrieveScope } from "./shared/types/rag.js";
type UploadRequest = Request & {
file?: Express.Multer.File;
};
export function createApp() { export function createApp() {
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const publicDir = path.resolve(__dirname, "../public"); const publicDir = path.resolve(__dirname, "../public");
const app = express(); const app = express();
const upload = multer({ storage: multer.memoryStorage() });
const embeddingProvider = new OpenRouterEmbeddingProvider(); const embeddingProvider = new OpenRouterEmbeddingProvider();
const vectorStore = new QdrantVectorStoreClient(); const vectorStore = new QdrantVectorStoreClient();
const ingestService = new IngestService(embeddingProvider, vectorStore); const ingestService = new IngestService(embeddingProvider, vectorStore);
@ -79,6 +88,45 @@ export function createApp() {
} }
}); });
app.post("/ingest/upload", upload.single("file"), async (req: UploadRequest, res) => {
let tempPath: string | undefined;
try {
if (!req.file) {
res.status(400).json({ ok: false, error: "Missing file upload" });
return;
}
const tempDir = await os.tmpdir();
tempPath = path.join(tempDir, `${Date.now()}-${req.file.originalname}`);
await writeFile(tempPath, req.file.buffer);
const tags = typeof req.body.tags === "string"
? req.body.tags.split(",").map((entry: string) => entry.trim()).filter(Boolean)
: [];
const result = await ingestService.ingest({
sourceId: req.body.sourceId ? String(req.body.sourceId) : undefined,
sourceType: "file",
sourceRef: req.file.originalname,
readPath: tempPath,
mode: req.body.mode === "interactive" ? "interactive" : "mechanical",
tags
});
res.status(202).json({
...result,
uploadedFile: req.file.originalname
});
} catch (error) {
res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown upload ingest error" });
} finally {
if (tempPath) {
await unlink(tempPath).catch(() => undefined);
}
}
});
app.post("/retrieve", async (req, res) => { app.post("/retrieve", async (req, res) => {
try { try {
const mode = (req.body.mode ?? "auto") as ChunkMode; const mode = (req.body.mode ?? "auto") as ChunkMode;

View file

@ -84,7 +84,7 @@ export class IngestService {
private async resolveInputFiles(source: IngestSourceInput): Promise<string[]> { private async resolveInputFiles(source: IngestSourceInput): Promise<string[]> {
if (source.sourceType === "file") { if (source.sourceType === "file") {
return [source.sourceRef]; return [source.readPath ?? source.sourceRef];
} }
return listFilesRecursively(source.sourceRef); return listFilesRecursively(source.sourceRef);
} }

View file

@ -10,6 +10,7 @@ export interface IngestSourceInput {
sourceId?: string; sourceId?: string;
sourceType: SourceType; sourceType: SourceType;
sourceRef: string; sourceRef: string;
readPath?: string;
mode?: IngestMode; mode?: IngestMode;
tags?: string[]; tags?: string[];
} }

View file

@ -65,6 +65,7 @@ Este archivo registra agentes y sesiones de trabajo de este workspace.
- Creacion de `RAG/docs/PLAYGROUND.md` para documentar la tecnologia elegida, su ubicacion y su papel dentro del modulo. - Creacion de `RAG/docs/PLAYGROUND.md` para documentar la tecnologia elegida, su ubicacion y su papel dentro del modulo.
- Ajuste de la API y del playground para hacer visible y seleccionable el modelo de `answer`, evitando dejarlo oculto como una decision fija del backend. - Ajuste de la API y del playground para hacer visible y seleccionable el modelo de `answer`, evitando dejarlo oculto como una decision fija del backend.
- Evolucion del playground a una mecanica mas completa con pestañas `Ingesta / Bootstrap / Chat`, indicador visual de contexto activo y endpoint `/chat` con bootstrap reutilizable y consultas adicionales al RAG durante la conversacion. - Evolucion del playground a una mecanica mas completa con pestañas `Ingesta / Bootstrap / Chat`, indicador visual de contexto activo y endpoint `/chat` con bootstrap reutilizable y consultas adicionales al RAG durante la conversacion.
- Ampliacion de la ingesta y del playground para soportar upload directo de archivos y `sourceId` personalizado, permitiendo aislar documentos ajenos al RAG en scopes separados.
- Reorganizacion de RAG como modulo raiz independiente con documentacion propia en `RAG/docs/`. - 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. - 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. - Creacion de `docs/TASK.md` para descomponer lineas de trabajo amplias en puntos de analisis y acuerdos.