feat: route chatbot replies through ai helper
This commit is contained in:
@@ -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 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,
|
"outputs": 1,
|
||||||
"noerr": 0,
|
"noerr": 0,
|
||||||
"initialize": "",
|
"initialize": "",
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ function extractJson(text) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function planWithAI(prompt, library, sources) {
|
function anthConfig() {
|
||||||
const base =
|
const base =
|
||||||
(process.env.ANTHROPIC_BASE_URL || '').trim() ||
|
(process.env.ANTHROPIC_BASE_URL || '').trim() ||
|
||||||
(process.env.ANTHROPIC_FALLBACK_BASE_URL || '').trim();
|
(process.env.ANTHROPIC_FALLBACK_BASE_URL || '').trim();
|
||||||
@@ -126,43 +126,35 @@ async function planWithAI(prompt, library, sources) {
|
|||||||
if (!base || !token) {
|
if (!base || !token) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const endpoint = `${base.replace(/\/$/, '')}/v1/messages`;
|
return {
|
||||||
const contextEntries = library
|
endpoint: `${base.replace(/\/$/, '')}/v1/messages`,
|
||||||
.slice(0, 3)
|
token
|
||||||
.map((entry) => ({
|
};
|
||||||
projectName: entry.projectName,
|
}
|
||||||
hash: entry.hash,
|
|
||||||
tempo: entry.liveSet?.tempo,
|
async function callAnthropic(systemText, userText, maxTokens = 800) {
|
||||||
tracks: (entry.tracks || []).slice(0, 5).map((t) => t.name)
|
const cfg = anthConfig();
|
||||||
}));
|
if (!cfg) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
model: process.env.ANTHROPIC_MODEL || 'claude-3-5-sonnet-20241022',
|
model: process.env.ANTHROPIC_MODEL || 'claude-3-5-sonnet-20241022',
|
||||||
max_tokens: 800,
|
max_tokens: maxTokens,
|
||||||
temperature: 0.2,
|
temperature: 0.3,
|
||||||
system:
|
system: systemText,
|
||||||
'Eres un generador de sesiones Ableton Live (.als). Devuelves un JSON con projectName, templateHash, tempo, trackNames (array) y notes. Usa solo los hashes ofrecidos.',
|
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: [
|
content: [{ type: 'text', text: userText }]
|
||||||
{
|
|
||||||
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":"..."}'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const res = await fetch(endpoint, {
|
const res = await fetch(cfg.endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
'x-api-key': token,
|
'x-api-key': cfg.token,
|
||||||
'anthropic-version': '2023-06-01'
|
'anthropic-version': '2023-06-01'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
@@ -172,18 +164,37 @@ async function planWithAI(prompt, library, sources) {
|
|||||||
throw new Error(await res.text());
|
throw new Error(await res.text());
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const text =
|
return data?.content?.[0]?.text || '';
|
||||||
data?.content?.[0]?.text ||
|
|
||||||
data?.content?.[0]?.text ||
|
|
||||||
data?.content ||
|
|
||||||
'';
|
|
||||||
return extractJson(text);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[alsGenerator] Error llamando a la IA:', err.message);
|
console.warn('[alsGenerator] Anthropic error:', err.message);
|
||||||
return null;
|
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) {
|
function setNameNode(nameNode, value) {
|
||||||
if (!nameNode) {
|
if (!nameNode) {
|
||||||
return;
|
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 = {
|
module.exports = {
|
||||||
generateFromPrompt,
|
generateFromPrompt,
|
||||||
listLibrary: () => {
|
listLibrary: () => {
|
||||||
@@ -349,5 +388,6 @@ module.exports = {
|
|||||||
listSources: () => {
|
listSources: () => {
|
||||||
ensureDirs();
|
ensureDirs();
|
||||||
return scanSources();
|
return scanSources();
|
||||||
}
|
},
|
||||||
|
chatReply: conversationalReply
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user