Implement cleanup endpoint and playground tab for scope deletion
This commit is contained in:
parent
6cdf82e80e
commit
93a5aee6cb
9 changed files with 211 additions and 10 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
31
RAG/docs/TASK_LIMPIEZA.md
Normal file
31
RAG/docs/TASK_LIMPIEZA.md
Normal file
|
|
@ -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<number>` (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.
|
||||
|
|
@ -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 = `<option value="">Error cargando scopes: ${error}</option>`;
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
<section class="tabs">
|
||||
<button class="tab-button active" data-tab="ingest">Ingesta</button>
|
||||
<button class="tab-button" data-tab="cleanup">Limpieza</button>
|
||||
<button class="tab-button" data-tab="bootstrap">Bootstrap</button>
|
||||
<button class="tab-button" data-tab="chat">Chat</button>
|
||||
</section>
|
||||
|
|
@ -75,6 +76,33 @@
|
|||
</article>
|
||||
</section>
|
||||
|
||||
<section id="tab-cleanup" class="tab-panel">
|
||||
<article class="panel">
|
||||
<h2>Limpieza controlada</h2>
|
||||
<p class="helper">Permite eliminar contexto ya ingerido del RAG antes de reingestar una fuente, para evitar duplicados y versiones mezcladas.</p>
|
||||
<div class="grid single-grid">
|
||||
<label>Scope disponible a limpiar
|
||||
<select id="cleanupScopeSelect">
|
||||
<option value="">Cargando scopes...</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Source ID (obligatorio si no hay Source Ref)
|
||||
<input id="cleanupSourceId" readonly />
|
||||
</label>
|
||||
<label>Source Ref (obligatorio si no hay Source ID)
|
||||
<input id="cleanupSourceRef" readonly />
|
||||
</label>
|
||||
<label>Tags asociados (informativo)
|
||||
<input id="cleanupTags" readonly />
|
||||
</label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="cleanupButton" style="background: var(--danger); color: white;" title="Borrar de Qdrant los vectores del scope seleccionado" aria-label="Eliminar scope de Qdrant">Eliminar contexto seleccionado</button>
|
||||
</div>
|
||||
<pre id="cleanupResult">Selecciona un scope y pulsa el boton para eliminarlo.</pre>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section id="tab-bootstrap" class="tab-panel">
|
||||
<article class="panel">
|
||||
<h2>Bootstrap de sesion</h2>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IngestResult> {
|
||||
const sourceId = source.sourceId ?? buildSourceId(source.sourceType, source.sourceRef);
|
||||
const discoveredFiles = await this.resolveInputFiles(source);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export interface VectorStoreClient {
|
|||
search(queryVector: number[], limit: number, mode?: string, scope?: RetrieveScope): Promise<RetrievedItem[]>;
|
||||
listScopes(): Promise<AvailableScope[]>;
|
||||
browseScope(limit: number, mode?: string, scope?: RetrieveScope): Promise<RetrievedItem[]>;
|
||||
deleteScope(scope: RetrieveScope): Promise<number>;
|
||||
}
|
||||
|
||||
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<number> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue