From 93a5aee6cb7e374b55b4ae38b27314496ced155f Mon Sep 17 00:00:00 2001 From: Paco POR-CORREO Date: Mon, 6 Apr 2026 22:29:03 +0200 Subject: [PATCH] Implement cleanup endpoint and playground tab for scope deletion --- RAG/docs/API_RAG.md | 31 ++++++++++++-- RAG/docs/HISTORIAL_SESIONES.md | 4 ++ RAG/docs/PLAYGROUND.md | 11 +++-- RAG/docs/TASK_LIMPIEZA.md | 31 ++++++++++++++ RAG/public/playground/app.js | 58 +++++++++++++++++++++++++-- RAG/public/playground/index.html | 28 +++++++++++++ RAG/src/app.ts | 25 ++++++++++++ RAG/src/modules/ingest/service.ts | 7 +++- RAG/src/modules/vectorstore/client.ts | 26 ++++++++++++ 9 files changed, 211 insertions(+), 10 deletions(-) create mode 100644 RAG/docs/TASK_LIMPIEZA.md diff --git a/RAG/docs/API_RAG.md b/RAG/docs/API_RAG.md index b0e6cad..d6ec99e 100644 --- a/RAG/docs/API_RAG.md +++ b/RAG/docs/API_RAG.md @@ -409,7 +409,32 @@ Si se usa `sourceId`, el archivo subido no se mezcla con otros scopes salvo que --- -### 8. `GET /logs/recent` +### 8. `POST /cleanup` + +Sirve para eliminar vectores (chunks) ya ingeridos en el RAG segun un scope determinado, permitiendo limpiar contexto viejo antes de una reingesta. + +Payload base (requiere `sourceId` o `sourceRef` por seguridad): + +```json +{ + "scope": { + "sourceId": "src:default:folder:src_a9df8105" + } +} +``` + +Respuesta esperada: + +```json +{ + "ok": true, + "deleted": 124 +} +``` + +--- + +### 9. `GET /logs/recent` Devuelve los logs recientes de evaluacion guardados por el sistema. @@ -428,7 +453,7 @@ Tambien admite actualizacion posterior del estado de revision mediante `PATCH /l --- -### 9. `POST /logs/manual` +### 10. `POST /logs/manual` Permite registrar manualmente una consulta o respuesta que quieras revisar despues. @@ -456,7 +481,7 @@ Payload base: --- -### 10. `PATCH /logs/:id` +### 11. `PATCH /logs/:id` Permite actualizar el seguimiento de un log ya creado. diff --git a/RAG/docs/HISTORIAL_SESIONES.md b/RAG/docs/HISTORIAL_SESIONES.md index f306ff2..ec07000 100644 --- a/RAG/docs/HISTORIAL_SESIONES.md +++ b/RAG/docs/HISTORIAL_SESIONES.md @@ -43,3 +43,7 @@ Dar continuidad al RAG en `RAG/` a partir del estado actual documentado. - Implementacion de ayuda visual en la zona de `Bootstrap` del playground. - Añadidos tooltip y `aria-label` en `Cargar bootstrap`, `Reemplazar contexto`, `Vaciar contexto`, `Preset docs`, `Preset RAG docs` y `Preset codigo`. - Actualizacion de `RAG/docs/PLAYGROUND.md` y `RAG/docs/TEXTOS_AYUDA_PLAYGROUND.md` para reflejar la mejora. +- Implementacion de la pestaña Limpieza en el playground y soporte en el backend (`POST /cleanup`) para borrado seguro de contextos ya ingeridos. +- Limpieza ejecutada exitosamente sobre el `scope` del código fuente antiguo (`RAG/src`). +- Reingesta del directorio `RAG/src` con el código actualizado. +- Documento de seguimiento `RAG/docs/TASK_LIMPIEZA.md` y documentacion API `RAG/docs/API_RAG.md` actualizados. diff --git a/RAG/docs/PLAYGROUND.md b/RAG/docs/PLAYGROUND.md index 2df35aa..b9aaf9c 100644 --- a/RAG/docs/PLAYGROUND.md +++ b/RAG/docs/PLAYGROUND.md @@ -69,14 +69,19 @@ Tambien permite: ## Mecanica actual del playground -El playground ya no funciona como una sola caja de consulta tecnica. Ahora se organiza en tres pestañas: +El playground ya no funciona como una sola caja de consulta tecnica. Ahora se organiza en cuatro pestañas: 1. `Ingesta` - lanzar ingesta documental o de codigo - subir archivos directamente desde el navegador - definir un `sourceId` propio para aislar una carga concreta -2. `Bootstrap` +2. `Limpieza` +- eliminar todo el contexto asociado a un scope especifico +- util para poder reingestar una fuente actualizada sin duplicar chunks +- dialogos nativos de confirmacion para evitar borrados accidentales + +3. `Bootstrap` - elegir scope - elegir modo - cargar un mapa inicial del dominio @@ -84,7 +89,7 @@ El playground ya no funciona como una sola caja de consulta tecnica. Ahora se or - reemplazar o vaciar el contexto de sesion - mostrar ayuda visible y tooltip en los botones principales para reducir confusiones de uso -3. `Chat` +4. `Chat` - conversar con el modelo - ver visualmente si hay contexto cargado o no - reutilizar el ultimo bootstrap como contexto base diff --git a/RAG/docs/TASK_LIMPIEZA.md b/RAG/docs/TASK_LIMPIEZA.md new file mode 100644 index 0000000..c3e4a18 --- /dev/null +++ b/RAG/docs/TASK_LIMPIEZA.md @@ -0,0 +1,31 @@ +# Task: Limpieza y reingesta controlada del RAG + +**Proyecto:** Workspace de tools IA para empresas +**Modulo:** RAG +**Ultima actualizacion:** 2026-04-06 +**Estado:** En implementacion + +--- + +## Proposito +Permitir borrar contenido ya ingerido del RAG de forma controlada antes de reingestar una fuente actualizada, evitando que convivan chunks viejos y nuevos del mismo `scope`. + +## Alcance de la implementacion + +### 1. Backend (API y Qdrant) +- **VectorStoreClient:** Añadir metodo `delete(scope: RetrieveScope): Promise` (devuelve cantidad de puntos borrados). +- **Endpoint:** Exponer `POST /cleanup` que reciba el `scope` (ej. `sourceId` o `sourceRef`) y orqueste el borrado. + +### 2. Frontend (Playground) +- **UI:** Nueva pestaña "Limpieza". +- **Comportamiento:** + - Selector de scopes disponibles (se reutiliza el endpoint `/sources`). + - Resumen visual de lo que se va a borrar (`sourceId`, `sourceRef`). + - Boton de borrado. + - Dialogo de confirmacion nativo (`window.confirm`). + - Log del resultado de la peticion. + +### 3. Validacion +- Borrar el codigo de `RAG/src` previamente ingerido. +- Reingestar `RAG/src`. +- Confirmar que se mantiene la integridad sin duplicar fragmentos. diff --git a/RAG/public/playground/app.js b/RAG/public/playground/app.js index a6636ba..62abed0 100644 --- a/RAG/public/playground/app.js +++ b/RAG/public/playground/app.js @@ -1,5 +1,6 @@ const healthButton = document.getElementById("healthButton"); const ingestButton = document.getElementById("ingestButton"); +const cleanupButton = document.getElementById("cleanupButton"); const bootstrapButton = document.getElementById("bootstrapButton"); const replaceBootstrapButton = document.getElementById("replaceBootstrapButton"); const clearBootstrapButton = document.getElementById("clearBootstrapButton"); @@ -12,6 +13,7 @@ const presetCode = document.getElementById("presetCode"); const healthResult = document.getElementById("healthResult"); const ingestResult = document.getElementById("ingestResult"); +const cleanupResult = document.getElementById("cleanupResult"); const bootstrapResult = document.getElementById("bootstrapResult"); const mainResult = document.getElementById("mainResult"); const compareResult = document.getElementById("compareResult"); @@ -33,6 +35,11 @@ const ingestMode = document.getElementById("ingestMode"); const ingestTags = document.getElementById("ingestTags"); const ingestModeHint = document.getElementById("ingestModeHint"); +const cleanupScopeSelect = document.getElementById("cleanupScopeSelect"); +const cleanupSourceId = document.getElementById("cleanupSourceId"); +const cleanupSourceRef = document.getElementById("cleanupSourceRef"); +const cleanupTags = document.getElementById("cleanupTags"); + const bootstrapQuery = document.getElementById("bootstrapQuery"); const bootstrapMode = document.getElementById("bootstrapMode"); const answerModel = document.getElementById("answerModel"); @@ -100,6 +107,12 @@ function applySelectedScope(scope) { } } +function applySelectedCleanupScope(scope) { + cleanupSourceId.value = scope?.sourceId || ""; + cleanupSourceRef.value = scope?.sourceRef || ""; + cleanupTags.value = (scope?.tags || []).join(", "); +} + function updateScopeEditState() { const locked = scopeEditMode.value !== "manual"; scopeSourceRef.readOnly = locked; @@ -185,24 +198,31 @@ async function loadScopes() { const scopes = await fetch("/sources").then((response) => response.json()); availableScopes = scopes; scopePresetSelect.innerHTML = ""; + cleanupScopeSelect.innerHTML = ""; const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = "Selecciona un scope disponible"; - scopePresetSelect.appendChild(placeholder); + scopePresetSelect.appendChild(placeholder.cloneNode(true)); + cleanupScopeSelect.appendChild(placeholder.cloneNode(true)); for (const scope of scopes) { const option = document.createElement("option"); option.value = JSON.stringify(scope); option.textContent = buildScopeLabel(scope); - scopePresetSelect.appendChild(option); + scopePresetSelect.appendChild(option.cloneNode(true)); + cleanupScopeSelect.appendChild(option.cloneNode(true)); } if (scopes.length === 0) { - placeholder.textContent = "No hay scopes detectados"; + const emptyMsg = "No hay scopes detectados"; + scopePresetSelect.options[0].textContent = emptyMsg; + cleanupScopeSelect.options[0].textContent = emptyMsg; } else if (!scopePresetSelect.value) { scopePresetSelect.value = JSON.stringify(scopes[0]); + cleanupScopeSelect.value = JSON.stringify(scopes[0]); applySelectedScope(scopes[0]); + applySelectedCleanupScope(scopes[0]); } } catch (error) { scopePresetSelect.innerHTML = ``; @@ -256,6 +276,15 @@ scopePresetSelect.addEventListener("change", () => { applySelectedScope(scope); }); +cleanupScopeSelect.addEventListener("change", () => { + if (!cleanupScopeSelect.value) { + applySelectedCleanupScope(null); + return; + } + const scope = JSON.parse(cleanupScopeSelect.value); + applySelectedCleanupScope(scope); +}); + ingestUploadFile.addEventListener("change", updateIngestUiState); ingestScopeMode.addEventListener("change", updateIngestUiState); scopeEditMode.addEventListener("change", updateScopeEditState); @@ -311,6 +340,29 @@ ingestButton.addEventListener("click", async () => { } }); +cleanupButton.addEventListener("click", async () => { + if (!cleanupScopeSelect.value) { + cleanupResult.textContent = "Error: Debes seleccionar un scope primero."; + return; + } + + const scope = JSON.parse(cleanupScopeSelect.value); + const identifier = scope.sourceId || scope.sourceRef || "scope desconocido"; + + if (!window.confirm(`¿Seguro que quieres eliminar TODO el contexto de:\n\n${identifier}\n\nEsto borrara todos los vectores de Qdrant asociados y no se puede deshacer.`)) { + return; + } + + cleanupResult.textContent = "Eliminando contexto..."; + try { + const data = await request("/cleanup", { scope }); + cleanupResult.textContent = format(data); + await loadScopes(); + } catch (error) { + cleanupResult.textContent = String(error); + } +}); + async function executeBootstrap() { bootstrapResult.textContent = "Cargando bootstrap..."; try { diff --git a/RAG/public/playground/index.html b/RAG/public/playground/index.html index a114629..f51fa8a 100644 --- a/RAG/public/playground/index.html +++ b/RAG/public/playground/index.html @@ -28,6 +28,7 @@
+
@@ -75,6 +76,33 @@ +
+
+

Limpieza controlada

+

Permite eliminar contexto ya ingerido del RAG antes de reingestar una fuente, para evitar duplicados y versiones mezcladas.

+
+ + + + +
+
+ +
+
Selecciona un scope y pulsa el boton para eliminarlo.
+
+
+

Bootstrap de sesion

diff --git a/RAG/src/app.ts b/RAG/src/app.ts index 4c277fc..982c4e8 100644 --- a/RAG/src/app.ts +++ b/RAG/src/app.ts @@ -124,6 +124,31 @@ export function createApp() { } }); + app.post("/cleanup", async (req, res) => { + try { + if (!req.body.scope) { + res.status(400).json({ ok: false, error: "Missing scope for cleanup" }); + return; + } + + const scope: RetrieveScope = { + sourceId: req.body.scope.sourceId ? String(req.body.scope.sourceId) : undefined, + sourceRef: req.body.scope.sourceRef ? String(req.body.scope.sourceRef) : undefined, + tags: Array.isArray(req.body.scope.tags) ? req.body.scope.tags.map(String) : undefined + }; + + if (!scope.sourceId && !scope.sourceRef) { + res.status(400).json({ ok: false, error: "Cleanup requires at least sourceId or sourceRef to prevent accidental mass deletion" }); + return; + } + + const result = await ingestService.cleanup(scope); + res.status(200).json({ ok: true, ...result }); + } catch (error) { + res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown cleanup error" }); + } + }); + app.post("/ingest/upload", upload.single("file"), async (req: UploadRequest, res) => { let tempPath: string | undefined; diff --git a/RAG/src/modules/ingest/service.ts b/RAG/src/modules/ingest/service.ts index c0732f9..12a3820 100644 --- a/RAG/src/modules/ingest/service.ts +++ b/RAG/src/modules/ingest/service.ts @@ -3,7 +3,7 @@ import { parseDocument, isSupportedDocument } from "../parsers/parser-registry.j 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 type { IngestResult, IngestSourceInput, IngestedChunk, RetrieveScope } 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"; @@ -14,6 +14,11 @@ export class IngestService { private readonly vectorStore: VectorStoreClient ) {} + async cleanup(scope: RetrieveScope): Promise<{ deleted: number }> { + const count = await this.vectorStore.deleteScope(scope); + return { deleted: count }; + } + async ingest(source: IngestSourceInput): Promise { const sourceId = source.sourceId ?? buildSourceId(source.sourceType, source.sourceRef); const discoveredFiles = await this.resolveInputFiles(source); diff --git a/RAG/src/modules/vectorstore/client.ts b/RAG/src/modules/vectorstore/client.ts index 25d0bec..294af16 100644 --- a/RAG/src/modules/vectorstore/client.ts +++ b/RAG/src/modules/vectorstore/client.ts @@ -21,6 +21,7 @@ export interface VectorStoreClient { search(queryVector: number[], limit: number, mode?: string, scope?: RetrieveScope): Promise; listScopes(): Promise; browseScope(limit: number, mode?: string, scope?: RetrieveScope): Promise; + deleteScope(scope: RetrieveScope): Promise; } function buildSearchFilter(mode?: string, scope?: RetrieveScope) { @@ -218,4 +219,29 @@ export class QdrantVectorStoreClient implements VectorStoreClient { endLine: point.payload?.end_line ? Number(point.payload.end_line) : undefined })); } + + async deleteScope(scope: RetrieveScope): Promise { + const collections = await this.client.getCollections(); + const exists = collections.collections.some((collection) => collection.name === env.qdrantCollection); + + if (!exists || !scope || (!scope.sourceId && !scope.sourceRef)) { + return 0; // Se requiere al menos un identificador fuerte para no borrar a lo loco + } + + const filter = buildSearchFilter(undefined, scope); + if (!filter) { + return 0; + } + + // Contamos antes de borrar para devolver la cantidad + const countResult = await this.client.count(env.qdrantCollection, { + filter + }); + + await this.client.delete(env.qdrantCollection, { + filter + }); + + return countResult.count; + } }