diff --git a/RAG/docs/API_RAG.md b/RAG/docs/API_RAG.md
index bc52c10..3a8af5a 100644
--- a/RAG/docs/API_RAG.md
+++ b/RAG/docs/API_RAG.md
@@ -47,6 +47,10 @@ Respuesta esperada aproximada:
"provider": "openrouter",
"model": "qwen/qwen3-embedding-8b"
},
+ "answer": {
+ "provider": "openrouter",
+ "model": "openai/gpt-4.1-mini"
+ },
"vectorStore": {
"ok": true,
"kind": "qdrant"
@@ -137,6 +141,7 @@ Payload base:
{
"mode": "documental",
"intent": "specific",
+ "model": "openai/gpt-4.1-mini",
"query": "que tenemos pendiente por hacer en este workspace",
"scope": {
"sourceRef": "/home/pancho/Documentos/Empresa/Desarrollo/IA/docs"
@@ -242,12 +247,15 @@ Es util para:
- respuestas listas para usuario
- pruebas rapidas desde n8n o aplicaciones
+Tambien permite indicar el modelo de respuesta explicitamente mediante el campo opcional `model`.
+
Payload base:
```json
{
"mode": "documental",
"intent": "specific",
+ "model": "openai/gpt-4.1-mini",
"query": "que tenemos pendiente por hacer en este workspace",
"scope": {
"sourceRef": "/home/pancho/Documentos/Empresa/Desarrollo/IA/docs"
@@ -291,6 +299,7 @@ Respuesta resumida esperada:
{
"mode": "codigo",
"intent": "specific",
+ "model": "openai/gpt-4.1-mini",
"answer": "...",
"summary": "...",
"topics": ["ids.ts"],
@@ -311,6 +320,32 @@ Respuesta resumida esperada:
}
```
+### 5. `POST /answer/direct`
+
+Sirve para pedir una respuesta directa al modelo sin usar contexto del RAG.
+
+Es util para comparar:
+- respuesta sin RAG
+- respuesta con RAG
+
+Payload base:
+
+```json
+{
+ "query": "que es este servicio",
+ "model": "openai/gpt-4.1-mini"
+}
+```
+
+Respuesta esperada:
+
+```json
+{
+ "model": "openai/gpt-4.1-mini",
+ "answer": "..."
+}
+```
+
---
## Recomendacion practica para n8n
diff --git a/RAG/docs/PLAYGROUND.md b/RAG/docs/PLAYGROUND.md
index a050807..e3a8e72 100644
--- a/RAG/docs/PLAYGROUND.md
+++ b/RAG/docs/PLAYGROUND.md
@@ -50,12 +50,14 @@ El backend lo sirve desde:
3. `retrieve`
4. `answer`
5. `answer` sin RAG para comparar impacto del contexto
+6. seleccion explicita del modelo de `answer`
Tambien permite:
- cambiar `mode`
- cambiar `intent`
- ajustar `scope`
+- seleccionar el modelo de respuesta
- usar presets para docs, docs del modulo y codigo del RAG
---
diff --git a/RAG/public/playground/app.js b/RAG/public/playground/app.js
index f15694b..e79cfa8 100644
--- a/RAG/public/playground/app.js
+++ b/RAG/public/playground/app.js
@@ -13,6 +13,8 @@ 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 scopePresetSelect = document.getElementById("scopePresetSelect");
const scopeSourceRef = document.getElementById("scopeSourceRef");
const scopeTags = document.getElementById("scopeTags");
const compareWithoutRag = document.getElementById("compareWithoutRag");
@@ -30,6 +32,11 @@ function splitTags(value) {
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
}
+function buildScopeLabel(scope) {
+ const modes = scope.chunkModes.join(", ") || "sin modo";
+ return `${scope.sourceRef} [${modes}]`;
+}
+
async function request(url, payload, method = "POST") {
const response = await fetch(url, {
method,
@@ -47,6 +54,51 @@ async function request(url, payload, method = "POST") {
return data;
}
+async function loadScopes() {
+ try {
+ const scopes = await fetch("/sources").then((response) => response.json());
+ scopePresetSelect.innerHTML = "";
+
+ const placeholder = document.createElement("option");
+ placeholder.value = "";
+ placeholder.textContent = "Selecciona un scope disponible";
+ scopePresetSelect.appendChild(placeholder);
+
+ for (const scope of scopes) {
+ const option = document.createElement("option");
+ option.value = JSON.stringify(scope);
+ option.textContent = buildScopeLabel(scope);
+ scopePresetSelect.appendChild(option);
+ }
+
+ if (scopes.length === 0) {
+ 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.addEventListener("change", () => {
+ if (!scopePresetSelect.value) {
+ return;
+ }
+
+ const scope = JSON.parse(scopePresetSelect.value);
+ scopeSourceRef.value = scope.sourceRef || "";
+ scopeTags.value = (scope.tags || []).join(", ");
+
+ if (scope.chunkModes.includes("codigo")) {
+ queryMode.value = "codigo";
+ } else if (scope.chunkModes.includes("documental")) {
+ queryMode.value = "documental";
+ }
+});
+
healthButton.addEventListener("click", async () => {
healthResult.textContent = "Comprobando...";
try {
@@ -81,6 +133,7 @@ queryButton.addEventListener("click", async () => {
mode: queryMode.value,
intent: queryIntent.value,
query: queryInput.value,
+ model: answerModel.value,
scope: {
sourceRef: scopeSourceRef.value,
tags: splitTags(scopeTags.value)
@@ -92,12 +145,13 @@ queryButton.addEventListener("click", async () => {
const data = await request(endpoint, payload);
mainResult.textContent = format(data);
- if (compareWithoutRag.checked && queryOperation.value === "answer") {
- const comparison = await request("/answer/direct", {
- query: queryInput.value
- });
- compareResult.textContent = format(comparison);
- }
+ if (compareWithoutRag.checked && queryOperation.value === "answer") {
+ const comparison = await request("/answer/direct", {
+ query: queryInput.value,
+ model: answerModel.value
+ });
+ compareResult.textContent = format(comparison);
+ }
} catch (error) {
mainResult.textContent = String(error);
compareResult.textContent = compareWithoutRag.checked ? String(error) : "Desactivada.";
@@ -110,6 +164,7 @@ presetDocs.addEventListener("click", () => {
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";
});
presetRagDocs.addEventListener("click", () => {
@@ -118,6 +173,7 @@ presetRagDocs.addEventListener("click", () => {
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";
});
presetCode.addEventListener("click", () => {
@@ -126,4 +182,7 @@ presetCode.addEventListener("click", () => {
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";
});
+
+loadScopes();
diff --git a/RAG/public/playground/index.html b/RAG/public/playground/index.html
index 79f9985..d45fe08 100644
--- a/RAG/public/playground/index.html
+++ b/RAG/public/playground/index.html
@@ -67,9 +67,17 @@
+
+
diff --git a/RAG/src/app.ts b/RAG/src/app.ts
index e5695e4..b919fdc 100644
--- a/RAG/src/app.ts
+++ b/RAG/src/app.ts
@@ -39,12 +39,25 @@ export function createApp() {
provider: embeddingProvider.providerName,
model: embeddingProvider.modelName
},
+ answer: {
+ provider: env.answerProvider,
+ model: env.answerModel
+ },
vectorStore: vectorHealth,
parsers: supportedParserExtensions(),
chunking: documentalChunkingPolicy
});
});
+ app.get("/sources", async (_req, res) => {
+ try {
+ const scopes = await vectorStore.listScopes();
+ res.json(scopes);
+ } catch (error) {
+ res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown sources error" });
+ }
+ });
+
app.post("/ingest", async (req, res) => {
try {
const result = await ingestService.ingest(req.body);
@@ -85,7 +98,8 @@ export function createApp() {
tags: Array.isArray(req.body.scope.tags) ? req.body.scope.tags.map(String) : undefined
}
: undefined;
- const result = await answerService.answer(mode, intent, query, scope);
+ const model = req.body.model ? String(req.body.model) : undefined;
+ const result = await answerService.answer(mode, intent, query, scope, model);
res.json(result);
} catch (error) {
res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown answer error" });
@@ -95,7 +109,8 @@ export function createApp() {
app.post("/answer/direct", async (req, res) => {
try {
const query = String(req.body.query ?? "");
- const result = await answerService.answerWithoutRag(query);
+ const model = req.body.model ? String(req.body.model) : undefined;
+ const result = await answerService.answerWithoutRag(query, model);
res.json(result);
} catch (error) {
res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown direct answer error" });
diff --git a/RAG/src/modules/answer/service.ts b/RAG/src/modules/answer/service.ts
index be661e4..29da2db 100644
--- a/RAG/src/modules/answer/service.ts
+++ b/RAG/src/modules/answer/service.ts
@@ -36,14 +36,15 @@ export class AnswerService {
});
}
- async answer(mode: ChunkMode, intent: RetrieveIntent, query: string, scope?: RetrieveScope): Promise
{
+ async answer(mode: ChunkMode, intent: RetrieveIntent, query: string, scope?: RetrieveScope, modelOverride?: string): Promise {
if (!env.answerApiKey) {
throw new Error("Missing ANSWER_API_KEY for answer provider");
}
const retrieved = await this.retrieveService.retrieve(mode, intent, query, scope);
+ const answerModel = modelOverride?.trim() || env.answerModel;
const completion = await this.client.chat.completions.create({
- model: env.answerModel,
+ model: answerModel,
temperature: 0.2,
messages: [
{
@@ -60,6 +61,7 @@ export class AnswerService {
return {
mode: retrieved.mode,
intent: retrieved.intent,
+ model: answerModel,
answer: completion.choices[0]?.message?.content?.trim() || "No se pudo generar respuesta.",
summary: retrieved.summary,
topics: retrieved.topics,
@@ -76,13 +78,14 @@ export class AnswerService {
};
}
- async answerWithoutRag(query: string): Promise {
+ async answerWithoutRag(query: string, modelOverride?: string): Promise {
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: env.answerModel,
+ model: answerModel,
temperature: 0.2,
messages: [
{
@@ -97,7 +100,7 @@ export class AnswerService {
});
return {
- model: env.answerModel,
+ model: answerModel,
answer: completion.choices[0]?.message?.content?.trim() || "No se pudo generar respuesta."
};
}
diff --git a/RAG/src/modules/vectorstore/client.ts b/RAG/src/modules/vectorstore/client.ts
index b67f717..7bb962a 100644
--- a/RAG/src/modules/vectorstore/client.ts
+++ b/RAG/src/modules/vectorstore/client.ts
@@ -1,6 +1,6 @@
import { QdrantClient } from "@qdrant/js-client-rest";
import { env } from "../../config/env.js";
-import type { IngestedChunk, RetrieveScope, RetrievedItem } from "../../shared/types/rag.js";
+import type { AvailableScope, IngestedChunk, RetrieveScope, RetrievedItem } from "../../shared/types/rag.js";
function buildQdrantClient(): QdrantClient {
const url = new URL(env.qdrantUrl);
@@ -19,6 +19,7 @@ export interface VectorStoreClient {
ensureCollection(vectorSize: number): Promise;
upsert(chunks: IngestedChunk[]): Promise;
search(queryVector: number[], limit: number, mode?: string, scope?: RetrieveScope): Promise;
+ listScopes(): Promise;
}
function buildSearchFilter(mode?: string, scope?: RetrieveScope) {
@@ -133,4 +134,59 @@ export class QdrantVectorStoreClient implements VectorStoreClient {
endLine: point.payload?.end_line ? Number(point.payload.end_line) : undefined
}));
}
+
+ async listScopes(): Promise {
+ const collections = await this.client.getCollections();
+ const exists = collections.collections.some((collection) => collection.name === env.qdrantCollection);
+
+ if (!exists) {
+ return [];
+ }
+
+ const scopeMap = new Map();
+ let nextOffset = undefined;
+
+ do {
+ const response = await this.client.scroll(env.qdrantCollection, {
+ limit: 128,
+ offset: nextOffset,
+ with_payload: ["source_id", "source_ref", "chunk_mode", "tags"]
+ });
+
+ for (const point of response.points) {
+ const sourceId = String(point.payload?.source_id ?? "");
+ const sourceRef = String(point.payload?.source_ref ?? "");
+ const chunkMode = String(point.payload?.chunk_mode ?? "");
+ const tags = Array.isArray(point.payload?.tags) ? point.payload.tags.map(String) : [];
+
+ if (!sourceId || !sourceRef) {
+ continue;
+ }
+
+ const key = `${sourceId}::${sourceRef}`;
+ const existing = scopeMap.get(key) ?? {
+ sourceId,
+ sourceRef,
+ chunkModes: [],
+ tags: []
+ };
+
+ if ((chunkMode === "documental" || chunkMode === "codigo" || chunkMode === "auto") && !existing.chunkModes.includes(chunkMode)) {
+ existing.chunkModes.push(chunkMode);
+ }
+
+ for (const tag of tags) {
+ if (!existing.tags.includes(tag)) {
+ existing.tags.push(tag);
+ }
+ }
+
+ scopeMap.set(key, existing);
+ }
+
+ nextOffset = response.next_page_offset;
+ } while (nextOffset);
+
+ return [...scopeMap.values()].sort((left, right) => left.sourceRef.localeCompare(right.sourceRef));
+ }
}
diff --git a/RAG/src/shared/types/rag.ts b/RAG/src/shared/types/rag.ts
index 8b4e376..0434f4e 100644
--- a/RAG/src/shared/types/rag.ts
+++ b/RAG/src/shared/types/rag.ts
@@ -48,6 +48,13 @@ export interface RetrieveScope {
tags?: string[];
}
+export interface AvailableScope {
+ sourceId: string;
+ sourceRef: string;
+ chunkModes: ChunkMode[];
+ tags: string[];
+}
+
export interface RetrieveResponse {
mode: ChunkMode;
intent: RetrieveIntent;
@@ -62,6 +69,7 @@ export interface RetrieveResponse {
export interface AnswerResponse {
mode: ChunkMode;
intent: RetrieveIntent;
+ model: string;
answer: string;
summary: string;
topics: string[];
diff --git a/docs/HISTORIAL_SESIONES.md b/docs/HISTORIAL_SESIONES.md
index 965ccc7..a73d447 100644
--- a/docs/HISTORIAL_SESIONES.md
+++ b/docs/HISTORIAL_SESIONES.md
@@ -60,8 +60,10 @@ Este archivo registra agentes y sesiones de trabajo de este workspace.
- Pruebas satisfactorias en produccion de `health`, `retrieve` y `answer`, tanto en modo documental como en modo codigo.
- Deteccion y anotacion como pendiente del cambio futuro del modelo actual de `answer`.
- Creacion de `RAG/docs/API_RAG.md` como referencia operativa de la API para conectar rapidamente el servicio desde n8n, agentes y otras aplicaciones.
+- Alineacion de las variables de entorno para EasyPanel, dejando `.env.example` ajustado al despliegue esperado y `RAG/.env.easypanel.local` como respaldo operativo local ignorado por Git.
- 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.
- 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.