feat: add chatbot memory and conversational replies
This commit is contained in:
@@ -110,7 +110,7 @@ Se añadió un flow en `data/flows.json` que usa Node-RED Dashboard para mostrar
|
|||||||
{ "prompt": "generame un als de reggaeton 2001" }
|
{ "prompt": "generame un als de reggaeton 2001" }
|
||||||
```
|
```
|
||||||
Devuelve el resumen del proyecto creado (nombre, hash y ruta del archivo). En el dashboard aparece la tarjeta **Chatbot ALS** para conversar visualmente.
|
Devuelve el resumen del proyecto creado (nombre, hash y ruta del archivo). En el dashboard aparece la tarjeta **Chatbot ALS** para conversar visualmente.
|
||||||
- Si envías mensajes generales (“hola”, “¿qué estilos tenemos?”, “qué hay en sources?”) el bot responde con el estado actual de la biblioteca o de la carpeta `data/sources/` y te guía hasta que le pidas explícitamente “generame un als ...”.
|
- Si envías mensajes generales (“hola”, “¿qué estilos tenemos?”, “qué hay en sources?”) el bot usa la API de MiniMax para responder de forma amigable, recuerda el contexto reciente y te guía hasta que le pidas explícitamente “generame un als ...”.
|
||||||
- Internamente se usa `data/lib/alsGenerator.js`: el bot elige una plantilla (`als-library.json`), edita el XML del `.als` (nombre del proyecto, tempo, anotaciones con los sources) y registra el resultado llamando otra vez a `/als/upload` para mantener la deduplicación.
|
- Internamente se usa `data/lib/alsGenerator.js`: el bot elige una plantilla (`als-library.json`), edita el XML del `.als` (nombre del proyecto, tempo, anotaciones con los sources) y registra el resultado llamando otra vez a `/als/upload` para mantener la deduplicación.
|
||||||
- También puedes usarlo desde terminal:
|
- También puedes usarlo desde terminal:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -365,7 +365,7 @@
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"z": "f68df1c4d2e4e1a9",
|
"z": "f68df1c4d2e4e1a9",
|
||||||
"name": "Chatbot ALS",
|
"name": "Chatbot ALS",
|
||||||
"func": "\nconst raw = typeof msg.payload === 'string' ? msg.payload : JSON.stringify(msg.payload || {});\nlet body = {};\ntry {\n body = typeof msg.payload === 'object' ? msg.payload : JSON.parse(raw);\n} catch (err) {\n body = {};\n}\nconst prompt = (body.prompt || body.message || msg.prompt || '').trim();\nif (!prompt) {\n msg.statusCode = 400;\n msg.payload = { error: 'Falta el prompt del chatbot.' };\n return msg;\n}\nconst generator = global.get('alsGenerator');\nif (!generator || typeof generator.generateFromPrompt !== 'function') {\n msg.statusCode = 500;\n msg.payload = { error: 'Generador no disponible.' };\n return msg;\n}\nconst info = generator.listLibrary ? generator.listLibrary() : [];\nconst sources = generator.listSources ? generator.listSources() : [];\nfunction shouldGenerate(text) {\n return /(genera(me)?|crea(me)?|haz(me)?|arma(me)?|produce(me)?).*(als|ableton|beat|proyecto)/i.test(text);\n}\nfunction shouldDescribeSources(text) {\n return /(source|sources|stems|loops|samples|archivos|carpeta)/i.test(text);\n}\nfunction describeSources(list) {\n if (!list || !list.length) {\n return 'Todav\u00eda no hay archivos en data/sources/. Copia stems o loops ah\u00ed para que puedan usarse como referencia.';\n }\n const preview = list.slice(0, 10).join(', ');\n return `Encontr\u00e9 ${list.length} archivos en data/sources/. Ejemplos: ${preview}${list.length > 10 ? ' ...' : ''}`;\n}\nfunction buildSummary(text, libraryList, sourceList) {\n if (!libraryList || !libraryList.length) {\n return 'A\u00fan no tengo proyectos cargados. Sube un .als primero y luego dime \"generame un als ...\" para empezar.';\n }\n const recent = libraryList.slice(-1)[0];\n const parts = [\n `Tengo ${libraryList.length} proyectos almacenados. El \u00faltimo fue \"${recent.projectName}\" a ${recent.liveSet?.tempo || 'N/D'} BPM.`,\n sourceList && sourceList.length ? `Hay ${sourceList.length} recursos en sources (ej: ${sourceList.slice(0,3).join(', ')})` : 'A\u00fan no hay archivos en data/sources/.',\n 'Cuando est\u00e9s listo dime algo como \"generame un als afrohouse 2025 con 124 bpm\" y preparo la sesi\u00f3n.'\n ];\n return parts.join(' ');\n}\nif (shouldDescribeSources(prompt)) {\n msg.statusCode = 200;\n msg.payload = { reply: describeSources(sources) };\n return msg;\n}\nif (!shouldGenerate(prompt)) {\n return (async () => {\n try {\n const reply = await (generator.chatReply ? generator.chatReply(prompt, info, sources) : buildSummary(prompt, info, sources));\n msg.statusCode = 200;\n msg.payload = { reply };\n return msg;\n } catch (err) {\n node.error(err.message, msg);\n msg.statusCode = 500;\n msg.payload = { error: err.message };\n return msg;\n }\n })();\n}\nreturn (async () => {\n try {\n const result = await generator.generateFromPrompt(prompt);\n msg.statusCode = 200;\n msg.payload = {\n prompt: prompt,\n projectName: result.plan.projectName,\n templateHash: result.plan.templateHash,\n outputPath: result.outputPath,\n registered: result.registered\n };\n return msg;\n } catch (err) {\n node.error(err.message, msg);\n msg.statusCode = 500;\n msg.payload = { error: err.message };\n return msg;\n }\n})();\n",
|
"func": "\nconst raw = typeof msg.payload === 'string' ? msg.payload : JSON.stringify(msg.payload || {});\nlet body = {};\ntry {\n body = typeof msg.payload === 'object' ? msg.payload : JSON.parse(raw);\n} catch (err) {\n body = {};\n}\nconst prompt = (body.prompt || body.message || msg.prompt || '').trim();\nconst chatHistory = context.get('chatHistory') || [];\nfunction remember(role, text) {\n if (!text) { return; }\n chatHistory.push({ role, text });\n if (chatHistory.length > 40) {\n chatHistory.shift();\n }\n context.set('chatHistory', chatHistory);\n}\nif (!prompt) {\n msg.statusCode = 400;\n msg.payload = { error: 'Falta el prompt del chatbot.' };\n return msg;\n}\nconst generator = global.get('alsGenerator');\nif (!generator || typeof generator.generateFromPrompt !== 'function') {\n msg.statusCode = 500;\n msg.payload = { error: 'Generador no disponible.' };\n return msg;\n}\nremember('user', prompt);\nconst info = generator.listLibrary ? generator.listLibrary() : [];\nconst sources = generator.listSources ? generator.listSources() : [];\nfunction shouldGenerate(text) {\n return /(genera(me)?|crea(me)?|haz(me)?|arma(me)?|produce(me)?).*(als|ableton|beat|proyecto)/i.test(text);\n}\nfunction shouldDescribeSources(text) {\n return /(source|sources|stems|loops|samples|archivos|carpeta)/i.test(text);\n}\nfunction describeSources(list) {\n if (!list || !list.length) {\n return 'Todav\u00eda no hay archivos en data/sources/. Copia stems o loops ah\u00ed para que puedan usarse como referencia.';\n }\n const preview = list.slice(0, 10).join(', ');\n return `Encontr\u00e9 ${list.length} archivos en data/sources/. Ejemplos: ${preview}${list.length > 10 ? ' ...' : ''}`;\n}\nfunction buildSummary(text, libraryList, sourceList) {\n if (!libraryList || !libraryList.length) {\n return 'A\u00fan no tengo proyectos cargados. Sube un .als primero y luego dime \"generame un als ...\" para empezar.';\n }\n const recent = libraryList.slice(-1)[0];\n const parts = [\n `Tengo ${libraryList.length} proyectos almacenados. El \u00faltimo fue \"${recent.projectName}\" a ${recent.liveSet?.tempo || 'N/D'} BPM.`,\n sourceList && sourceList.length ? `Hay ${sourceList.length} recursos en sources (ej: ${sourceList.slice(0,3).join(', ')})` : 'A\u00fan no hay archivos en data/sources/.',\n 'Cuando est\u00e9s listo dime algo como \"generame un als afrohouse 2025 con 124 bpm\" y preparo la sesi\u00f3n.'\n ];\n return parts.join(' ');\n}\nif (shouldDescribeSources(prompt)) {\n const reply = describeSources(sources);\n remember('bot', reply);\n msg.statusCode = 200;\n msg.payload = { reply };\n return msg;\n}\nif (!shouldGenerate(prompt)) {\n return (async () => {\n try {\n const reply = await (generator.chatReply ? generator.chatReply(prompt, info, sources, chatHistory) : buildSummary(prompt, info, sources));\n remember('bot', reply);\n msg.statusCode = 200;\n msg.payload = { reply };\n return msg;\n } catch (err) {\n node.error(err.message, msg);\n msg.statusCode = 500;\n msg.payload = { error: err.message };\n return msg;\n }\n })();\n}\nreturn (async () => {\n try {\n const result = await generator.generateFromPrompt(prompt);\n const summary = `Listo, gener\u00e9 \"${result.plan.projectName}\" usando ${result.plan.templateHash}.`;\n remember('bot', summary);\n msg.statusCode = 200;\n msg.payload = {\n prompt: prompt,\n projectName: result.plan.projectName,\n templateHash: result.plan.templateHash,\n outputPath: result.outputPath,\n registered: result.registered\n };\n return msg;\n } catch (err) {\n node.error(err.message, msg);\n remember('bot', err.message);\n msg.statusCode = 500;\n msg.payload = { error: err.message };\n return msg;\n }\n})();\n",
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
"noerr": 0,
|
"noerr": 0,
|
||||||
"initialize": "",
|
"initialize": "",
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ async function generateFromPrompt(prompt, options = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function conversationalReply(prompt, library, sources) {
|
async function conversationalReply(prompt, library, sources, history = []) {
|
||||||
const context = {
|
const context = {
|
||||||
prompt,
|
prompt,
|
||||||
projects: library.slice(-5).map((entry) => ({
|
projects: library.slice(-5).map((entry) => ({
|
||||||
@@ -361,22 +361,29 @@ async function conversationalReply(prompt, library, sources) {
|
|||||||
})),
|
})),
|
||||||
sources: sources.slice(0, 20)
|
sources: sources.slice(0, 20)
|
||||||
};
|
};
|
||||||
|
const historySummary = (history || [])
|
||||||
|
.slice(-10)
|
||||||
|
.map((msg) => `${msg.role === 'user' ? 'Usuario' : 'Bot'}: ${msg.text}`)
|
||||||
|
.join('\\n');
|
||||||
|
|
||||||
const text = await callAnthropic(
|
const text = await callAnthropic(
|
||||||
'Actúa como un productor musical amigable que ayuda a planear nuevos proyectos de Ableton Live. Responde en español latino, máximo 3 frases, incluye sugerencias creativas y cuándo usar \"generame un als ...\".',
|
'Actúa como un productor musical amigable que ayuda a planear nuevos proyectos de Ableton Live. Responde en español latino, máximo 3 frases, incluye sugerencias creativas y cuándo usar \"generame un als ...\".',
|
||||||
`Usuario: ${prompt}\nContexto: ${JSON.stringify(context, null, 2)}`
|
`Historial reciente:\n${historySummary || 'Sin historial'}\nUsuario: ${prompt}\nContexto: ${JSON.stringify(context, null, 2)}`
|
||||||
);
|
);
|
||||||
if (text) {
|
if (text) {
|
||||||
return text.trim();
|
return text.trim();
|
||||||
}
|
}
|
||||||
if (!library.length) {
|
if (!library.length) {
|
||||||
return '¡Hola! Aún no tengo proyectos para tomar como referencia. Sube un .als y cuéntame qué estilo buscas; luego pide "generame un als ..." y prepararé una sesión.';
|
const vibe = prompt ? `Me encanta esa idea de ${prompt}. ` : '';
|
||||||
|
return `${vibe}Aún no tengo proyectos para tomar como referencia. Sube un .als y luego dime "generame un als ..." y prepararé una sesión nueva.`;
|
||||||
}
|
}
|
||||||
const recent = library.slice(-1)[0];
|
const recent = library.slice(-1)[0];
|
||||||
const srcText =
|
const srcText =
|
||||||
sources && sources.length
|
sources && sources.length
|
||||||
? `También veo ${sources.length} archivos en data/sources/ (ej: ${sources.slice(0, 3).join(', ')}). `
|
? `También veo ${sources.length} archivos en data/sources/ (ej: ${sources.slice(0, 3).join(', ')}). `
|
||||||
: '';
|
: '';
|
||||||
return `Hola, tengo ${library.length} proyectos guardados y el más reciente es "${recent.projectName}" (${recent.liveSet?.tempo || 'N/D'} BPM). ${srcText}Cuéntame la vibra y cuando digas "generame un als ..." lo construyo.`;
|
const vibe = prompt ? `Esa vibra de "${prompt}" suena genial. ` : '';
|
||||||
|
return `${vibe}Tengo ${library.length} proyectos guardados y el más reciente es "${recent.projectName}" (${recent.liveSet?.tempo || 'N/D'} BPM). ${srcText}Cuéntame más y cuando digas "generame un als ..." lo construyo.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
Reference in New Issue
Block a user