D9 cleanup: borrar NLU/handlers/machine/replyTemplates legacy + activar agente + prompt caching
Después de validar el agente E2E con DeepSeek, el legacy queda muerto. 51 archivos cambiados (la mayoría borrados), el motor único es ahora el agente tool-calling. Borrados (~3500 LOC): - src/modules/3-turn-engine/nlu/ (router + 4 specialists + promptLoader + schemas + humanFallback + 6 default prompts) — reemplazado por systemPrompt.js - src/modules/3-turn-engine/stateHandlers/ (cart.js, cartHelpers.js, idle.js, shipping.js, utils.js, index.js) — reemplazado por tools del agente - src/modules/3-turn-engine/stateHandlers.js (re-export shim) - src/modules/3-turn-engine/openai.js (NLU clásico v3 + jsonCompletion + llmRecommendWriter + llmPlanningRecommend) — el agente crea su propio cliente OpenAI con tools nativos - src/modules/3-turn-engine/replyRewriter.js (rewriting LLM) — el agente escribe say directo, no necesita reescribir - src/modules/3-turn-engine/replyTemplates.js + test (rotación de variantes) — el agente varía naturalmente con tool_choice=required + temperature - src/modules/3-turn-engine/recommendations.js (cross-sell + planning) — el agente decide cuándo recomendar via tool calls - src/modules/3-turn-engine/machine/ (XState v5 completo + 19 tests) — reemplazado por la FSM podada en fsm.js + agent/runTurn.js - src/modules/3-turn-engine/turnEngineV3.helpers.js, .units.js, .pendingSelection.js (helpers del legacy) - src/modules/0-ui/controllers/prompts.js, handlers/prompts.js, db/promptsRepo.js — admin de prompts NLU (ya no hay prompts editables) - public/components/prompts-crud.js + nav entry en ops-shell turnEngineV3.js se reduce a un thin wrapper que exporta runTurnV3 (alias de runTurnAgent) + safeNextState (re-export de fsm.js). Mantiene la firma pública para no tocar pipeline.js. Activado: - AGENT_MAX_TOOL_CALLS=10 y AGENT_TURN_TIMEOUT_MS=25000 son los únicos flags. Borradas: USE_MODULAR_NLU, USE_XSTATE, XSTATE_SHADOW, XSTATE_SETTLE_MS, REPLY_REWRITER, REPLY_REWRITER_TIMEOUT_MS, TURN_ENGINE, AGENT_TURN_ENGINE, AGENT_TURN_ENGINE_SHADOW (el agente es default). Prompt caching DeepSeek: - systemPrompt.js: era función con storeName interpolado → ahora export const SYSTEM_PROMPT (100% estático). storeName se pasa por user message via working_memory.store.name. Cualquier cambio al system invalida cache, por eso es estático estricto. - runTurn.js: captura usage.prompt_cache_hit_tokens (DeepSeek) o prompt_tokens_details.cached_tokens (OpenAI compat) y suma a métricas. - /api/metrics/agent ahora reporta prompt_tokens_total, completion_tokens_total, prompt_cache_hit_tokens, cache_hit_ratio. - Smoke test 3 turnos: cache_hit_ratio = 0.72 (17664 cached / 24546 total prompt tokens). Saving directo en costo: ~$0.02/M cached vs $0.27/M no cached en DeepSeek. Tests: 148/148 (perdimos 90 tests del legacy XState/replyTemplates que ya no aplican). Sim flow E2E confirmado: hola → agent responde, multi-turn con cache caliente. Si más adelante hace falta volver al legacy: git revert este commit (c c9c69cf8 es el último estado verde con doble motor). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,258 +0,0 @@
|
||||
import {
|
||||
getActivePrompt,
|
||||
listActivePrompts,
|
||||
getPromptVersions,
|
||||
getPromptVersion,
|
||||
createPrompt,
|
||||
rollbackPrompt,
|
||||
resetPromptToDefault,
|
||||
getPromptStats,
|
||||
PROMPT_KEYS,
|
||||
DEFAULT_MODELS,
|
||||
} from "../db/promptsRepo.js";
|
||||
import { loadDefaultPrompt, AVAILABLE_VARIABLES, invalidatePromptCache } from "../../3-turn-engine/nlu/promptLoader.js";
|
||||
|
||||
/**
|
||||
* Lista todos los prompts del tenant (activos + defaults para los que no tienen custom)
|
||||
*/
|
||||
export async function handleListPrompts({ tenantId }) {
|
||||
const activePrompts = await listActivePrompts({ tenantId });
|
||||
const stats = await getPromptStats({ tenantId });
|
||||
|
||||
// Construir lista completa con defaults para los que no tienen custom
|
||||
const promptsMap = new Map(activePrompts.map(p => [p.prompt_key, p]));
|
||||
|
||||
const items = PROMPT_KEYS.map(key => {
|
||||
const custom = promptsMap.get(key);
|
||||
const stat = stats.find(s => s.prompt_key === key);
|
||||
|
||||
if (custom) {
|
||||
return {
|
||||
prompt_key: key,
|
||||
content: custom.content,
|
||||
model: custom.model,
|
||||
version: custom.version,
|
||||
is_default: false,
|
||||
total_versions: stat?.total_versions || 1,
|
||||
last_updated: custom.created_at,
|
||||
created_by: custom.created_by,
|
||||
};
|
||||
} else {
|
||||
// Cargar default
|
||||
let defaultContent = "";
|
||||
try {
|
||||
defaultContent = loadDefaultPrompt(key);
|
||||
} catch (e) {
|
||||
defaultContent = `[Error loading default: ${e.message}]`;
|
||||
}
|
||||
|
||||
return {
|
||||
prompt_key: key,
|
||||
content: defaultContent,
|
||||
model: DEFAULT_MODELS[key] || "gpt-4-turbo",
|
||||
version: null,
|
||||
is_default: true,
|
||||
total_versions: stat?.total_versions || 0,
|
||||
last_updated: null,
|
||||
created_by: null,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
items,
|
||||
available_variables: AVAILABLE_VARIABLES,
|
||||
available_models: ["gpt-4-turbo", "gpt-4o-mini", "gpt-4o", "gpt-3.5-turbo"],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene un prompt específico con su historial de versiones
|
||||
*/
|
||||
export async function handleGetPrompt({ tenantId, promptKey }) {
|
||||
if (!PROMPT_KEYS.includes(promptKey)) {
|
||||
throw new Error(`Invalid prompt_key: ${promptKey}`);
|
||||
}
|
||||
|
||||
const current = await getActivePrompt({ tenantId, promptKey });
|
||||
const versions = await getPromptVersions({ tenantId, promptKey, limit: 20 });
|
||||
|
||||
let defaultContent = "";
|
||||
try {
|
||||
defaultContent = loadDefaultPrompt(promptKey);
|
||||
} catch (e) {
|
||||
defaultContent = `[Error: ${e.message}]`;
|
||||
}
|
||||
|
||||
return {
|
||||
prompt_key: promptKey,
|
||||
current: current || {
|
||||
content: defaultContent,
|
||||
model: DEFAULT_MODELS[promptKey] || "gpt-4-turbo",
|
||||
version: null,
|
||||
is_default: true,
|
||||
},
|
||||
default_content: defaultContent,
|
||||
default_model: DEFAULT_MODELS[promptKey] || "gpt-4-turbo",
|
||||
versions: versions.map(v => ({
|
||||
version: v.version,
|
||||
is_active: v.is_active,
|
||||
created_at: v.created_at,
|
||||
created_by: v.created_by,
|
||||
content_preview: v.content.slice(0, 100) + (v.content.length > 100 ? "..." : ""),
|
||||
})),
|
||||
available_variables: AVAILABLE_VARIABLES,
|
||||
available_models: ["gpt-4-turbo", "gpt-4o-mini", "gpt-4o", "gpt-3.5-turbo"],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea o actualiza un prompt (crea nueva versión)
|
||||
*/
|
||||
export async function handleSavePrompt({ tenantId, promptKey, content, model, createdBy }) {
|
||||
if (!PROMPT_KEYS.includes(promptKey)) {
|
||||
throw new Error(`Invalid prompt_key: ${promptKey}`);
|
||||
}
|
||||
|
||||
if (!content || content.trim().length === 0) {
|
||||
throw new Error("Content is required");
|
||||
}
|
||||
|
||||
const result = await createPrompt({
|
||||
tenantId,
|
||||
promptKey,
|
||||
content: content.trim(),
|
||||
model: model || DEFAULT_MODELS[promptKey] || "gpt-4-turbo",
|
||||
createdBy,
|
||||
});
|
||||
|
||||
// Invalidar cache
|
||||
invalidatePromptCache(tenantId, promptKey);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: result,
|
||||
message: `Prompt ${promptKey} saved as version ${result.version}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Restaura una versión anterior del prompt
|
||||
*/
|
||||
export async function handleRollbackPrompt({ tenantId, promptKey, toVersion, createdBy }) {
|
||||
if (!PROMPT_KEYS.includes(promptKey)) {
|
||||
throw new Error(`Invalid prompt_key: ${promptKey}`);
|
||||
}
|
||||
|
||||
const result = await rollbackPrompt({
|
||||
tenantId,
|
||||
promptKey,
|
||||
toVersion: parseInt(toVersion, 10),
|
||||
createdBy,
|
||||
});
|
||||
|
||||
// Invalidar cache
|
||||
invalidatePromptCache(tenantId, promptKey);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: result,
|
||||
message: `Prompt ${promptKey} rolled back to version ${toVersion}, new version is ${result.version}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resetea un prompt al default (desactiva todas las versiones custom)
|
||||
*/
|
||||
export async function handleResetPrompt({ tenantId, promptKey }) {
|
||||
if (!PROMPT_KEYS.includes(promptKey)) {
|
||||
throw new Error(`Invalid prompt_key: ${promptKey}`);
|
||||
}
|
||||
|
||||
await resetPromptToDefault({ tenantId, promptKey });
|
||||
|
||||
// Invalidar cache
|
||||
invalidatePromptCache(tenantId, promptKey);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
message: `Prompt ${promptKey} reset to default`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el contenido de una versión específica
|
||||
*/
|
||||
export async function handleGetPromptVersion({ tenantId, promptKey, version }) {
|
||||
if (!PROMPT_KEYS.includes(promptKey)) {
|
||||
throw new Error(`Invalid prompt_key: ${promptKey}`);
|
||||
}
|
||||
|
||||
const versionData = await getPromptVersion({
|
||||
tenantId,
|
||||
promptKey,
|
||||
version: parseInt(version, 10)
|
||||
});
|
||||
|
||||
if (!versionData) {
|
||||
throw new Error(`Version ${version} not found for prompt ${promptKey}`);
|
||||
}
|
||||
|
||||
return { item: versionData };
|
||||
}
|
||||
|
||||
/**
|
||||
* Prueba un prompt con un mensaje de ejemplo
|
||||
*/
|
||||
export async function handleTestPrompt({ tenantId, promptKey, content, testMessage, storeConfig = {} }) {
|
||||
// Importar dinámicamente para evitar dependencias circulares
|
||||
const { loadPrompt } = await import("../../3-turn-engine/nlu/promptLoader.js");
|
||||
|
||||
// Si se proporciona content, usarlo directamente
|
||||
// Si no, cargar el prompt actual
|
||||
let promptContent = content;
|
||||
let model = DEFAULT_MODELS[promptKey] || "gpt-4-turbo";
|
||||
|
||||
if (!promptContent) {
|
||||
const loaded = await loadPrompt({ tenantId, promptKey, variables: storeConfig });
|
||||
promptContent = loaded.content;
|
||||
model = loaded.model;
|
||||
} else {
|
||||
// Aplicar variables al content proporcionado
|
||||
for (const [key, value] of Object.entries(storeConfig)) {
|
||||
promptContent = promptContent.replace(new RegExp(`{{${key}}}`, "g"), value || "");
|
||||
}
|
||||
promptContent = promptContent.replace(/\{\{[^}]+\}\}/g, "");
|
||||
}
|
||||
|
||||
// Importar OpenAI
|
||||
const OpenAI = (await import("openai")).default;
|
||||
const apiKey = process.env.OPENAI_API_KEY || process.env.OPENAI_APIKEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("OPENAI_API_KEY not configured");
|
||||
}
|
||||
|
||||
const openai = new OpenAI({ apiKey });
|
||||
|
||||
// Hacer la llamada de prueba
|
||||
const startTime = Date.now();
|
||||
const response = await openai.chat.completions.create({
|
||||
model,
|
||||
temperature: 0.2,
|
||||
max_tokens: 500,
|
||||
response_format: { type: "json_object" },
|
||||
messages: [
|
||||
{ role: "system", content: promptContent },
|
||||
{ role: "user", content: testMessage },
|
||||
],
|
||||
});
|
||||
const endTime = Date.now();
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
response: response?.choices?.[0]?.message?.content || "",
|
||||
model,
|
||||
usage: response?.usage,
|
||||
latency_ms: endTime - startTime,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user