Add selectable scopes and models to RAG playground

This commit is contained in:
Paco POR-CORREO 2026-04-06 14:52:25 +02:00
parent 1847eaefbe
commit 93985976d6
5 changed files with 135 additions and 2 deletions

View file

@ -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();

View file

@ -68,7 +68,9 @@
</select>
</label>
<label>Modelo answer
<input id="answerModel" value="openai/gpt-4.1-mini" />
<select id="answerModel">
<option value="openai/gpt-4.1-mini">openai/gpt-4.1-mini</option>
</select>
</label>
</div>
@ -91,6 +93,11 @@
Comparar tambien con respuesta sin RAG
</label>
<label class="checkbox">
<input type="checkbox" id="useModelInRetrieve" />
Usar tambien un modelo para sintetizar el resultado de retrieve
</label>
<div class="actions">
<button id="queryButton">Ejecutar consulta</button>
<button id="presetDocs" class="secondary">Preset docs</button>

View file

@ -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" });

View file

@ -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<string[]> {
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;
}
}
}

View file

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