From 7fb03b4aacb0d0f388a735a9195361e24eca0a2d Mon Sep 17 00:00:00 2001 From: renato97 Date: Mon, 1 Dec 2025 03:10:57 +0000 Subject: [PATCH] feat: route chatbot replies through ai helper --- data/flows.json | 2 +- data/lib/alsGenerator.js | 108 +++++++++++++++++++++++++++------------ 2 files changed, 75 insertions(+), 35 deletions(-) diff --git a/data/flows.json b/data/flows.json index 17d4f5b..cacce49 100644 --- a/data/flows.json +++ b/data/flows.json @@ -365,7 +365,7 @@ "type": "function", "z": "f68df1c4d2e4e1a9", "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 msg.statusCode = 200;\n msg.payload = { reply: buildSummary(prompt, info, sources) };\n return msg;\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();\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", "outputs": 1, "noerr": 0, "initialize": "", diff --git a/data/lib/alsGenerator.js b/data/lib/alsGenerator.js index 6bdbb95..bcc2187 100644 --- a/data/lib/alsGenerator.js +++ b/data/lib/alsGenerator.js @@ -116,7 +116,7 @@ function extractJson(text) { return null; } -async function planWithAI(prompt, library, sources) { +function anthConfig() { const base = (process.env.ANTHROPIC_BASE_URL || '').trim() || (process.env.ANTHROPIC_FALLBACK_BASE_URL || '').trim(); @@ -126,43 +126,35 @@ async function planWithAI(prompt, library, sources) { if (!base || !token) { return null; } - const endpoint = `${base.replace(/\/$/, '')}/v1/messages`; - const contextEntries = library - .slice(0, 3) - .map((entry) => ({ - projectName: entry.projectName, - hash: entry.hash, - tempo: entry.liveSet?.tempo, - tracks: (entry.tracks || []).slice(0, 5).map((t) => t.name) - })); + return { + endpoint: `${base.replace(/\/$/, '')}/v1/messages`, + token + }; +} + +async function callAnthropic(systemText, userText, maxTokens = 800) { + const cfg = anthConfig(); + if (!cfg) { + return null; + } const payload = { model: process.env.ANTHROPIC_MODEL || 'claude-3-5-sonnet-20241022', - max_tokens: 800, - temperature: 0.2, - system: - 'Eres un generador de sesiones Ableton Live (.als). Devuelves un JSON con projectName, templateHash, tempo, trackNames (array) y notes. Usa solo los hashes ofrecidos.', + max_tokens: maxTokens, + temperature: 0.3, + system: systemText, messages: [ { role: 'user', - content: [ - { - type: 'text', - text: - `Prompt del usuario: ${prompt}\n` + - `Plantillas disponibles: ${JSON.stringify(contextEntries, null, 2)}\n` + - `Sources disponibles (${sources.length}): ${sources.join(', ') || 'ninguna'}\n` + - 'Respuesta esperada (JSON): {"projectName":"","templateHash":"","tempo":120,"trackNames":["..."],"notes":"..."}' - } - ] + content: [{ type: 'text', text: userText }] } ] }; try { - const res = await fetch(endpoint, { + const res = await fetch(cfg.endpoint, { method: 'POST', headers: { 'content-type': 'application/json', - 'x-api-key': token, + 'x-api-key': cfg.token, 'anthropic-version': '2023-06-01' }, body: JSON.stringify(payload), @@ -172,18 +164,37 @@ async function planWithAI(prompt, library, sources) { throw new Error(await res.text()); } const data = await res.json(); - const text = - data?.content?.[0]?.text || - data?.content?.[0]?.text || - data?.content || - ''; - return extractJson(text); + return data?.content?.[0]?.text || ''; } catch (err) { - console.warn('[alsGenerator] Error llamando a la IA:', err.message); + console.warn('[alsGenerator] Anthropic error:', err.message); return null; } } +async function planWithAI(prompt, library, sources) { + const cfg = anthConfig(); + if (!cfg) { + return null; + } + const contextEntries = library + .slice(0, 3) + .map((entry) => ({ + projectName: entry.projectName, + hash: entry.hash, + tempo: entry.liveSet?.tempo, + tracks: (entry.tracks || []).slice(0, 5).map((t) => t.name) + })); + const text = await callAnthropic( + 'Eres un generador de sesiones Ableton Live (.als). Devuelves un JSON con projectName, templateHash, tempo, trackNames (array) y notes. Usa solo los hashes ofrecidos.', + `Prompt del usuario: ${prompt}\nPlantillas disponibles: ${JSON.stringify( + contextEntries, + null, + 2 + )}\nSources disponibles (${sources.length}): ${sources.join(', ') || 'ninguna'}\nRespuesta esperada (JSON): {"projectName":"","templateHash":"","tempo":120,"trackNames":["..."],"notes":"..."}` + ); + return extractJson(text); +} + function setNameNode(nameNode, value) { if (!nameNode) { return; @@ -340,6 +351,34 @@ async function generateFromPrompt(prompt, options = {}) { }; } +async function conversationalReply(prompt, library, sources) { + const context = { + prompt, + projects: library.slice(-5).map((entry) => ({ + projectName: entry.projectName, + hash: entry.hash, + tempo: entry.liveSet?.tempo + })), + sources: sources.slice(0, 20) + }; + 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 ...\".', + `Usuario: ${prompt}\nContexto: ${JSON.stringify(context, null, 2)}` + ); + if (text) { + return text.trim(); + } + 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 recent = library.slice(-1)[0]; + const srcText = + sources && sources.length + ? `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.`; +} + module.exports = { generateFromPrompt, listLibrary: () => { @@ -349,5 +388,6 @@ module.exports = { listSources: () => { ensureDirs(); return scanSources(); - } + }, + chatReply: conversationalReply };