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[];