diff --git a/RAG/docs/API_RAG.md b/RAG/docs/API_RAG.md index 3a8af5a..d572975 100644 --- a/RAG/docs/API_RAG.md +++ b/RAG/docs/API_RAG.md @@ -337,6 +337,51 @@ Payload base: } ``` +--- + +### 6. `POST /chat` + +Sirve para conversar con un modelo usando: + +- contexto bootstrap precargado +- historial reciente de mensajes +- y opcionalmente una consulta adicional al RAG durante la propia conversacion + +Payload base: + +```json +{ + "message": "y ahora cuales son los siguientes pasos mas naturales?", + "history": [ + { + "role": "user", + "content": "dame un mapa inicial del workspace" + } + ], + "mode": "documental", + "model": "openai/gpt-4.1-mini", + "preloadedContext": "", + "allowAdditionalRetrieve": true, + "scope": { + "sourceRef": "/home/pancho/Documentos/Empresa/Desarrollo/IA/docs" + } +} +``` + +Respuesta esperada resumida: + +```json +{ + "model": "openai/gpt-4.1-mini", + "answer": "...", + "usedBootstrapContext": true, + "usedAdditionalRetrieve": true, + "retrieved": { + "summary": "..." + } +} +``` + Respuesta esperada: ```json diff --git a/RAG/docs/PLAYGROUND.md b/RAG/docs/PLAYGROUND.md index e3a8e72..51790aa 100644 --- a/RAG/docs/PLAYGROUND.md +++ b/RAG/docs/PLAYGROUND.md @@ -47,10 +47,12 @@ El backend lo sirve desde: 1. `health` 2. `ingest` -3. `retrieve` -4. `answer` -5. `answer` sin RAG para comparar impacto del contexto -6. seleccion explicita del modelo de `answer` +3. `bootstrap` +4. `chat` con contexto precargado +5. `retrieve` +6. `answer` +7. `answer` sin RAG para comparar impacto del contexto +8. seleccion explicita del modelo de `answer` Tambien permite: @@ -62,6 +64,46 @@ 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: + +1. `Ingesta` +- lanzar ingesta documental o de codigo + +2. `Bootstrap` +- elegir scope +- elegir modo +- cargar un mapa inicial del dominio +- opcionalmente pedir a un modelo que sintetice ese bootstrap +- reemplazar o vaciar el contexto de sesion + +3. `Chat` +- conversar con el modelo +- ver visualmente si hay contexto cargado o no +- reutilizar el ultimo bootstrap como contexto base +- permitir que el modelo haga consultas adicionales al RAG durante la conversacion + +### Indicador visual de contexto + +En la pestaña `Chat` hay un indicador visual: + +- rojo: no hay bootstrap cargado +- verde: hay contexto bootstrap activo + +Tambien se muestra el `scope` actualmente cargado. + +### Chat con consultas adicionales al RAG + +El chat ya soporta dos niveles: + +1. respuesta usando solo el bootstrap cargado +2. respuesta usando bootstrap y, si se activa la opcion correspondiente, una consulta adicional al RAG durante la conversacion + +Esto permite aproximar mejor el comportamiento esperado de una app o agente conectado al servicio. + +--- + ## Idea de uso Este playground no sustituye a clientes finales ni al futuro MCP. diff --git a/RAG/public/playground/app.js b/RAG/public/playground/app.js index 21c3fde..21207de 100644 --- a/RAG/public/playground/app.js +++ b/RAG/public/playground/app.js @@ -1,30 +1,46 @@ const healthButton = document.getElementById("healthButton"); const ingestButton = document.getElementById("ingestButton"); -const queryButton = document.getElementById("queryButton"); +const bootstrapButton = document.getElementById("bootstrapButton"); +const replaceBootstrapButton = document.getElementById("replaceBootstrapButton"); +const clearBootstrapButton = document.getElementById("clearBootstrapButton"); +const sendChatButton = document.getElementById("sendChatButton"); +const clearChatButton = document.getElementById("clearChatButton"); const presetDocs = document.getElementById("presetDocs"); const presetRagDocs = document.getElementById("presetRagDocs"); const presetCode = document.getElementById("presetCode"); const healthResult = document.getElementById("healthResult"); +const ingestResult = document.getElementById("ingestResult"); +const bootstrapResult = document.getElementById("bootstrapResult"); const mainResult = document.getElementById("mainResult"); const compareResult = document.getElementById("compareResult"); - -const queryInput = document.getElementById("queryInput"); -const queryMode = document.getElementById("queryMode"); -const queryIntent = document.getElementById("queryIntent"); -const queryOperation = document.getElementById("queryOperation"); -const answerModel = document.getElementById("answerModel"); -const useModelInRetrieve = document.getElementById("useModelInRetrieve"); -const scopePresetSelect = document.getElementById("scopePresetSelect"); -const scopeSourceRef = document.getElementById("scopeSourceRef"); -const scopeTags = document.getElementById("scopeTags"); -const compareWithoutRag = document.getElementById("compareWithoutRag"); +const bootstrapContextResult = document.getElementById("bootstrapContextResult"); +const chatMessages = document.getElementById("chatMessages"); +const contextIndicator = document.getElementById("contextIndicator"); +const contextStatusText = document.getElementById("contextStatusText"); +const contextScopeText = document.getElementById("contextScopeText"); const ingestSourceType = document.getElementById("ingestSourceType"); const ingestSourceRef = document.getElementById("ingestSourceRef"); const ingestMode = document.getElementById("ingestMode"); const ingestTags = document.getElementById("ingestTags"); +const bootstrapQuery = document.getElementById("bootstrapQuery"); +const bootstrapMode = document.getElementById("bootstrapMode"); +const answerModel = document.getElementById("answerModel"); +const useModelInRetrieve = document.getElementById("useModelInRetrieve"); +const reuseBootstrapContext = document.getElementById("reuseBootstrapContext"); +const allowAdditionalRetrieve = document.getElementById("allowAdditionalRetrieve"); +const scopePresetSelect = document.getElementById("scopePresetSelect"); +const scopeSourceRef = document.getElementById("scopeSourceRef"); +const scopeTags = document.getElementById("scopeTags"); +const compareWithoutRag = document.getElementById("compareWithoutRag"); +const chatMode = document.getElementById("chatMode"); +const chatInput = document.getElementById("chatInput"); + +let lastBootstrapContext = null; +let chatHistory = []; + function format(value) { return JSON.stringify(value, null, 2); } @@ -38,21 +54,66 @@ function buildScopeLabel(scope) { return `${scope.sourceRef} [${modes}]`; } -async function request(url, payload, method = "POST") { - const response = await fetch(url, { +function request(url, payload, method = "POST") { + return fetch(url, { method, headers: { "Content-Type": "application/json" }, body: payload ? JSON.stringify(payload) : undefined + }).then(async (response) => { + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || `HTTP ${response.status}`); + } + return data; }); +} - const data = await response.json(); - if (!response.ok) { - throw new Error(data.error || `HTTP ${response.status}`); +function renderBootstrapContext() { + if (!lastBootstrapContext) { + bootstrapContextResult.textContent = "Aun no hay bootstrap cargado."; + contextIndicator.className = "indicator indicator-off"; + contextStatusText.textContent = "Sin contexto cargado"; + contextScopeText.textContent = "No hay bootstrap activo."; + return; } - return data; + bootstrapContextResult.textContent = format(lastBootstrapContext); + contextIndicator.className = "indicator indicator-on"; + contextStatusText.textContent = "Contexto bootstrap activo"; + contextScopeText.textContent = lastBootstrapContext.scope?.sourceRef || "Scope no especificado"; +} + +function renderChatHistory() { + if (chatHistory.length === 0) { + chatMessages.innerHTML = '

Aun no hay conversacion. Carga un bootstrap y empieza a preguntar.

'; + return; + } + + chatMessages.innerHTML = chatHistory.map((entry) => ` +
+
${entry.role === "user" ? "Usuario" : "Modelo"}
+

${entry.content.replace(/ +

+ `).join(""); + + chatMessages.scrollTop = chatMessages.scrollHeight; +} + +function buildScopeFromInputs() { + return { + sourceRef: scopeSourceRef.value, + tags: splitTags(scopeTags.value) + }; +} + +function applyPreset(mode, query, sourceRef, useRetrieveModel = false) { + bootstrapMode.value = mode; + chatMode.value = mode; + bootstrapQuery.value = query; + scopeSourceRef.value = sourceRef; + useModelInRetrieve.checked = useRetrieveModel; } async function loadScopes() { @@ -76,11 +137,7 @@ async function loadScopes() { placeholder.textContent = "No hay scopes detectados"; } } catch (error) { - scopePresetSelect.innerHTML = ""; - const option = document.createElement("option"); - option.value = ""; - option.textContent = `Error cargando scopes: ${error}`; - scopePresetSelect.appendChild(option); + scopePresetSelect.innerHTML = ``; } } @@ -88,26 +145,29 @@ async function loadAnswerModels() { try { const payload = await fetch("/models/answer").then((response) => response.json()); answerModel.innerHTML = ""; - for (const model of payload.models || []) { const option = document.createElement("option"); option.value = model; option.textContent = model; answerModel.appendChild(option); } - if (payload.defaultModel) { answerModel.value = payload.defaultModel; } } catch { - answerModel.innerHTML = ""; - const option = document.createElement("option"); - option.value = "openai/gpt-4.1-mini"; - option.textContent = "openai/gpt-4.1-mini"; - answerModel.appendChild(option); + answerModel.innerHTML = ''; } } +document.querySelectorAll(".tab-button").forEach((button) => { + button.addEventListener("click", () => { + document.querySelectorAll(".tab-button").forEach((entry) => entry.classList.remove("active")); + document.querySelectorAll(".tab-panel").forEach((panel) => panel.classList.remove("active")); + button.classList.add("active"); + document.getElementById(`tab-${button.dataset.tab}`).classList.add("active"); + }); +}); + scopePresetSelect.addEventListener("change", () => { if (!scopePresetSelect.value) { return; @@ -118,17 +178,18 @@ scopePresetSelect.addEventListener("change", () => { scopeTags.value = (scope.tags || []).join(", "); if (scope.chunkModes.includes("codigo")) { - queryMode.value = "codigo"; + bootstrapMode.value = "codigo"; + chatMode.value = "codigo"; } else if (scope.chunkModes.includes("documental")) { - queryMode.value = "documental"; + bootstrapMode.value = "documental"; + chatMode.value = "documental"; } }); healthButton.addEventListener("click", async () => { healthResult.textContent = "Comprobando..."; try { - const response = await fetch("/health"); - const data = await response.json(); + const data = await fetch("/health").then((response) => response.json()); healthResult.textContent = format(data); } catch (error) { healthResult.textContent = String(error); @@ -136,7 +197,7 @@ healthButton.addEventListener("click", async () => { }); ingestButton.addEventListener("click", async () => { - mainResult.textContent = "Ejecutando ingesta..."; + ingestResult.textContent = "Ejecutando ingesta..."; try { const data = await request("/ingest", { sourceType: ingestSourceType.value, @@ -144,72 +205,122 @@ ingestButton.addEventListener("click", async () => { mode: ingestMode.value, tags: splitTags(ingestTags.value) }); - mainResult.textContent = format(data); + ingestResult.textContent = format(data); + await loadScopes(); } catch (error) { - mainResult.textContent = String(error); + ingestResult.textContent = String(error); } }); -queryButton.addEventListener("click", async () => { +async function executeBootstrap() { + bootstrapResult.textContent = "Cargando bootstrap..."; + try { + const data = await request("/retrieve", { + mode: bootstrapMode.value, + intent: "bootstrap", + query: bootstrapQuery.value, + model: answerModel.value, + useModelInRetrieve: useModelInRetrieve.checked, + scope: buildScopeFromInputs() + }); + + lastBootstrapContext = data; + bootstrapResult.textContent = format(data); + renderBootstrapContext(); + } catch (error) { + bootstrapResult.textContent = String(error); + } +} + +bootstrapButton.addEventListener("click", executeBootstrap); +replaceBootstrapButton.addEventListener("click", executeBootstrap); + +clearBootstrapButton.addEventListener("click", () => { + lastBootstrapContext = null; + renderBootstrapContext(); +}); + +sendChatButton.addEventListener("click", async () => { + const message = chatInput.value.trim(); + if (!message) { + return; + } + + chatHistory.push({ role: "user", content: message }); + renderChatHistory(); mainResult.textContent = "Consultando..."; compareResult.textContent = compareWithoutRag.checked ? "Comparando..." : "Desactivada."; - const payload = { - mode: queryMode.value, - intent: queryIntent.value, - query: queryInput.value, - model: answerModel.value, - useModelInRetrieve: useModelInRetrieve.checked, - scope: { - sourceRef: scopeSourceRef.value, - tags: splitTags(scopeTags.value) - } - }; - try { - const endpoint = queryOperation.value === "answer" ? "/answer" : "/retrieve"; - const data = await request(endpoint, payload); - mainResult.textContent = format(data); + const response = await request("/chat", { + message, + history: chatHistory, + mode: chatMode.value, + model: answerModel.value, + preloadedContext: reuseBootstrapContext.checked && lastBootstrapContext + ? (lastBootstrapContext.modelSummary || lastBootstrapContext.summary || "") + : undefined, + allowAdditionalRetrieve: allowAdditionalRetrieve.checked, + scope: buildScopeFromInputs() + }); - if (compareWithoutRag.checked && queryOperation.value === "answer") { - const comparison = await request("/answer/direct", { - query: queryInput.value, - model: answerModel.value - }); - compareResult.textContent = format(comparison); - } + chatHistory.push({ role: "assistant", content: response.answer }); + renderChatHistory(); + mainResult.textContent = format(response); + + if (compareWithoutRag.checked) { + const comparison = await request("/answer/direct", { + query: message, + model: answerModel.value, + preloadedContext: reuseBootstrapContext.checked && lastBootstrapContext + ? (lastBootstrapContext.modelSummary || lastBootstrapContext.summary || "") + : undefined + }); + compareResult.textContent = format(comparison); + } + + chatInput.value = ""; } catch (error) { mainResult.textContent = String(error); compareResult.textContent = compareWithoutRag.checked ? String(error) : "Desactivada."; } }); +clearChatButton.addEventListener("click", () => { + chatHistory = []; + renderChatHistory(); + mainResult.textContent = "Sin ejecutar aun."; + compareResult.textContent = "Desactivada."; +}); + presetDocs.addEventListener("click", () => { - queryMode.value = "documental"; - queryIntent.value = "specific"; - queryOperation.value = "answer"; - queryInput.value = "que tenemos pendiente por hacer en este workspace"; - scopeSourceRef.value = "/home/pancho/Documentos/Empresa/Desarrollo/IA/docs"; - answerModel.value = "openai/gpt-4.1-mini"; + applyPreset( + "documental", + "dame un mapa inicial del workspace, sus lineas de trabajo principales, reglas, documentacion base y puntos importantes a tener presentes", + "/home/pancho/Documentos/Empresa/Desarrollo/IA/docs", + true + ); }); presetRagDocs.addEventListener("click", () => { - queryMode.value = "documental"; - queryIntent.value = "bootstrap"; - queryOperation.value = "retrieve"; - queryInput.value = "dame un mapa inicial del modulo RAG, su arquitectura, decisiones, estado actual y documentos clave"; - scopeSourceRef.value = "/home/pancho/Documentos/Empresa/Desarrollo/IA/RAG/docs"; - answerModel.value = "openai/gpt-4.1-mini"; + applyPreset( + "documental", + "dame un mapa inicial del modulo RAG, su arquitectura, decisiones, estado actual y documentos clave", + "/home/pancho/Documentos/Empresa/Desarrollo/IA/RAG/docs", + true + ); }); presetCode.addEventListener("click", () => { - queryMode.value = "codigo"; - queryIntent.value = "specific"; - queryOperation.value = "answer"; - queryInput.value = "como se construye source_id en el rag"; - scopeSourceRef.value = "/home/pancho/Documentos/Empresa/Desarrollo/IA/RAG/src"; - answerModel.value = "openai/gpt-4.1-mini"; + applyPreset( + "codigo", + "dame un mapa inicial del codigo del modulo RAG, sus modulos principales, flujo interno y piezas clave", + "/home/pancho/Documentos/Empresa/Desarrollo/IA/RAG/src", + true + ); }); loadScopes(); loadAnswerModels(); +renderBootstrapContext(); +renderChatHistory(); diff --git a/RAG/public/playground/index.html b/RAG/public/playground/index.html index 8275ec4..e20c5fd 100644 --- a/RAG/public/playground/index.html +++ b/RAG/public/playground/index.html @@ -11,70 +11,52 @@

RAG Playground

-

Pruebas rapidas del RAG

-

Interfaz interna para probar ingesta, retrieve, answer y comparacion sin RAG desde el mismo servicio.

+

Laboratorio del RAG

+

Prueba ingesta, bootstrap y chat con contexto visible para evaluar el RAG como si estuviera integrado en una app o agente real.

-
+
+ + + +
+ +

Ingesta

- - - - - -
- -
-

Consulta

- - -
- - + - -
+
+ +
+
Sin ejecutar aun.
+
+
-
+
+
+

Bootstrap de sesion

+
+ + +
- - - -
- + + +
+
Sin ejecutar aun.
-
-
-

Resultado principal

-
Sin ejecutar aun.
-
-
-

Comparacion sin RAG

-
Desactivada.
-
+
+
+
+

Estado del contexto

+
+ +
+ Sin contexto cargado +

No hay bootstrap activo.

+
+
+ + + + + + + +

Contexto bootstrap cargado

+
Aun no hay bootstrap cargado.
+
+ +
+

Chat con el modelo

+
+

Aun no hay conversacion. Carga un bootstrap y empieza a preguntar.

+
+ + + +
+ + +
+ +

Ultima respuesta estructurada

+
Sin ejecutar aun.
+ +

Comparacion sin RAG

+ +
Desactivada.
+
+
@@ -126,4 +170,4 @@ - + diff --git a/RAG/public/playground/styles.css b/RAG/public/playground/styles.css index 9e4048d..c8fb3d1 100644 --- a/RAG/public/playground/styles.css +++ b/RAG/public/playground/styles.css @@ -8,6 +8,7 @@ --accent: #72e0a8; --accent-2: #7ca8ff; --border: #2e3a5f; + --danger: #ff6b6b; } * { box-sizing: border-box; } @@ -20,12 +21,12 @@ body { } .layout { - max-width: 1280px; + max-width: 1320px; margin: 0 auto; padding: 32px; } -.hero, .panel { +.hero, .panel, .tabs { background: rgba(21, 29, 49, 0.92); border: 1px solid var(--border); border-radius: 20px; @@ -38,9 +39,35 @@ body { justify-content: space-between; gap: 20px; align-items: flex-start; + margin-bottom: 18px; +} + +.tabs { + display: inline-flex; + gap: 10px; + padding: 10px; margin-bottom: 24px; } +.tab-button { + width: auto; + padding: 12px 18px; + border-radius: 14px; + border: 1px solid var(--border); + background: transparent; + color: var(--text); + cursor: pointer; +} + +.tab-button.active { + background: linear-gradient(135deg, var(--accent) 0%, #4fceff 100%); + color: #0b1020; + border: none; +} + +.tab-panel { display: none; margin-bottom: 24px; } +.tab-panel.active { display: block; } + .eyebrow { margin: 0 0 8px; color: var(--accent); @@ -49,22 +76,20 @@ body { letter-spacing: 0.14em; } -h1, h2 { margin: 0 0 12px; } -.lead { margin: 0; color: var(--muted); max-width: 720px; } +h1, h2, h3 { margin: 0 0 12px; } +.lead { margin: 0; color: var(--muted); max-width: 760px; } .grid { display: grid; gap: 24px; - grid-template-columns: 1fr 2fr; + grid-template-columns: 1fr 1fr; margin-bottom: 24px; } -.results-grid { grid-template-columns: 1fr 1fr; } -.panel-wide { min-width: 0; } +.single-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.chat-grid { grid-template-columns: 0.9fr 1.3fr; } -.panel { - padding: 24px; -} +.panel { padding: 24px; } label { display: block; @@ -100,25 +125,14 @@ button.secondary { border: 1px solid var(--border); } -.row, .actions { +.actions, .checkbox { display: flex; gap: 12px; flex-wrap: wrap; -} - -.row > label { - flex: 1 1 220px; -} - -.checkbox { - display: flex; align-items: center; - gap: 10px; } -.checkbox input { - width: auto; -} +.checkbox input { width: auto; } pre { background: #0b1020; @@ -126,13 +140,65 @@ pre { border-radius: 16px; padding: 16px; overflow: auto; - min-height: 180px; + min-height: 160px; white-space: pre-wrap; word-break: break-word; } +.context-status { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 16px; + border-radius: 16px; + border: 1px solid var(--border); + background: #0b1020; + margin-bottom: 18px; +} + +.context-status p { + margin: 4px 0 0; + color: var(--muted); +} + +.indicator { + width: 16px; + height: 16px; + border-radius: 999px; + box-shadow: 0 0 14px rgba(255,255,255,0.15); +} + +.indicator-off { background: var(--danger); } +.indicator-on { background: var(--accent); } + +.chat-messages { + min-height: 260px; + max-height: 520px; + overflow: auto; + border: 1px solid var(--border); + border-radius: 16px; + padding: 16px; + background: #0b1020; + margin-bottom: 16px; +} + +.empty-chat { color: var(--muted); margin: 0; } + +.message { + margin-bottom: 14px; + padding: 14px; + border-radius: 14px; + background: rgba(28, 39, 66, 0.9); + border: 1px solid var(--border); +} + +.message.user { border-left: 4px solid #4fceff; } +.message.assistant { border-left: 4px solid var(--accent); } +.message header { font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); margin-bottom: 8px; } +.message p { margin: 0; white-space: pre-wrap; } + @media (max-width: 980px) { - .grid, .results-grid { + .grid, .single-grid, .chat-grid { grid-template-columns: 1fr; } diff --git a/RAG/src/app.ts b/RAG/src/app.ts index a6758cb..a99d46c 100644 --- a/RAG/src/app.ts +++ b/RAG/src/app.ts @@ -9,7 +9,7 @@ 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"; +import type { ChatMessage, ChunkMode, RetrieveIntent, RetrieveScope } from "./shared/types/rag.js"; export function createApp() { const __filename = fileURLToPath(import.meta.url); @@ -122,7 +122,8 @@ export function createApp() { } : undefined; const model = req.body.model ? String(req.body.model) : undefined; - const result = await answerService.answer(mode, intent, query, scope, model); + const preloadedContext = req.body.preloadedContext ? String(req.body.preloadedContext) : undefined; + const result = await answerService.answer(mode, intent, query, scope, model, preloadedContext); res.json(result); } catch (error) { res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown answer error" }); @@ -133,12 +134,53 @@ export function createApp() { try { const query = String(req.body.query ?? ""); const model = req.body.model ? String(req.body.model) : undefined; - const result = await answerService.answerWithoutRag(query, model); + const preloadedContext = req.body.preloadedContext ? String(req.body.preloadedContext) : undefined; + const result = await answerService.answerWithoutRag(query, model, preloadedContext); res.json(result); } catch (error) { res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown direct answer error" }); } }); + app.post("/chat", async (req, res) => { + try { + const message = String(req.body.message ?? ""); + const mode = (req.body.mode ?? "documental") as ChunkMode; + const model = req.body.model ? String(req.body.model) : undefined; + const preloadedContext = req.body.preloadedContext ? String(req.body.preloadedContext) : undefined; + const allowAdditionalRetrieve = Boolean(req.body.allowAdditionalRetrieve); + const historyEntries = Array.isArray(req.body.history) ? req.body.history as Array<{ role?: string; content?: unknown }> : []; + const history: ChatMessage[] = historyEntries.length > 0 + ? historyEntries + .map((entry: { role?: string; content?: unknown }): ChatMessage => ({ + role: entry.role === "assistant" ? "assistant" : "user", + content: String(entry.content ?? "") + })) + .filter((entry) => entry.content.length > 0) + : []; + 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.chat({ + message, + history, + mode, + scope, + modelOverride: model, + preloadedContext, + allowAdditionalRetrieve + }); + + res.json(result); + } catch (error) { + res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown chat error" }); + } + }); + return app; } diff --git a/RAG/src/modules/answer/service.ts b/RAG/src/modules/answer/service.ts index 26fc72a..9e3ca9b 100644 --- a/RAG/src/modules/answer/service.ts +++ b/RAG/src/modules/answer/service.ts @@ -1,6 +1,6 @@ import OpenAI from "openai"; import { env } from "../../config/env.js"; -import type { AnswerResponse, ChunkMode, DirectAnswerResponse, RetrieveIntent, RetrieveResponse, RetrieveScope, RetrievedItem } from "../../shared/types/rag.js"; +import type { AnswerResponse, ChatMessage, ChatResponse, ChunkMode, DirectAnswerResponse, RetrieveIntent, RetrieveResponse, RetrieveScope, RetrievedItem } from "../../shared/types/rag.js"; import { RetrieveService } from "../retrieve/service.js"; const answerModelFallbacks = [ @@ -11,7 +11,7 @@ const answerModelFallbacks = [ "mistralai/mistral-small-3.1-24b-instruct" ]; -function buildPrompt(query: string, summary: string, items: RetrievedItem[]): string { +function buildPrompt(query: string, summary: string, items: RetrievedItem[], preloadedContext?: string): string { const context = items .map((item, index) => [ `Fuente ${index + 1}: ${item.title}`, @@ -27,11 +27,42 @@ function buildPrompt(query: string, summary: string, items: RetrievedItem[]): st "Responde usando solo el contexto recuperado.", "Si el contexto no es suficiente, dilo claramente.", "Prioriza exactitud y brevedad.", + preloadedContext ? `Contexto base precargado de la sesion: ${preloadedContext}` : undefined, `Resumen del contexto: ${summary}`, `Pregunta: ${query}`, "Contexto recuperado:", context - ].join("\n\n"); + ].filter(Boolean).join("\n\n"); +} + +function buildChatPrompt(message: string, history: ChatMessage[], preloadedContext?: string, retrieved?: RetrieveResponse): string { + const serializedHistory = history + .slice(-8) + .map((entry) => `${entry.role.toUpperCase()}: ${entry.content}`) + .join("\n"); + + const retrievedContext = retrieved + ? [ + `Resumen retrieve: ${retrieved.summary}`, + retrieved.modelSummary ? `Sintesis con modelo: ${retrieved.modelSummary}` : undefined, + `Temas: ${retrieved.topics.join(", ")}`, + `Puntos criticos: ${retrieved.criticalPoints.join(", ")}`, + "Fragmentos recuperados:", + retrieved.items.map((item, index) => [ + `Fuente ${index + 1}: ${item.title}`, + item.sectionTitle ? `section_title: ${item.sectionTitle}` : undefined, + item.startLine ? `line_range: ${item.startLine}-${item.endLine ?? item.startLine}` : undefined, + item.content + ].filter(Boolean).join("\n")).join("\n\n---\n\n") + ].filter(Boolean).join("\n\n") + : undefined; + + return [ + preloadedContext ? `Contexto base precargado:\n${preloadedContext}` : undefined, + serializedHistory ? `Historial reciente:\n${serializedHistory}` : undefined, + retrievedContext ? `Contexto adicional recuperado:\n${retrievedContext}` : undefined, + `Mensaje actual del usuario:\n${message}` + ].filter(Boolean).join("\n\n"); } export class AnswerService { @@ -44,7 +75,7 @@ export class AnswerService { }); } - async answer(mode: ChunkMode, intent: RetrieveIntent, query: string, scope?: RetrieveScope, modelOverride?: string): Promise { + async answer(mode: ChunkMode, intent: RetrieveIntent, query: string, scope?: RetrieveScope, modelOverride?: string, preloadedContext?: string): Promise { if (!env.answerApiKey) { throw new Error("Missing ANSWER_API_KEY for answer provider"); } @@ -61,7 +92,7 @@ export class AnswerService { }, { role: "user", - content: buildPrompt(query, retrieved.summary, retrieved.items) + content: buildPrompt(query, retrieved.summary, retrieved.items, preloadedContext) } ] }); @@ -86,7 +117,7 @@ export class AnswerService { }; } - async answerWithoutRag(query: string, modelOverride?: string): Promise { + async answerWithoutRag(query: string, modelOverride?: string, preloadedContext?: string): Promise { if (!env.answerApiKey) { throw new Error("Missing ANSWER_API_KEY for answer provider"); } @@ -102,7 +133,9 @@ export class AnswerService { }, { role: "user", - content: query + content: preloadedContext + ? `Contexto base precargado de la sesion:\n${preloadedContext}\n\nPregunta:\n${query}` + : query } ] }); @@ -178,4 +211,53 @@ export class AnswerService { return answerModelFallbacks; } } + + async chat(options: { + message: string; + history?: ChatMessage[]; + mode: ChunkMode; + scope?: RetrieveScope; + modelOverride?: string; + preloadedContext?: string; + allowAdditionalRetrieve?: boolean; + }): Promise { + if (!env.answerApiKey) { + throw new Error("Missing ANSWER_API_KEY for answer provider"); + } + + const answerModel = options.modelOverride?.trim() || env.answerModel; + let retrieved: RetrieveResponse | undefined; + + if (options.allowAdditionalRetrieve) { + retrieved = await this.retrieveService.retrieve(options.mode, "specific", options.message, options.scope); + } + + const completion = await this.client.chat.completions.create({ + model: answerModel, + temperature: 0.2, + messages: [ + { + role: "system", + content: "Eres un asistente conversacional apoyado por un sistema RAG. Usa primero el contexto precargado si existe. Si tambien recibes contexto adicional recuperado, apoyate en el. Si la informacion sigue siendo insuficiente, dilo claramente." + }, + { + role: "user", + content: buildChatPrompt( + options.message, + options.history ?? [], + options.preloadedContext, + retrieved + ) + } + ] + }); + + return { + model: answerModel, + answer: completion.choices[0]?.message?.content?.trim() || "No se pudo generar respuesta.", + usedBootstrapContext: Boolean(options.preloadedContext), + usedAdditionalRetrieve: Boolean(retrieved), + retrieved + }; + } } diff --git a/RAG/src/shared/types/rag.ts b/RAG/src/shared/types/rag.ts index a7a94ba..a85c285 100644 --- a/RAG/src/shared/types/rag.ts +++ b/RAG/src/shared/types/rag.ts @@ -91,3 +91,16 @@ export interface DirectAnswerResponse { model: string; answer: string; } + +export interface ChatMessage { + role: "user" | "assistant"; + content: string; +} + +export interface ChatResponse { + model: string; + answer: string; + usedBootstrapContext: boolean; + usedAdditionalRetrieve: boolean; + retrieved?: RetrieveResponse; +} diff --git a/docs/HISTORIAL_SESIONES.md b/docs/HISTORIAL_SESIONES.md index a73d447..f3b394c 100644 --- a/docs/HISTORIAL_SESIONES.md +++ b/docs/HISTORIAL_SESIONES.md @@ -64,6 +64,7 @@ Este archivo registra agentes y sesiones de trabajo de este workspace. - Implementacion de un playground web interno servido por el propio backend del RAG para probar `health`, `ingest`, `retrieve`, `answer` y comparacion sin RAG. - 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. +- 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. - 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.