Add bootstrap-aware chat playground for RAG
This commit is contained in:
parent
93985976d6
commit
150aa818ad
9 changed files with 634 additions and 188 deletions
|
|
@ -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": "<resumen del bootstrap>",
|
||||||
|
"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:
|
Respuesta esperada:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,12 @@ El backend lo sirve desde:
|
||||||
|
|
||||||
1. `health`
|
1. `health`
|
||||||
2. `ingest`
|
2. `ingest`
|
||||||
3. `retrieve`
|
3. `bootstrap`
|
||||||
4. `answer`
|
4. `chat` con contexto precargado
|
||||||
5. `answer` sin RAG para comparar impacto del contexto
|
5. `retrieve`
|
||||||
6. seleccion explicita del modelo de `answer`
|
6. `answer`
|
||||||
|
7. `answer` sin RAG para comparar impacto del contexto
|
||||||
|
8. seleccion explicita del modelo de `answer`
|
||||||
|
|
||||||
Tambien permite:
|
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
|
## Idea de uso
|
||||||
|
|
||||||
Este playground no sustituye a clientes finales ni al futuro MCP.
|
Este playground no sustituye a clientes finales ni al futuro MCP.
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,46 @@
|
||||||
const healthButton = document.getElementById("healthButton");
|
const healthButton = document.getElementById("healthButton");
|
||||||
const ingestButton = document.getElementById("ingestButton");
|
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 presetDocs = document.getElementById("presetDocs");
|
||||||
const presetRagDocs = document.getElementById("presetRagDocs");
|
const presetRagDocs = document.getElementById("presetRagDocs");
|
||||||
const presetCode = document.getElementById("presetCode");
|
const presetCode = document.getElementById("presetCode");
|
||||||
|
|
||||||
const healthResult = document.getElementById("healthResult");
|
const healthResult = document.getElementById("healthResult");
|
||||||
|
const ingestResult = document.getElementById("ingestResult");
|
||||||
|
const bootstrapResult = document.getElementById("bootstrapResult");
|
||||||
const mainResult = document.getElementById("mainResult");
|
const mainResult = document.getElementById("mainResult");
|
||||||
const compareResult = document.getElementById("compareResult");
|
const compareResult = document.getElementById("compareResult");
|
||||||
|
const bootstrapContextResult = document.getElementById("bootstrapContextResult");
|
||||||
const queryInput = document.getElementById("queryInput");
|
const chatMessages = document.getElementById("chatMessages");
|
||||||
const queryMode = document.getElementById("queryMode");
|
const contextIndicator = document.getElementById("contextIndicator");
|
||||||
const queryIntent = document.getElementById("queryIntent");
|
const contextStatusText = document.getElementById("contextStatusText");
|
||||||
const queryOperation = document.getElementById("queryOperation");
|
const contextScopeText = document.getElementById("contextScopeText");
|
||||||
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 ingestSourceType = document.getElementById("ingestSourceType");
|
const ingestSourceType = document.getElementById("ingestSourceType");
|
||||||
const ingestSourceRef = document.getElementById("ingestSourceRef");
|
const ingestSourceRef = document.getElementById("ingestSourceRef");
|
||||||
const ingestMode = document.getElementById("ingestMode");
|
const ingestMode = document.getElementById("ingestMode");
|
||||||
const ingestTags = document.getElementById("ingestTags");
|
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) {
|
function format(value) {
|
||||||
return JSON.stringify(value, null, 2);
|
return JSON.stringify(value, null, 2);
|
||||||
}
|
}
|
||||||
|
|
@ -38,21 +54,66 @@ function buildScopeLabel(scope) {
|
||||||
return `${scope.sourceRef} [${modes}]`;
|
return `${scope.sourceRef} [${modes}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function request(url, payload, method = "POST") {
|
function request(url, payload, method = "POST") {
|
||||||
const response = await fetch(url, {
|
return fetch(url, {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: payload ? JSON.stringify(payload) : undefined
|
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();
|
function renderBootstrapContext() {
|
||||||
if (!response.ok) {
|
if (!lastBootstrapContext) {
|
||||||
throw new Error(data.error || `HTTP ${response.status}`);
|
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 = '<p class="empty-chat">Aun no hay conversacion. Carga un bootstrap y empieza a preguntar.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chatMessages.innerHTML = chatHistory.map((entry) => `
|
||||||
|
<article class="message ${entry.role}">
|
||||||
|
<header>${entry.role === "user" ? "Usuario" : "Modelo"}</header>
|
||||||
|
<p>${entry.content.replace(/</g, "<")}</p>
|
||||||
|
</article>
|
||||||
|
`).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() {
|
async function loadScopes() {
|
||||||
|
|
@ -76,11 +137,7 @@ async function loadScopes() {
|
||||||
placeholder.textContent = "No hay scopes detectados";
|
placeholder.textContent = "No hay scopes detectados";
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
scopePresetSelect.innerHTML = "";
|
scopePresetSelect.innerHTML = `<option value="">Error cargando scopes: ${error}</option>`;
|
||||||
const option = document.createElement("option");
|
|
||||||
option.value = "";
|
|
||||||
option.textContent = `Error cargando scopes: ${error}`;
|
|
||||||
scopePresetSelect.appendChild(option);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,26 +145,29 @@ async function loadAnswerModels() {
|
||||||
try {
|
try {
|
||||||
const payload = await fetch("/models/answer").then((response) => response.json());
|
const payload = await fetch("/models/answer").then((response) => response.json());
|
||||||
answerModel.innerHTML = "";
|
answerModel.innerHTML = "";
|
||||||
|
|
||||||
for (const model of payload.models || []) {
|
for (const model of payload.models || []) {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = model;
|
option.value = model;
|
||||||
option.textContent = model;
|
option.textContent = model;
|
||||||
answerModel.appendChild(option);
|
answerModel.appendChild(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.defaultModel) {
|
if (payload.defaultModel) {
|
||||||
answerModel.value = payload.defaultModel;
|
answerModel.value = payload.defaultModel;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
answerModel.innerHTML = "";
|
answerModel.innerHTML = '<option value="openai/gpt-4.1-mini">openai/gpt-4.1-mini</option>';
|
||||||
const option = document.createElement("option");
|
|
||||||
option.value = "openai/gpt-4.1-mini";
|
|
||||||
option.textContent = "openai/gpt-4.1-mini";
|
|
||||||
answerModel.appendChild(option);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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", () => {
|
scopePresetSelect.addEventListener("change", () => {
|
||||||
if (!scopePresetSelect.value) {
|
if (!scopePresetSelect.value) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -118,17 +178,18 @@ scopePresetSelect.addEventListener("change", () => {
|
||||||
scopeTags.value = (scope.tags || []).join(", ");
|
scopeTags.value = (scope.tags || []).join(", ");
|
||||||
|
|
||||||
if (scope.chunkModes.includes("codigo")) {
|
if (scope.chunkModes.includes("codigo")) {
|
||||||
queryMode.value = "codigo";
|
bootstrapMode.value = "codigo";
|
||||||
|
chatMode.value = "codigo";
|
||||||
} else if (scope.chunkModes.includes("documental")) {
|
} else if (scope.chunkModes.includes("documental")) {
|
||||||
queryMode.value = "documental";
|
bootstrapMode.value = "documental";
|
||||||
|
chatMode.value = "documental";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
healthButton.addEventListener("click", async () => {
|
healthButton.addEventListener("click", async () => {
|
||||||
healthResult.textContent = "Comprobando...";
|
healthResult.textContent = "Comprobando...";
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/health");
|
const data = await fetch("/health").then((response) => response.json());
|
||||||
const data = await response.json();
|
|
||||||
healthResult.textContent = format(data);
|
healthResult.textContent = format(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
healthResult.textContent = String(error);
|
healthResult.textContent = String(error);
|
||||||
|
|
@ -136,7 +197,7 @@ healthButton.addEventListener("click", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
ingestButton.addEventListener("click", async () => {
|
ingestButton.addEventListener("click", async () => {
|
||||||
mainResult.textContent = "Ejecutando ingesta...";
|
ingestResult.textContent = "Ejecutando ingesta...";
|
||||||
try {
|
try {
|
||||||
const data = await request("/ingest", {
|
const data = await request("/ingest", {
|
||||||
sourceType: ingestSourceType.value,
|
sourceType: ingestSourceType.value,
|
||||||
|
|
@ -144,72 +205,122 @@ ingestButton.addEventListener("click", async () => {
|
||||||
mode: ingestMode.value,
|
mode: ingestMode.value,
|
||||||
tags: splitTags(ingestTags.value)
|
tags: splitTags(ingestTags.value)
|
||||||
});
|
});
|
||||||
mainResult.textContent = format(data);
|
ingestResult.textContent = format(data);
|
||||||
|
await loadScopes();
|
||||||
} catch (error) {
|
} 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...";
|
mainResult.textContent = "Consultando...";
|
||||||
compareResult.textContent = compareWithoutRag.checked ? "Comparando..." : "Desactivada.";
|
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 {
|
try {
|
||||||
const endpoint = queryOperation.value === "answer" ? "/answer" : "/retrieve";
|
const response = await request("/chat", {
|
||||||
const data = await request(endpoint, payload);
|
message,
|
||||||
mainResult.textContent = format(data);
|
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") {
|
chatHistory.push({ role: "assistant", content: response.answer });
|
||||||
const comparison = await request("/answer/direct", {
|
renderChatHistory();
|
||||||
query: queryInput.value,
|
mainResult.textContent = format(response);
|
||||||
model: answerModel.value
|
|
||||||
});
|
if (compareWithoutRag.checked) {
|
||||||
compareResult.textContent = format(comparison);
|
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) {
|
} catch (error) {
|
||||||
mainResult.textContent = String(error);
|
mainResult.textContent = String(error);
|
||||||
compareResult.textContent = compareWithoutRag.checked ? String(error) : "Desactivada.";
|
compareResult.textContent = compareWithoutRag.checked ? String(error) : "Desactivada.";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearChatButton.addEventListener("click", () => {
|
||||||
|
chatHistory = [];
|
||||||
|
renderChatHistory();
|
||||||
|
mainResult.textContent = "Sin ejecutar aun.";
|
||||||
|
compareResult.textContent = "Desactivada.";
|
||||||
|
});
|
||||||
|
|
||||||
presetDocs.addEventListener("click", () => {
|
presetDocs.addEventListener("click", () => {
|
||||||
queryMode.value = "documental";
|
applyPreset(
|
||||||
queryIntent.value = "specific";
|
"documental",
|
||||||
queryOperation.value = "answer";
|
"dame un mapa inicial del workspace, sus lineas de trabajo principales, reglas, documentacion base y puntos importantes a tener presentes",
|
||||||
queryInput.value = "que tenemos pendiente por hacer en este workspace";
|
"/home/pancho/Documentos/Empresa/Desarrollo/IA/docs",
|
||||||
scopeSourceRef.value = "/home/pancho/Documentos/Empresa/Desarrollo/IA/docs";
|
true
|
||||||
answerModel.value = "openai/gpt-4.1-mini";
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
presetRagDocs.addEventListener("click", () => {
|
presetRagDocs.addEventListener("click", () => {
|
||||||
queryMode.value = "documental";
|
applyPreset(
|
||||||
queryIntent.value = "bootstrap";
|
"documental",
|
||||||
queryOperation.value = "retrieve";
|
"dame un mapa inicial del modulo RAG, su arquitectura, decisiones, estado actual y documentos clave",
|
||||||
queryInput.value = "dame un mapa inicial del modulo RAG, su arquitectura, decisiones, estado actual y documentos clave";
|
"/home/pancho/Documentos/Empresa/Desarrollo/IA/RAG/docs",
|
||||||
scopeSourceRef.value = "/home/pancho/Documentos/Empresa/Desarrollo/IA/RAG/docs";
|
true
|
||||||
answerModel.value = "openai/gpt-4.1-mini";
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
presetCode.addEventListener("click", () => {
|
presetCode.addEventListener("click", () => {
|
||||||
queryMode.value = "codigo";
|
applyPreset(
|
||||||
queryIntent.value = "specific";
|
"codigo",
|
||||||
queryOperation.value = "answer";
|
"dame un mapa inicial del codigo del modulo RAG, sus modulos principales, flujo interno y piezas clave",
|
||||||
queryInput.value = "como se construye source_id en el rag";
|
"/home/pancho/Documentos/Empresa/Desarrollo/IA/RAG/src",
|
||||||
scopeSourceRef.value = "/home/pancho/Documentos/Empresa/Desarrollo/IA/RAG/src";
|
true
|
||||||
answerModel.value = "openai/gpt-4.1-mini";
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
loadScopes();
|
loadScopes();
|
||||||
loadAnswerModels();
|
loadAnswerModels();
|
||||||
|
renderBootstrapContext();
|
||||||
|
renderChatHistory();
|
||||||
|
|
|
||||||
|
|
@ -11,70 +11,52 @@
|
||||||
<header class="hero">
|
<header class="hero">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">RAG Playground</p>
|
<p class="eyebrow">RAG Playground</p>
|
||||||
<h1>Pruebas rapidas del RAG</h1>
|
<h1>Laboratorio del RAG</h1>
|
||||||
<p class="lead">Interfaz interna para probar ingesta, retrieve, answer y comparacion sin RAG desde el mismo servicio.</p>
|
<p class="lead">Prueba ingesta, bootstrap y chat con contexto visible para evaluar el RAG como si estuviera integrado en una app o agente real.</p>
|
||||||
</div>
|
</div>
|
||||||
<button id="healthButton" class="secondary">Comprobar health</button>
|
<button id="healthButton" class="secondary">Comprobar health</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="grid">
|
<section class="tabs">
|
||||||
|
<button class="tab-button active" data-tab="ingest">Ingesta</button>
|
||||||
|
<button class="tab-button" data-tab="bootstrap">Bootstrap</button>
|
||||||
|
<button class="tab-button" data-tab="chat">Chat</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="tab-ingest" class="tab-panel active">
|
||||||
<article class="panel">
|
<article class="panel">
|
||||||
<h2>Ingesta</h2>
|
<h2>Ingesta</h2>
|
||||||
<label>Tipo de fuente
|
<div class="grid single-grid">
|
||||||
<select id="ingestSourceType">
|
<label>Tipo de fuente
|
||||||
<option value="folder">folder</option>
|
<select id="ingestSourceType">
|
||||||
<option value="file">file</option>
|
<option value="folder">folder</option>
|
||||||
</select>
|
<option value="file">file</option>
|
||||||
</label>
|
|
||||||
<label>Ruta de la fuente
|
|
||||||
<input id="ingestSourceRef" value="/home/pancho/Documentos/Empresa/Desarrollo/IA/docs" />
|
|
||||||
</label>
|
|
||||||
<label>Modo
|
|
||||||
<select id="ingestMode">
|
|
||||||
<option value="mechanical">mechanical</option>
|
|
||||||
<option value="interactive">interactive</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>Tags (coma separada)
|
|
||||||
<input id="ingestTags" value="workspace,global-docs" />
|
|
||||||
</label>
|
|
||||||
<button id="ingestButton">Lanzar ingesta</button>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="panel panel-wide">
|
|
||||||
<h2>Consulta</h2>
|
|
||||||
<label>Pregunta
|
|
||||||
<textarea id="queryInput" rows="4">que tenemos pendiente por hacer en este workspace</textarea>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<label>Modo
|
|
||||||
<select id="queryMode">
|
|
||||||
<option value="documental">documental</option>
|
|
||||||
<option value="codigo">codigo</option>
|
|
||||||
<option value="auto">auto</option>
|
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Intent
|
<label>Ruta de la fuente
|
||||||
<select id="queryIntent">
|
<input id="ingestSourceRef" value="/home/pancho/Documentos/Empresa/Desarrollo/IA/docs" />
|
||||||
<option value="specific">specific</option>
|
</label>
|
||||||
<option value="bootstrap">bootstrap</option>
|
<label>Modo de ingesta
|
||||||
|
<select id="ingestMode">
|
||||||
|
<option value="mechanical">mechanical</option>
|
||||||
|
<option value="interactive">interactive</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Operacion
|
<label>Tags (coma separada)
|
||||||
<select id="queryOperation">
|
<input id="ingestTags" value="workspace,global-docs" />
|
||||||
<option value="retrieve">retrieve</option>
|
|
||||||
<option value="answer">answer</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>Modelo answer
|
|
||||||
<select id="answerModel">
|
|
||||||
<option value="openai/gpt-4.1-mini">openai/gpt-4.1-mini</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="ingestButton">Lanzar ingesta</button>
|
||||||
|
</div>
|
||||||
|
<pre id="ingestResult">Sin ejecutar aun.</pre>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="row">
|
<section id="tab-bootstrap" class="tab-panel">
|
||||||
|
<article class="panel">
|
||||||
|
<h2>Bootstrap de sesion</h2>
|
||||||
|
<div class="grid single-grid">
|
||||||
<label>Scope disponible
|
<label>Scope disponible
|
||||||
<select id="scopePresetSelect">
|
<select id="scopePresetSelect">
|
||||||
<option value="">Cargando scopes...</option>
|
<option value="">Cargando scopes...</option>
|
||||||
|
|
@ -86,36 +68,98 @@
|
||||||
<label>Tags (coma separada)
|
<label>Tags (coma separada)
|
||||||
<input id="scopeTags" value="" />
|
<input id="scopeTags" value="" />
|
||||||
</label>
|
</label>
|
||||||
|
<label>Modo del bootstrap
|
||||||
|
<select id="bootstrapMode">
|
||||||
|
<option value="documental">documental</option>
|
||||||
|
<option value="codigo">codigo</option>
|
||||||
|
<option value="auto">auto</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>Modelo para sintetizar bootstrap
|
||||||
|
<select id="answerModel">
|
||||||
|
<option value="openai/gpt-4.1-mini">openai/gpt-4.1-mini</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>Consulta bootstrap
|
||||||
|
<textarea id="bootstrapQuery" rows="4">dame un mapa inicial del workspace, sus lineas de trabajo principales, reglas, documentacion base y puntos importantes a tener presentes</textarea>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" id="compareWithoutRag" />
|
<input type="checkbox" id="useModelInRetrieve" checked />
|
||||||
Comparar tambien con respuesta sin RAG
|
Usar un modelo para sintetizar el bootstrap recuperado
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox" id="useModelInRetrieve" />
|
|
||||||
Usar tambien un modelo para sintetizar el resultado de retrieve
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="queryButton">Ejecutar consulta</button>
|
<button id="bootstrapButton">Cargar bootstrap</button>
|
||||||
|
<button id="replaceBootstrapButton" class="secondary">Reemplazar contexto</button>
|
||||||
|
<button id="clearBootstrapButton" class="secondary">Vaciar contexto</button>
|
||||||
<button id="presetDocs" class="secondary">Preset docs</button>
|
<button id="presetDocs" class="secondary">Preset docs</button>
|
||||||
<button id="presetRagDocs" class="secondary">Preset RAG docs</button>
|
<button id="presetRagDocs" class="secondary">Preset RAG docs</button>
|
||||||
<button id="presetCode" class="secondary">Preset codigo</button>
|
<button id="presetCode" class="secondary">Preset codigo</button>
|
||||||
</div>
|
</div>
|
||||||
|
<pre id="bootstrapResult">Sin ejecutar aun.</pre>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="grid results-grid">
|
<section id="tab-chat" class="tab-panel">
|
||||||
<article class="panel">
|
<div class="grid chat-grid">
|
||||||
<h2>Resultado principal</h2>
|
<article class="panel context-panel">
|
||||||
<pre id="mainResult">Sin ejecutar aun.</pre>
|
<h2>Estado del contexto</h2>
|
||||||
</article>
|
<div class="context-status" id="contextStatusCard">
|
||||||
<article class="panel">
|
<span id="contextIndicator" class="indicator indicator-off"></span>
|
||||||
<h2>Comparacion sin RAG</h2>
|
<div>
|
||||||
<pre id="compareResult">Desactivada.</pre>
|
<strong id="contextStatusText">Sin contexto cargado</strong>
|
||||||
</article>
|
<p id="contextScopeText">No hay bootstrap activo.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" id="reuseBootstrapContext" checked />
|
||||||
|
Reutilizar bootstrap como contexto base
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" id="allowAdditionalRetrieve" checked />
|
||||||
|
Permitir que el modelo haga consultas adicionales al RAG durante la conversacion
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>Modo de chat
|
||||||
|
<select id="chatMode">
|
||||||
|
<option value="documental">documental</option>
|
||||||
|
<option value="codigo">codigo</option>
|
||||||
|
<option value="auto">auto</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<h3>Contexto bootstrap cargado</h3>
|
||||||
|
<pre id="bootstrapContextResult">Aun no hay bootstrap cargado.</pre>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="panel chat-panel">
|
||||||
|
<h2>Chat con el modelo</h2>
|
||||||
|
<div id="chatMessages" class="chat-messages">
|
||||||
|
<p class="empty-chat">Aun no hay conversacion. Carga un bootstrap y empieza a preguntar.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>Tu mensaje
|
||||||
|
<textarea id="chatInput" rows="4">que tenemos pendiente por hacer en este workspace</textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button id="sendChatButton">Enviar mensaje</button>
|
||||||
|
<button id="clearChatButton" class="secondary">Limpiar chat</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Ultima respuesta estructurada</h3>
|
||||||
|
<pre id="mainResult">Sin ejecutar aun.</pre>
|
||||||
|
|
||||||
|
<h3>Comparacion sin RAG</h3>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" id="compareWithoutRag" />
|
||||||
|
Comparar tambien con respuesta sin RAG
|
||||||
|
</label>
|
||||||
|
<pre id="compareResult">Desactivada.</pre>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
|
|
@ -126,4 +170,4 @@
|
||||||
|
|
||||||
<script src="/playground/app.js" type="module"></script>
|
<script src="/playground/app.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
--accent: #72e0a8;
|
--accent: #72e0a8;
|
||||||
--accent-2: #7ca8ff;
|
--accent-2: #7ca8ff;
|
||||||
--border: #2e3a5f;
|
--border: #2e3a5f;
|
||||||
|
--danger: #ff6b6b;
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
|
@ -20,12 +21,12 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout {
|
.layout {
|
||||||
max-width: 1280px;
|
max-width: 1320px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero, .panel {
|
.hero, .panel, .tabs {
|
||||||
background: rgba(21, 29, 49, 0.92);
|
background: rgba(21, 29, 49, 0.92);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
|
@ -38,9 +39,35 @@ body {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
margin-bottom: 24px;
|
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 {
|
.eyebrow {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|
@ -49,22 +76,20 @@ body {
|
||||||
letter-spacing: 0.14em;
|
letter-spacing: 0.14em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2 { margin: 0 0 12px; }
|
h1, h2, h3 { margin: 0 0 12px; }
|
||||||
.lead { margin: 0; color: var(--muted); max-width: 720px; }
|
.lead { margin: 0; color: var(--muted); max-width: 760px; }
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
grid-template-columns: 1fr 2fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.results-grid { grid-template-columns: 1fr 1fr; }
|
.single-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
.panel-wide { min-width: 0; }
|
.chat-grid { grid-template-columns: 0.9fr 1.3fr; }
|
||||||
|
|
||||||
.panel {
|
.panel { padding: 24px; }
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -100,25 +125,14 @@ button.secondary {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row, .actions {
|
.actions, .checkbox {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
|
||||||
|
|
||||||
.row > label {
|
|
||||||
flex: 1 1 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox input {
|
.checkbox input { width: auto; }
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
background: #0b1020;
|
background: #0b1020;
|
||||||
|
|
@ -126,13 +140,65 @@ pre {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
min-height: 180px;
|
min-height: 160px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
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) {
|
@media (max-width: 980px) {
|
||||||
.grid, .results-grid {
|
.grid, .single-grid, .chat-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { documentalChunkingPolicy } from "./modules/process/chunking.js";
|
||||||
import { RetrieveService } from "./modules/retrieve/service.js";
|
import { RetrieveService } from "./modules/retrieve/service.js";
|
||||||
import { supportedParserExtensions } from "./modules/parsers/parser-registry.js";
|
import { supportedParserExtensions } from "./modules/parsers/parser-registry.js";
|
||||||
import { QdrantVectorStoreClient } from "./modules/vectorstore/client.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() {
|
export function createApp() {
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
|
@ -122,7 +122,8 @@ export function createApp() {
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
const model = req.body.model ? String(req.body.model) : 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);
|
res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown answer error" });
|
res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown answer error" });
|
||||||
|
|
@ -133,12 +134,53 @@ export function createApp() {
|
||||||
try {
|
try {
|
||||||
const query = String(req.body.query ?? "");
|
const query = String(req.body.query ?? "");
|
||||||
const model = req.body.model ? String(req.body.model) : undefined;
|
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);
|
res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ ok: false, error: error instanceof Error ? error.message : "Unknown direct answer 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;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import OpenAI from "openai";
|
import OpenAI from "openai";
|
||||||
import { env } from "../../config/env.js";
|
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";
|
import { RetrieveService } from "../retrieve/service.js";
|
||||||
|
|
||||||
const answerModelFallbacks = [
|
const answerModelFallbacks = [
|
||||||
|
|
@ -11,7 +11,7 @@ const answerModelFallbacks = [
|
||||||
"mistralai/mistral-small-3.1-24b-instruct"
|
"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
|
const context = items
|
||||||
.map((item, index) => [
|
.map((item, index) => [
|
||||||
`Fuente ${index + 1}: ${item.title}`,
|
`Fuente ${index + 1}: ${item.title}`,
|
||||||
|
|
@ -27,11 +27,42 @@ function buildPrompt(query: string, summary: string, items: RetrievedItem[]): st
|
||||||
"Responde usando solo el contexto recuperado.",
|
"Responde usando solo el contexto recuperado.",
|
||||||
"Si el contexto no es suficiente, dilo claramente.",
|
"Si el contexto no es suficiente, dilo claramente.",
|
||||||
"Prioriza exactitud y brevedad.",
|
"Prioriza exactitud y brevedad.",
|
||||||
|
preloadedContext ? `Contexto base precargado de la sesion: ${preloadedContext}` : undefined,
|
||||||
`Resumen del contexto: ${summary}`,
|
`Resumen del contexto: ${summary}`,
|
||||||
`Pregunta: ${query}`,
|
`Pregunta: ${query}`,
|
||||||
"Contexto recuperado:",
|
"Contexto recuperado:",
|
||||||
context
|
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 {
|
export class AnswerService {
|
||||||
|
|
@ -44,7 +75,7 @@ export class AnswerService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async answer(mode: ChunkMode, intent: RetrieveIntent, query: string, scope?: RetrieveScope, modelOverride?: string): Promise<AnswerResponse> {
|
async answer(mode: ChunkMode, intent: RetrieveIntent, query: string, scope?: RetrieveScope, modelOverride?: string, preloadedContext?: string): Promise<AnswerResponse> {
|
||||||
if (!env.answerApiKey) {
|
if (!env.answerApiKey) {
|
||||||
throw new Error("Missing ANSWER_API_KEY for answer provider");
|
throw new Error("Missing ANSWER_API_KEY for answer provider");
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +92,7 @@ export class AnswerService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
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<DirectAnswerResponse> {
|
async answerWithoutRag(query: string, modelOverride?: string, preloadedContext?: string): Promise<DirectAnswerResponse> {
|
||||||
if (!env.answerApiKey) {
|
if (!env.answerApiKey) {
|
||||||
throw new Error("Missing ANSWER_API_KEY for answer provider");
|
throw new Error("Missing ANSWER_API_KEY for answer provider");
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +133,9 @@ export class AnswerService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
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;
|
return answerModelFallbacks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async chat(options: {
|
||||||
|
message: string;
|
||||||
|
history?: ChatMessage[];
|
||||||
|
mode: ChunkMode;
|
||||||
|
scope?: RetrieveScope;
|
||||||
|
modelOverride?: string;
|
||||||
|
preloadedContext?: string;
|
||||||
|
allowAdditionalRetrieve?: boolean;
|
||||||
|
}): Promise<ChatResponse> {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,3 +91,16 @@ export interface DirectAnswerResponse {
|
||||||
model: string;
|
model: string;
|
||||||
answer: string;
|
answer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
role: "user" | "assistant";
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatResponse {
|
||||||
|
model: string;
|
||||||
|
answer: string;
|
||||||
|
usedBootstrapContext: boolean;
|
||||||
|
usedAdditionalRetrieve: boolean;
|
||||||
|
retrieved?: RetrieveResponse;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
- 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.
|
- 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.
|
- 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/`.
|
- 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.
|
- 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.
|
- Creacion de `docs/TASK.md` para descomponer lineas de trabajo amplias en puntos de analisis y acuerdos.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue