From 93985976d6a0aca1196269e22dd59e79ca3ceb63 Mon Sep 17 00:00:00 2001 From: Paco POR-CORREO Date: Mon, 6 Apr 2026 14:52:25 +0200 Subject: [PATCH] Add selectable scopes and models to RAG playground --- RAG/public/playground/app.js | 27 +++++++++++ RAG/public/playground/index.html | 9 +++- RAG/src/app.ts | 23 ++++++++++ RAG/src/modules/answer/service.ts | 76 ++++++++++++++++++++++++++++++- RAG/src/shared/types/rag.ts | 2 + 5 files changed, 135 insertions(+), 2 deletions(-) diff --git a/RAG/public/playground/app.js b/RAG/public/playground/app.js index e79cfa8..21c3fde 100644 --- a/RAG/public/playground/app.js +++ b/RAG/public/playground/app.js @@ -14,6 +14,7 @@ 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"); @@ -83,6 +84,30 @@ async function loadScopes() { } } +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); + } +} + scopePresetSelect.addEventListener("change", () => { if (!scopePresetSelect.value) { return; @@ -134,6 +159,7 @@ queryButton.addEventListener("click", async () => { intent: queryIntent.value, query: queryInput.value, model: answerModel.value, + useModelInRetrieve: useModelInRetrieve.checked, scope: { sourceRef: scopeSourceRef.value, tags: splitTags(scopeTags.value) @@ -186,3 +212,4 @@ presetCode.addEventListener("click", () => { }); loadScopes(); +loadAnswerModels(); diff --git a/RAG/public/playground/index.html b/RAG/public/playground/index.html index d45fe08..8275ec4 100644 --- a/RAG/public/playground/index.html +++ b/RAG/public/playground/index.html @@ -68,7 +68,9 @@ @@ -91,6 +93,11 @@ Comparar tambien con respuesta sin RAG + +
diff --git a/RAG/src/app.ts b/RAG/src/app.ts index b919fdc..a6758cb 100644 --- a/RAG/src/app.ts +++ b/RAG/src/app.ts @@ -58,6 +58,18 @@ export function createApp() { } }); + app.get("/models/answer", async (_req, res) => { + try { + const models = await answerService.listAvailableAnswerModels(); + res.json({ + defaultModel: env.answerModel, + models + }); + } catch (error) { + res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown answer models error" }); + } + }); + app.post("/ingest", async (req, res) => { try { const result = await ingestService.ingest(req.body); @@ -72,6 +84,8 @@ export function createApp() { const mode = (req.body.mode ?? "auto") as ChunkMode; const intent = (req.body.intent ?? "specific") as RetrieveIntent; const query = String(req.body.query ?? ""); + const model = req.body.model ? String(req.body.model) : undefined; + const useModel = Boolean(req.body.useModelInRetrieve); const scope: RetrieveScope | undefined = req.body.scope ? { sourceId: req.body.scope.sourceId, @@ -80,6 +94,15 @@ export function createApp() { } : undefined; const result = await retrieveService.retrieve(mode, intent, query, scope); + if (useModel) { + const modelSummary = await answerService.summarizeRetrieve(query, result, model); + res.json({ + ...result, + model: modelSummary.model, + modelSummary: modelSummary.summary + }); + return; + } res.json(result); } catch (error) { res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown retrieve error" }); diff --git a/RAG/src/modules/answer/service.ts b/RAG/src/modules/answer/service.ts index 29da2db..26fc72a 100644 --- a/RAG/src/modules/answer/service.ts +++ b/RAG/src/modules/answer/service.ts @@ -1,8 +1,16 @@ import OpenAI from "openai"; import { env } from "../../config/env.js"; -import type { AnswerResponse, ChunkMode, DirectAnswerResponse, RetrieveIntent, RetrieveScope, RetrievedItem } from "../../shared/types/rag.js"; +import type { AnswerResponse, ChunkMode, DirectAnswerResponse, RetrieveIntent, RetrieveResponse, RetrieveScope, RetrievedItem } from "../../shared/types/rag.js"; import { RetrieveService } from "../retrieve/service.js"; +const answerModelFallbacks = [ + "openai/gpt-4.1-mini", + "qwen/qwen-2.5-72b-instruct", + "google/gemini-2.0-flash-001", + "anthropic/claude-3.5-haiku", + "mistralai/mistral-small-3.1-24b-instruct" +]; + function buildPrompt(query: string, summary: string, items: RetrievedItem[]): string { const context = items .map((item, index) => [ @@ -104,4 +112,70 @@ export class AnswerService { answer: completion.choices[0]?.message?.content?.trim() || "No se pudo generar respuesta." }; } + + async summarizeRetrieve(query: string, retrieved: RetrieveResponse, modelOverride?: string): Promise<{ model: string; summary: string }> { + if (!env.answerApiKey) { + throw new Error("Missing ANSWER_API_KEY for answer provider"); + } + + const answerModel = modelOverride?.trim() || env.answerModel; + const completion = await this.client.chat.completions.create({ + model: answerModel, + temperature: 0.2, + messages: [ + { + role: "system", + content: "Eres un sintetizador de contexto RAG. No des una respuesta final al usuario; devuelve una panoramica util y breve del contexto recuperado para que otro agente pueda continuar trabajando con el." + }, + { + role: "user", + content: [ + `Consulta original: ${query}`, + `Intent: ${retrieved.intent}`, + `Resumen base: ${retrieved.summary}`, + `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") + ].join("\n\n") + } + ] + }); + + return { + model: answerModel, + summary: completion.choices[0]?.message?.content?.trim() || retrieved.summary + }; + } + + async listAvailableAnswerModels(): Promise { + try { + const response = await fetch(`${env.answerBaseUrl}/models`, { + headers: { + Authorization: `Bearer ${env.answerApiKey}` + } + }); + + if (!response.ok) { + throw new Error(`Model list request failed with ${response.status}`); + } + + const payload = await response.json() as { data?: Array<{ id?: string }> }; + const ids = (payload.data ?? []) + .map((item) => item.id) + .filter((id): id is string => Boolean(id)) + .filter((id) => !/embed/i.test(id)); + + const preferred = answerModelFallbacks.filter((id) => ids.includes(id)); + const others = ids.filter((id) => !preferred.includes(id)).sort(); + return [...preferred, ...others].slice(0, 80); + } catch { + return answerModelFallbacks; + } + } } diff --git a/RAG/src/shared/types/rag.ts b/RAG/src/shared/types/rag.ts index 0434f4e..a7a94ba 100644 --- a/RAG/src/shared/types/rag.ts +++ b/RAG/src/shared/types/rag.ts @@ -58,7 +58,9 @@ export interface AvailableScope { export interface RetrieveResponse { mode: ChunkMode; intent: RetrieveIntent; + model?: string; summary: string; + modelSummary?: string; topics: string[]; criticalPoints: string[]; items: RetrievedItem[];