n8n-nodes-tembory 1.0.30 → 1.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Node de memoria operacional da Tembory para agentes de IA no n8n.
|
|
4
4
|
|
|
5
|
-
Versao atual: `1.0.
|
|
5
|
+
Versao atual: `1.0.31`.
|
|
6
|
+
|
|
7
|
+
## 1.0.31
|
|
8
|
+
|
|
9
|
+
- Considera pre-reserva recente no transcript como estado operacional valido quando `tool_history` ainda nao foi recuperado no turno.
|
|
10
|
+
- Evita chamar `agenda_pre_reservar_horario` novamente antes de confirmar quando a mensagem recente ja diz que houve pre-reserva.
|
|
11
|
+
- Direciona confirmacoes como "pode confirmar" direto para `agenda_confirmar_agendamento` quando existe pre-reserva recente pendente.
|
|
6
12
|
|
|
7
13
|
## 1.0.30
|
|
8
14
|
|
|
@@ -295,7 +295,7 @@ const extractProfileFactsFromText = (text, source = 'message', at = nowIso()) =>
|
|
|
295
295
|
const canExtractStrongProfileFacts = sourceName !== 'assistant_message';
|
|
296
296
|
if (!content.trim())
|
|
297
297
|
return facts;
|
|
298
|
-
if (/\[Used tools:|Calling\s+
|
|
298
|
+
if (/\[Used tools:|Calling\s+[A-Za-z_][A-Za-z0-9_.:-]*|"tool"\s*:|"args"\s*:|confirmation_id|reservation_id/i.test(content))
|
|
299
299
|
return facts;
|
|
300
300
|
const email = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i.exec(content);
|
|
301
301
|
if (email && canExtractStrongProfileFacts)
|
|
@@ -407,53 +407,63 @@ const safeParseToolPayload = (value) => {
|
|
|
407
407
|
};
|
|
408
408
|
const compactToolPayload = (value) => truncate(typeof value === 'string' ? value : safeStringify(value), 900);
|
|
409
409
|
const maybeToolResult = (tool, includeResults = true) => includeResults === false ? undefined : compactToolPayload(safeParseToolPayload(tool === null || tool === void 0 ? void 0 : tool.result));
|
|
410
|
-
const
|
|
411
|
-
const
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
if (
|
|
423
|
-
return
|
|
424
|
-
return
|
|
425
|
-
text: truncate(String(latest.content || latest.text || latest.memory || ''), 900),
|
|
426
|
-
at: latest.at || latest.created_at || latest.createdAt || nowIso(),
|
|
427
|
-
source: latest.source || 'recent_message',
|
|
428
|
-
};
|
|
410
|
+
const isToolName = (value = '') => /^[A-Za-z_][A-Za-z0-9_.:-]{1,127}$/.test(String(value || ''));
|
|
411
|
+
const pickRequiredToolFromAction = (text = '') => {
|
|
412
|
+
const next = String(text || '');
|
|
413
|
+
const protectedCall = /\bdo not call\s+([A-Za-z_][A-Za-z0-9_.:-]{1,127})\b[\s\S]*?\bcall\s+([A-Za-z_][A-Za-z0-9_.:-]{1,127})\b/i.exec(next);
|
|
414
|
+
if (protectedCall && isToolName(protectedCall[2]))
|
|
415
|
+
return protectedCall[2];
|
|
416
|
+
if (/\bdo not call\b/i.test(next))
|
|
417
|
+
return '';
|
|
418
|
+
const mustCall = /\bMUST\s+call\s+([A-Za-z_][A-Za-z0-9_.:-]{1,127})\b/i.exec(next);
|
|
419
|
+
if (mustCall && isToolName(mustCall[1]))
|
|
420
|
+
return mustCall[1];
|
|
421
|
+
const call = /\bcall\s+([A-Za-z_][A-Za-z0-9_.:-]{1,127})\b/i.exec(next);
|
|
422
|
+
if (call && isToolName(call[1]))
|
|
423
|
+
return call[1];
|
|
424
|
+
return '';
|
|
429
425
|
};
|
|
430
426
|
const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessages = [], includeResults = true) => {
|
|
431
427
|
const tools = Array.isArray(toolHistory) ? toolHistory : [];
|
|
432
428
|
const successfulTools = tools.filter((tool) => tool.ok !== false);
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
|
|
429
|
+
const failedTools = tools.filter((tool) => tool.ok === false);
|
|
430
|
+
const toolCountsByName = {};
|
|
431
|
+
const latestByName = {};
|
|
432
|
+
const failedByName = {};
|
|
433
|
+
const recentSequence = [];
|
|
434
|
+
for (let index = 0; index < tools.length; index += 1) {
|
|
435
|
+
const tool = tools[index];
|
|
436
|
+
const name = String(tool.name || 'unknown_tool');
|
|
437
|
+
const compactInput = compactToolPayload(safeParseToolPayload(tool.input));
|
|
438
|
+
const compactResult = maybeToolResult(tool, includeResults);
|
|
439
|
+
const reason = truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 260);
|
|
440
|
+
toolCountsByName[name] = (toolCountsByName[name] || 0) + 1;
|
|
441
|
+
latestByName[name] = {
|
|
442
|
+
name,
|
|
443
|
+
ok: tool.ok !== false,
|
|
444
|
+
at: tool.at || null,
|
|
445
|
+
input: compactInput,
|
|
446
|
+
result: compactResult,
|
|
447
|
+
reason,
|
|
448
|
+
source: tool.source || 'unknown',
|
|
449
|
+
};
|
|
450
|
+
recentSequence.push(cleanContextValue({
|
|
451
|
+
sequence: tool.sequence || index + 1,
|
|
452
|
+
tool: name,
|
|
453
|
+
status: tool.ok === false ? 'failed' : 'ok',
|
|
454
|
+
at: tool.at || null,
|
|
455
|
+
reason,
|
|
456
|
+
input: compactInput,
|
|
457
|
+
output: compactResult,
|
|
458
|
+
}));
|
|
459
|
+
if (tool.ok === false)
|
|
460
|
+
failedByName[name] = (failedByName[name] || 0) + 1;
|
|
461
|
+
}
|
|
439
462
|
const lastTool = tools[tools.length - 1] || null;
|
|
440
|
-
const lastReservation = reservations[reservations.length - 1] || null;
|
|
441
|
-
const lastConfirmation = confirmations[confirmations.length - 1] || null;
|
|
442
|
-
const hasPendingReservation = Boolean(lastReservation && (!lastConfirmation || String(lastReservation.at || '') > String(lastConfirmation.at || '')));
|
|
443
463
|
const blockedWithoutContext = [];
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
blockedWithoutContext.push('agenda_confirmar_agendamento');
|
|
448
|
-
const guidance = [];
|
|
449
|
-
if (!hasAvailability)
|
|
450
|
-
guidance.push('No availability result is known for this session; consult availability before reserving or confirming.');
|
|
451
|
-
else if (hasPendingReservation)
|
|
452
|
-
guidance.push('A pre-reservation exists after the latest confirmation; confirmation can use the latest pre-reservation context.');
|
|
453
|
-
else if (lastConfirmation)
|
|
454
|
-
guidance.push('The latest reservation appears confirmed; do not confirm again unless the user explicitly asks to repeat or change it.');
|
|
455
|
-
else if (hasAvailability)
|
|
456
|
-
guidance.push('Availability is known; if the user chooses one listed slot, reserve without repeating availability.');
|
|
464
|
+
const guidance = [
|
|
465
|
+
'Use tool_state and action_ledger as the source of truth for prior tool calls, inputs, outputs, failures and decisions. Domain-specific tool policy must come from the agent prompt, not from memory.',
|
|
466
|
+
];
|
|
457
467
|
return {
|
|
458
468
|
profile_complete: Boolean(profileFacts && profileFacts.name && profileFacts.company && profileFacts.email && profileFacts.phone),
|
|
459
469
|
last_tool: lastTool ? { name: lastTool.name, ok: lastTool.ok, at: lastTool.at } : null,
|
|
@@ -461,19 +471,22 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
|
|
|
461
471
|
total: tools.length,
|
|
462
472
|
ok: successfulTools.length,
|
|
463
473
|
failed: tools.length - successfulTools.length,
|
|
464
|
-
|
|
465
|
-
agenda_pre_reservar_horario: reservations.length,
|
|
466
|
-
agenda_confirmar_agendamento: confirmations.length,
|
|
474
|
+
by_name: toolCountsByName,
|
|
467
475
|
},
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
476
|
+
tool_state: {
|
|
477
|
+
total: tools.length,
|
|
478
|
+
ok: successfulTools.length,
|
|
479
|
+
failed: failedTools.length,
|
|
480
|
+
names: Object.keys(toolCountsByName),
|
|
481
|
+
counts_by_name: toolCountsByName,
|
|
482
|
+
failed_by_name: failedByName,
|
|
483
|
+
latest_by_name: latestByName,
|
|
484
|
+
recent_sequence: pruneByLimit(recentSequence, 20),
|
|
485
|
+
last_successful_tool: successfulTools.length ? {
|
|
486
|
+
name: successfulTools[successfulTools.length - 1].name,
|
|
487
|
+
at: successfulTools[successfulTools.length - 1].at || null,
|
|
488
|
+
result: maybeToolResult(successfulTools[successfulTools.length - 1], includeResults),
|
|
489
|
+
} : null,
|
|
477
490
|
},
|
|
478
491
|
blocked_without_context: Array.from(new Set(blockedWithoutContext)),
|
|
479
492
|
guidance,
|
|
@@ -488,6 +501,7 @@ const deriveActionLedger = (toolHistory = [], maxItems = 20, includeResults = tr
|
|
|
488
501
|
kind: 'tool_call',
|
|
489
502
|
name: tool.name || 'unknown_tool',
|
|
490
503
|
status: tool.ok === false ? 'failed' : 'ok',
|
|
504
|
+
reason: truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 260),
|
|
491
505
|
input: compactToolPayload(safeParseToolPayload(tool.input)),
|
|
492
506
|
result: maybeToolResult(tool, includeResults),
|
|
493
507
|
at: tool.at || null,
|
|
@@ -664,7 +678,7 @@ const toolHistoryFromMemory = (item) => {
|
|
|
664
678
|
source: 'semantic_fact',
|
|
665
679
|
};
|
|
666
680
|
}
|
|
667
|
-
const namedTool = meta.kind === 'tool_history' ? /(?:tool|ferramenta)\s+([A-Za-
|
|
681
|
+
const namedTool = meta.kind === 'tool_history' ? /(?:tool|ferramenta)\s+([A-Za-z_][A-Za-z0-9_.:-]{1,127})/i.exec(String(content || '')) : null;
|
|
668
682
|
if (namedTool) {
|
|
669
683
|
return {
|
|
670
684
|
id: meta.id || meta.call_id || meta.callId || '',
|
|
@@ -677,26 +691,6 @@ const toolHistoryFromMemory = (item) => {
|
|
|
677
691
|
source: 'semantic_named_tool',
|
|
678
692
|
};
|
|
679
693
|
}
|
|
680
|
-
const text = meta.kind === 'tool_history' ? String(content || '').toLowerCase() : '';
|
|
681
|
-
const inferredAgendaTool = text.includes('confirm') && text.includes('agendamento')
|
|
682
|
-
? 'agenda_confirmar_agendamento'
|
|
683
|
-
: text.includes('pré-reserva') || text.includes('pre-reserva') || text.includes('pre reserva') || text.includes('pré reserva')
|
|
684
|
-
? 'agenda_pre_reservar_horario'
|
|
685
|
-
: (text.includes('horários disponíveis') || text.includes('horarios disponiveis') || text.includes('available') || text.includes('disponibilidade')) && text.includes('agenda')
|
|
686
|
-
? 'agenda_consultar_disponibilidade'
|
|
687
|
-
: '';
|
|
688
|
-
if (inferredAgendaTool) {
|
|
689
|
-
return {
|
|
690
|
-
id: meta.id || meta.call_id || meta.callId || '',
|
|
691
|
-
turnId: meta.turn_id || meta.turnId || '',
|
|
692
|
-
name: inferredAgendaTool,
|
|
693
|
-
input: '',
|
|
694
|
-
ok: true,
|
|
695
|
-
result: truncate(content || '', 1000),
|
|
696
|
-
at: meta.at || item.created_at || item.createdAt || nowIso(),
|
|
697
|
-
source: 'semantic_inference',
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
694
|
if (meta.kind !== 'tool_history')
|
|
701
695
|
return null;
|
|
702
696
|
const name = meta.name || meta.tool || meta.toolName;
|
|
@@ -1246,17 +1240,18 @@ const normalizeIntentText = (value = '') => String(value || '')
|
|
|
1246
1240
|
.toLowerCase()
|
|
1247
1241
|
.normalize('NFD')
|
|
1248
1242
|
.replace(/[\u0300-\u036f]/g, '');
|
|
1249
|
-
const
|
|
1243
|
+
const hasCommitIntent = (value = '') => /\b(confirm\w*|confim\w*|cnfirm\w*|cofnirm\w*|ocnfi\w*|ocnfia\w*|fechar|aprovar|finalizar|prosseguir|continuar|sim)\b/.test(normalizeIntentText(value));
|
|
1250
1244
|
const isConversationRecallQuery = (value = '') => {
|
|
1251
1245
|
const text = normalizeIntentText(value);
|
|
1252
1246
|
return /\b(minha|qual|quais|o que|oq|voce|vc|lembra|lembrar|sabe|diga|fala)\b.{0,80}\b(ultima|ultimas|msg|msgs|mensagem|mensagens|pergunta|perguntei|falei|disse)\b/.test(text)
|
|
1253
1247
|
|| /\b(o que|oq)\b.{0,30}\b(eu)\b.{0,30}\b(falei|disse|perguntei|mandei)\b/.test(text)
|
|
1254
1248
|
|| /\b(nao|nao)\b.{0,20}\b(lembra|sabe)\b.{0,60}\b(msg|msgs|mensagem|pergunta|falei|disse|perguntei)\b/.test(text);
|
|
1255
1249
|
};
|
|
1256
|
-
const
|
|
1250
|
+
const isOperationalStatusQuery = (value = '') => {
|
|
1257
1251
|
const text = normalizeIntentText(value);
|
|
1258
|
-
return /\b(
|
|
1259
|
-
|| /\b(
|
|
1252
|
+
return /\b(quais?|qual|lista|mostra|resume|status|estado|historico|hist[oó]rico|outputs?|retornos?|resultado|resultados?)\b.{0,80}\b(tools?|ferramentas?|apis?|a[cç][oõ]es?|chamadas?|executadas?|usadas?)\b/.test(text)
|
|
1253
|
+
|| /\b(tools?|ferramentas?|apis?|a[cç][oõ]es?|chamadas?|executadas?|usadas?)\b.{0,80}\b(quais?|qual|lista|mostra|resume|status|estado|historico|hist[oó]rico|outputs?|retornos?|resultado|resultados?)\b/.test(text)
|
|
1254
|
+
|| /\b(ja|ainda|tinha|tenho|foi|esta|ta)\b.{0,60}\b(feito|feita|executad\w*|chamad\w*|concluid\w*|confirm\w*|criad\w*|atualizad\w*|agend\w*|agned\w*|ganed\w*|marc\w*|reserv\w*)\b/.test(text);
|
|
1260
1255
|
};
|
|
1261
1256
|
const recentUserMessages = (recentMessages = []) => [...(recentMessages || [])]
|
|
1262
1257
|
.filter((msg) => /^(user|human)$/i.test(String(msg.role || '')))
|
|
@@ -1314,8 +1309,8 @@ const buildConversationFrame = ({ query = '', recentMessages = [], workingMemory
|
|
|
1314
1309
|
};
|
|
1315
1310
|
const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalState = {}, workingMemory = {} }) => {
|
|
1316
1311
|
const recall = isConversationRecallQuery(query);
|
|
1317
|
-
const
|
|
1318
|
-
if (!recall && !
|
|
1312
|
+
const operationalStatus = isOperationalStatusQuery(query);
|
|
1313
|
+
if (!recall && !operationalStatus)
|
|
1319
1314
|
return null;
|
|
1320
1315
|
const previousUser = previousUserMessageForQuery(query, recentMessages);
|
|
1321
1316
|
const firstUser = firstUserMessageFromConversation(recentMessages);
|
|
@@ -1323,31 +1318,22 @@ const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalSta
|
|
|
1323
1318
|
.filter((msg) => normalizeIntentText(msg.content).trim() !== normalizeIntentText(query).trim())
|
|
1324
1319
|
.slice(0, 3)
|
|
1325
1320
|
.map((msg) => ({ role: msg.role, content: truncate(msg.content, 500), at: msg.at }));
|
|
1326
|
-
const
|
|
1321
|
+
const toolState = (operationalState || {}).tool_state || {};
|
|
1327
1322
|
const previousUserMessage = previousUser ? truncate(previousUser.content, 500) : previousUserFallbackFromWorkingMemory(query, workingMemory);
|
|
1328
1323
|
const focus = cleanContextValue({
|
|
1329
1324
|
current_user_request: truncate(query, 500),
|
|
1330
|
-
intent: recall ? 'conversation_recall' : '
|
|
1325
|
+
intent: recall ? 'conversation_recall' : 'tool_or_status_question',
|
|
1331
1326
|
first_user_message: firstUser ? truncate(firstUser.content, 500) : null,
|
|
1332
1327
|
previous_user_message: previousUserMessage,
|
|
1333
1328
|
recent_user_messages: users,
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
? 'pending_pre_reservation'
|
|
1339
|
-
: agenda.has_pre_reservation
|
|
1340
|
-
? 'pre_reserved'
|
|
1341
|
-
: agenda.has_availability
|
|
1342
|
-
? 'availability_known_not_scheduled'
|
|
1343
|
-
: 'none',
|
|
1344
|
-
has_confirmation: Boolean(agenda.has_confirmation && !agenda.has_pending_pre_reservation),
|
|
1345
|
-
has_pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
|
|
1346
|
-
has_availability: Boolean(agenda.has_availability),
|
|
1329
|
+
tool_status: operationalStatus ? {
|
|
1330
|
+
executed_tools: toolState.names || [],
|
|
1331
|
+
last_successful_tool: toolState.last_successful_tool || null,
|
|
1332
|
+
failed_by_name: toolState.failed_by_name || {},
|
|
1347
1333
|
} : undefined,
|
|
1348
1334
|
instruction: recall
|
|
1349
|
-
? 'Answer the user meta-question from previous_user_message/recent_user_messages and the chronological transcript. Do not
|
|
1350
|
-
: 'Answer
|
|
1335
|
+
? 'Answer the user meta-question from previous_user_message/recent_user_messages and the chronological transcript. Do not call tools for recall-only questions.'
|
|
1336
|
+
: 'Answer status questions from tool_state, tool_history and action_ledger. Do not call tools unless the agent prompt requires it.',
|
|
1351
1337
|
});
|
|
1352
1338
|
if (!previousUserMessage)
|
|
1353
1339
|
focus.previous_user_message = null;
|
|
@@ -1355,47 +1341,29 @@ const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalSta
|
|
|
1355
1341
|
};
|
|
1356
1342
|
const buildActionDirective = ({ workingMemory = {}, operationalState = {} }) => {
|
|
1357
1343
|
const next = String((workingMemory || {}).next_expected_action || '');
|
|
1358
|
-
const
|
|
1359
|
-
if (!
|
|
1344
|
+
const tool = pickRequiredToolFromAction(next);
|
|
1345
|
+
if (!tool || !/\bcall\b|\bMUST\b/i.test(next))
|
|
1360
1346
|
return null;
|
|
1361
|
-
const
|
|
1362
|
-
const
|
|
1347
|
+
const toolState = (operationalState || {}).tool_state || {};
|
|
1348
|
+
const latestForTool = ((toolState.latest_by_name || {})[tool]) || null;
|
|
1363
1349
|
return cleanContextValue({
|
|
1364
1350
|
required_tool: tool,
|
|
1365
1351
|
next_expected_action: next,
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
has_confirmation: Boolean(agenda.has_confirmation),
|
|
1352
|
+
tool_state: {
|
|
1353
|
+
last_tool: (operationalState || {}).last_tool || null,
|
|
1354
|
+
required_tool_last_result: latestForTool,
|
|
1355
|
+
counts_by_name: toolState.counts_by_name || {},
|
|
1356
|
+
failed_by_name: toolState.failed_by_name || {},
|
|
1372
1357
|
},
|
|
1373
|
-
instruction: `
|
|
1358
|
+
instruction: `If the agent prompt requires this action, call ${tool} now using tool_state/action_ledger as evidence. Do not infer domain policy from memory; memory only provides prior conversation, decisions, inputs and outputs.`,
|
|
1374
1359
|
});
|
|
1375
1360
|
};
|
|
1376
1361
|
const inferToolGuard = ({ query, recentMessages, toolHistory, vectorMemories }) => {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
const hasPreReservationTool = names.has('agenda_pre_reservar_horario');
|
|
1383
|
-
const hasAnyContext = names.size > 0 || hasRecentContext || hasVectorContext;
|
|
1384
|
-
const blocked = [];
|
|
1385
|
-
const reasons = [];
|
|
1386
|
-
if (!hasAnyContext) {
|
|
1387
|
-
blocked.push('agenda_pre_reservar_horario', 'agenda_confirmar_agendamento');
|
|
1388
|
-
reasons.push('no prior availability or pre-reservation context was found for this session');
|
|
1389
|
-
}
|
|
1390
|
-
if (hasConfirmIntent(text) && !hasPreReservationTool) {
|
|
1391
|
-
blocked.push('agenda_confirmar_agendamento');
|
|
1392
|
-
reasons.push('confirmation requested but no structured agenda_pre_reservar_horario tool result was found; assistant text or vector memories are not enough to confirm');
|
|
1393
|
-
}
|
|
1394
|
-
if ((/pre.?reserv|pre.?reserv|reservar/.test(text)) && !hasAvailabilityTool) {
|
|
1395
|
-
blocked.push('agenda_pre_reservar_horario');
|
|
1396
|
-
reasons.push('reservation requested but no structured agenda_consultar_disponibilidade tool result was found');
|
|
1397
|
-
}
|
|
1398
|
-
return { blockedTools: Array.from(new Set(blocked)), reasons };
|
|
1362
|
+
return {
|
|
1363
|
+
blockedTools: [],
|
|
1364
|
+
reasons: [],
|
|
1365
|
+
instruction: 'Memory does not block domain tools by hardcoded rules. The agent prompt decides which tool to call; use tool_state/tool_history/action_ledger to verify prior inputs, outputs and failures.',
|
|
1366
|
+
};
|
|
1399
1367
|
};
|
|
1400
1368
|
const inferUserIntent = (query = '', recentMessages = []) => {
|
|
1401
1369
|
const latestUser = [...(recentMessages || [])].reverse().find((msg) => /^(user|human)$/i.test(String(msg.role || '')));
|
|
@@ -1404,20 +1372,14 @@ const inferUserIntent = (query = '', recentMessages = []) => {
|
|
|
1404
1372
|
const normalizedQuery = normalizeIntentText(query).trim();
|
|
1405
1373
|
if (isConversationRecallQuery(query))
|
|
1406
1374
|
return 'conversation_recall';
|
|
1407
|
-
if (
|
|
1408
|
-
return '
|
|
1375
|
+
if (isOperationalStatusQuery(query))
|
|
1376
|
+
return 'operational_status_question';
|
|
1409
1377
|
if (/^(ok|sim|pode|pode sim|isso|isso mesmo|confirmo|confirmar)$/.test(normalizedQuery))
|
|
1410
1378
|
return 'affirm';
|
|
1411
|
-
if (
|
|
1412
|
-
return '
|
|
1413
|
-
if (
|
|
1414
|
-
return '
|
|
1415
|
-
if (/\b(pre.?reserv|pre.?reserv|reservar|segurar|marcar)\b/.test(text))
|
|
1416
|
-
return 'pre_reserve';
|
|
1417
|
-
if (/\b(disponibilidade|horarios?|agenda(?:r|mento)?|quando pode|tem vaga|quero agendar|gostaria de agendar|preciso agendar)\b/.test(text))
|
|
1418
|
-
return 'check_availability';
|
|
1419
|
-
if (/\b(cancel|desmarcar|remarcar|alterar|mudar)\b/.test(text))
|
|
1420
|
-
return 'change_or_cancel';
|
|
1379
|
+
if (hasCommitIntent(text))
|
|
1380
|
+
return 'commit_or_continue';
|
|
1381
|
+
if (/\b(buscar|busca|criar|cria|atualizar|atualiza|consultar|consulta|reservar|reserva|agendar|agenda|abrir|abre|cancelar|cancela|enviar|envia|gerar|gera|validar|valida|processar|processa|executar|executa)\b/.test(text))
|
|
1382
|
+
return 'tool_action_candidate';
|
|
1421
1383
|
if (/\b(meu nome|email|telefone|empresa|prefiro|preferencia)\b/.test(text))
|
|
1422
1384
|
return 'profile_update';
|
|
1423
1385
|
if (text.trim())
|
|
@@ -1425,41 +1387,14 @@ const inferUserIntent = (query = '', recentMessages = []) => {
|
|
|
1425
1387
|
return 'unknown';
|
|
1426
1388
|
};
|
|
1427
1389
|
const deriveNextExpectedAction = (intent, operationalState = {}) => {
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
if (agenda.has_pending_pre_reservation)
|
|
1431
|
-
return 'MUST call agenda_confirmar_agendamento now; do not ask the same confirmation question again';
|
|
1432
|
-
if (agenda.has_availability)
|
|
1433
|
-
return 'MUST call agenda_pre_reservar_horario now using the selected or latest discussed slot; do not ask again';
|
|
1434
|
-
return 'call agenda_consultar_disponibilidade before reserving';
|
|
1435
|
-
}
|
|
1436
|
-
if (intent === 'confirm') {
|
|
1437
|
-
if (agenda.has_pending_pre_reservation)
|
|
1438
|
-
return 'MUST call agenda_confirmar_agendamento now using the latest pre-reservation context; do not ask again';
|
|
1439
|
-
if (agenda.has_availability)
|
|
1440
|
-
return 'MUST call agenda_pre_reservar_horario first using the selected or latest discussed slot; do not ask for permission again';
|
|
1441
|
-
return 'do not call agenda_confirmar_agendamento; call agenda_consultar_disponibilidade first because no availability is known';
|
|
1442
|
-
}
|
|
1443
|
-
if (intent === 'select_slot') {
|
|
1444
|
-
if (agenda.has_availability)
|
|
1445
|
-
return 'MUST call agenda_pre_reservar_horario now using the selected slot; do not ask for confirmation before pre-reserving';
|
|
1446
|
-
return 'call agenda_consultar_disponibilidade before pre-reserving';
|
|
1447
|
-
}
|
|
1448
|
-
if (intent === 'pre_reserve') {
|
|
1449
|
-
if (agenda.has_availability)
|
|
1450
|
-
return 'MUST call agenda_pre_reservar_horario now using one of the known available slots';
|
|
1451
|
-
return 'call agenda_consultar_disponibilidade before pre-reserving';
|
|
1452
|
-
}
|
|
1453
|
-
if (intent === 'check_availability')
|
|
1454
|
-
return agenda.has_availability ? 'reuse known availability unless the user asks to refresh' : 'call agenda_consultar_disponibilidade';
|
|
1455
|
-
if (intent === 'change_or_cancel')
|
|
1456
|
-
return 'inspect current reservation state before changing or cancelling';
|
|
1390
|
+
if (['affirm', 'commit_or_continue', 'tool_action_candidate'].includes(intent))
|
|
1391
|
+
return 'continue according to the agent prompt using conversation_frame, tool_state, tool_history and action_ledger; do not apply domain-specific memory rules';
|
|
1457
1392
|
if (intent === 'profile_update')
|
|
1458
1393
|
return 'save stable profile facts and continue the conversation';
|
|
1459
1394
|
if (intent === 'conversation_recall')
|
|
1460
|
-
return 'answer directly using
|
|
1461
|
-
if (intent === '
|
|
1462
|
-
return 'answer
|
|
1395
|
+
return 'answer directly using previous_user_message and conversation_history_chronological; do not call tools for recall-only questions';
|
|
1396
|
+
if (intent === 'operational_status_question')
|
|
1397
|
+
return 'answer status questions from tool_state, tool_history and action_ledger; do not call tools unless the agent prompt requires it';
|
|
1463
1398
|
return 'answer using retrieved context and avoid unnecessary tool calls';
|
|
1464
1399
|
};
|
|
1465
1400
|
const deriveWorkingMemory = ({ query = '', profileFacts = {}, recentMessages = [], toolHistory = [], operationalState = {}, previous = {} }) => {
|
|
@@ -1484,11 +1419,11 @@ const deriveWorkingMemory = ({ query = '', profileFacts = {}, recentMessages = [
|
|
|
1484
1419
|
};
|
|
1485
1420
|
};
|
|
1486
1421
|
const deriveDecisionState = ({ query = '', toolHistory = [], operationalState = {}, workingMemory = {} }) => {
|
|
1487
|
-
const agenda = operationalState.agenda_state || {};
|
|
1488
1422
|
const decisions = [];
|
|
1489
1423
|
const doNotRepeatTools = [];
|
|
1490
1424
|
const intent = workingMemory.last_user_intent || inferUserIntent(query, []);
|
|
1491
1425
|
const now = nowIso();
|
|
1426
|
+
const toolState = operationalState.tool_state || {};
|
|
1492
1427
|
const pushDecision = (id, decision, reason, appliesTo, extra = {}) => {
|
|
1493
1428
|
decisions.push({
|
|
1494
1429
|
id,
|
|
@@ -1501,41 +1436,25 @@ const deriveDecisionState = ({ query = '', toolHistory = [], operationalState =
|
|
|
1501
1436
|
created_at: extra.created_at || now,
|
|
1502
1437
|
valid_until: extra.valid_until || null,
|
|
1503
1438
|
superseded_by: extra.superseded_by || null,
|
|
1439
|
+
tool: extra.tool,
|
|
1504
1440
|
});
|
|
1505
1441
|
};
|
|
1506
|
-
|
|
1507
|
-
doNotRepeatTools.push(
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
pushDecision(
|
|
1513
|
-
}
|
|
1514
|
-
if (agenda.has_confirmation && !agenda.has_pending_pre_reservation) {
|
|
1515
|
-
doNotRepeatTools.push('agenda_confirmar_agendamento');
|
|
1516
|
-
pushDecision('agenda_confirmation_done', 'do not confirm again unless the user explicitly asks to repeat or change it', 'latest reservation appears confirmed', 'agenda_flow', { confidence: 0.9 });
|
|
1517
|
-
}
|
|
1518
|
-
const reservationStatus = agenda.has_confirmation && !agenda.has_pending_pre_reservation
|
|
1519
|
-
? 'confirmed'
|
|
1520
|
-
: agenda.has_pending_pre_reservation
|
|
1521
|
-
? 'pending_pre_reservation'
|
|
1522
|
-
: agenda.has_pre_reservation
|
|
1523
|
-
? 'pre_reserved'
|
|
1524
|
-
: agenda.has_availability
|
|
1525
|
-
? 'availability_known'
|
|
1526
|
-
: 'none';
|
|
1442
|
+
for (const name of Object.keys(toolState.counts_by_name || {})) {
|
|
1443
|
+
doNotRepeatTools.push(name);
|
|
1444
|
+
}
|
|
1445
|
+
if (toolState.last_successful_tool)
|
|
1446
|
+
pushDecision('last_successful_tool_recorded', `latest successful tool is ${toolState.last_successful_tool.name}`, 'tool_state records successful tool execution', 'tool_orchestration', { confidence: 0.9, tool: toolState.last_successful_tool.name });
|
|
1447
|
+
for (const [name, count] of Object.entries(toolState.failed_by_name || {}))
|
|
1448
|
+
pushDecision(`tool_failed_${name}`, `${name} failed ${count} time(s); inspect tool output before retrying`, 'tool_state records failed tool execution', 'tool_orchestration', { confidence: 0.86, tool: name });
|
|
1527
1449
|
return {
|
|
1528
1450
|
active_decisions: decisions,
|
|
1529
1451
|
do_not_repeat_tools: Array.from(new Set(doNotRepeatTools)),
|
|
1530
1452
|
current_intent: intent,
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
reschedule_requested: intent === 'change_or_cancel' && Boolean(agenda.has_confirmation || agenda.has_pre_reservation),
|
|
1537
|
-
cancel_requested: /\b(cancelar|cancele|desmarcar|remover)\b/i.test(String(query || '')),
|
|
1538
|
-
reservation_status: reservationStatus,
|
|
1453
|
+
tool_decision_state: {
|
|
1454
|
+
executed_tools: Object.keys(toolState.counts_by_name || {}),
|
|
1455
|
+
failed_tools: Object.keys(toolState.failed_by_name || {}),
|
|
1456
|
+
last_successful_tool: toolState.last_successful_tool || null,
|
|
1457
|
+
do_not_repeat_tools: Array.from(new Set(doNotRepeatTools)),
|
|
1539
1458
|
evaluated_at: now,
|
|
1540
1459
|
},
|
|
1541
1460
|
conflict_policy: 'prefer active decisions with newer timestamps; ignore superseded or expired memories unless auditing',
|
|
@@ -1586,7 +1505,12 @@ const deriveMemoryCompression = ({ recentMessages = [], toolHistory = [], profil
|
|
|
1586
1505
|
session_summary: {
|
|
1587
1506
|
messages: lastMessages.length,
|
|
1588
1507
|
tools: lastTools,
|
|
1589
|
-
|
|
1508
|
+
tool_state: (operationalState || {}).tool_state ? {
|
|
1509
|
+
names: ((operationalState.tool_state || {}).names || []).slice(0, maxItems),
|
|
1510
|
+
counts_by_name: (operationalState.tool_state || {}).counts_by_name || {},
|
|
1511
|
+
failed_by_name: (operationalState.tool_state || {}).failed_by_name || {},
|
|
1512
|
+
last_successful_tool: (operationalState.tool_state || {}).last_successful_tool || null,
|
|
1513
|
+
} : {},
|
|
1590
1514
|
},
|
|
1591
1515
|
entity_summary: profile,
|
|
1592
1516
|
workflow_summary: {
|
|
@@ -1632,7 +1556,6 @@ const deriveContextHealth = ({ userId = '', project = '', vectorMemories = [], r
|
|
|
1632
1556
|
score += weights[key] || 0;
|
|
1633
1557
|
}
|
|
1634
1558
|
const missing = Object.entries(checks).filter(([, ok]) => !ok).map(([key]) => key);
|
|
1635
|
-
const agenda = operationalState.agenda_state || {};
|
|
1636
1559
|
return {
|
|
1637
1560
|
kind: 'tembory.context_health.v1',
|
|
1638
1561
|
namespace: userId || null,
|
|
@@ -1647,11 +1570,10 @@ const deriveContextHealth = ({ userId = '', project = '', vectorMemories = [], r
|
|
|
1647
1570
|
tool_history: Array.isArray(toolHistory) ? toolHistory.length : 0,
|
|
1648
1571
|
active_decisions: Array.isArray(decisionState.active_decisions) ? decisionState.active_decisions.length : 0,
|
|
1649
1572
|
},
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
has_pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
|
|
1573
|
+
tool_state: {
|
|
1574
|
+
names: (((operationalState || {}).tool_state || {}).names || []).slice(0, 12),
|
|
1575
|
+
last_successful_tool: (((operationalState || {}).tool_state || {}).last_successful_tool) || null,
|
|
1576
|
+
failed_by_name: (((operationalState || {}).tool_state || {}).failed_by_name) || {},
|
|
1655
1577
|
},
|
|
1656
1578
|
generated_at: nowIso(),
|
|
1657
1579
|
};
|
|
@@ -1662,13 +1584,13 @@ const contextMemoryText = (memory, max = 700) => {
|
|
|
1662
1584
|
if (tools.length) {
|
|
1663
1585
|
const names = tools.map((tool) => `${tool.name || 'tool'}:${tool.ok === false ? 'failed' : 'ok'}`).join(', ');
|
|
1664
1586
|
const ids = tools.map((tool) => tool.result || tool.input || '').join(' ');
|
|
1665
|
-
const idMatch = /(
|
|
1587
|
+
const idMatch = /([A-Za-z_][A-Za-z0-9_.:-]*id)["':\s]+([A-Za-z0-9_.:-]+)/i.exec(ids);
|
|
1666
1588
|
return truncate(`[tool_events_extracted] ${names}${idMatch ? ` ${idMatch[1]}=${idMatch[2]}` : ''}. See Tool history for structured details.`, max);
|
|
1667
1589
|
}
|
|
1668
1590
|
return truncate(text, max);
|
|
1669
1591
|
};
|
|
1670
1592
|
const approxTokenCount = (text) => Math.ceil(String(text || '').length / 4);
|
|
1671
|
-
const importantJsonFields = ['
|
|
1593
|
+
const importantJsonFields = ['id', 'status', 'state', 'next_step', 'action', 'intent', 'tool', 'input', 'output', 'result', 'error', 'message', 'reason', 'customer_id', 'user_id', 'lead_id', 'ticket_id', 'charge_id'];
|
|
1672
1594
|
const pickImportantFields = (value) => {
|
|
1673
1595
|
if (value === null || value === undefined)
|
|
1674
1596
|
return {};
|
|
@@ -1698,9 +1620,7 @@ const compactToolResult = (result, max = 360) => {
|
|
|
1698
1620
|
return undefined;
|
|
1699
1621
|
try {
|
|
1700
1622
|
const parsed = JSON.parse(text);
|
|
1701
|
-
|
|
1702
|
-
if (Object.keys(picked).length)
|
|
1703
|
-
return truncate(safeStringify(picked), max);
|
|
1623
|
+
return truncate(safeStringify(parsed), max);
|
|
1704
1624
|
}
|
|
1705
1625
|
catch { }
|
|
1706
1626
|
const picked = {};
|
|
@@ -1719,8 +1639,9 @@ const compactToolHistoryForAgent = (toolHistory = [], maxItems = 6, includeResul
|
|
|
1719
1639
|
name: tool.name,
|
|
1720
1640
|
status: tool.ok === false ? 'failed' : 'ok',
|
|
1721
1641
|
at: tool.at,
|
|
1722
|
-
|
|
1723
|
-
|
|
1642
|
+
reason: truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 140),
|
|
1643
|
+
input: truncate(String(tool.input || ''), 140) || undefined,
|
|
1644
|
+
result: includeResults ? compactToolResult(tool.result, 240) : undefined,
|
|
1724
1645
|
}));
|
|
1725
1646
|
const cleanContextValue = (value) => {
|
|
1726
1647
|
if (Array.isArray(value)) {
|
|
@@ -1764,7 +1685,7 @@ const compactDecisionStateForAgent = (state = {}) => cleanContextValue({
|
|
|
1764
1685
|
at: decision.at || decision.updated_at,
|
|
1765
1686
|
})),
|
|
1766
1687
|
do_not_repeat_tools: state.do_not_repeat_tools,
|
|
1767
|
-
|
|
1688
|
+
tool_decision_state: state.tool_decision_state,
|
|
1768
1689
|
latest_tool: state.latest_tool ? cleanContextValue({
|
|
1769
1690
|
name: state.latest_tool.name,
|
|
1770
1691
|
status: state.latest_tool.status || (state.latest_tool.ok === false ? 'failed' : 'ok'),
|
|
@@ -1773,19 +1694,21 @@ const compactDecisionStateForAgent = (state = {}) => cleanContextValue({
|
|
|
1773
1694
|
conflict_policy: state.conflict_policy,
|
|
1774
1695
|
});
|
|
1775
1696
|
const compactOperationalStateForAgent = (state = {}) => {
|
|
1776
|
-
const
|
|
1697
|
+
const counts = state.tool_counts || {};
|
|
1698
|
+
const toolState = state.tool_state || {};
|
|
1777
1699
|
return {
|
|
1778
1700
|
last_tool: state.last_tool || null,
|
|
1779
|
-
tool_counts:
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
has_confirmation: Boolean(agenda.has_confirmation),
|
|
1784
|
-
has_pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
|
|
1785
|
-
latest_availability: compactToolResult(agenda.latest_availability_result),
|
|
1786
|
-
latest_pre_reservation: compactToolResult(agenda.latest_pre_reservation_result),
|
|
1787
|
-
latest_confirmation: compactToolResult(agenda.latest_confirmation_result),
|
|
1701
|
+
tool_counts: {
|
|
1702
|
+
total: counts.total || 0,
|
|
1703
|
+
ok: counts.ok || 0,
|
|
1704
|
+
failed: counts.failed || 0,
|
|
1788
1705
|
},
|
|
1706
|
+
tool_state: (toolState.names || []).length ? {
|
|
1707
|
+
names: toolState.names,
|
|
1708
|
+
counts_by_name: toolState.counts_by_name || {},
|
|
1709
|
+
failed_by_name: toolState.failed_by_name || {},
|
|
1710
|
+
last_successful_tool: toolState.last_successful_tool || undefined,
|
|
1711
|
+
} : undefined,
|
|
1789
1712
|
blocked_without_context: state.blocked_without_context || [],
|
|
1790
1713
|
guidance: (state.guidance || []).slice(0, 3),
|
|
1791
1714
|
};
|
|
@@ -1793,12 +1716,6 @@ const compactOperationalStateForAgent = (state = {}) => {
|
|
|
1793
1716
|
const compactMemoryCompressionForAgent = (compression = {}) => ({
|
|
1794
1717
|
turn_summary: (compression.turn_summary || []).slice(-3),
|
|
1795
1718
|
session_tools: (((compression.session_summary || {}).tools) || []).slice(-6),
|
|
1796
|
-
agenda_state: ((compression.session_summary || {}).agenda_state) ? {
|
|
1797
|
-
has_availability: Boolean(compression.session_summary.agenda_state.has_availability),
|
|
1798
|
-
has_pre_reservation: Boolean(compression.session_summary.agenda_state.has_pre_reservation),
|
|
1799
|
-
has_confirmation: Boolean(compression.session_summary.agenda_state.has_confirmation),
|
|
1800
|
-
has_pending_pre_reservation: Boolean(compression.session_summary.agenda_state.has_pending_pre_reservation),
|
|
1801
|
-
} : undefined,
|
|
1802
1719
|
active_memory_count: ((compression.workflow_summary || {}).active_memory_count) || 0,
|
|
1803
1720
|
});
|
|
1804
1721
|
const compactActionLedgerForAgent = (ledger = [], maxItems = 6, includeResults = true) => pruneByLimit(ledger || [], maxItems).map((item) => cleanContextValue({
|
|
@@ -1807,7 +1724,9 @@ const compactActionLedgerForAgent = (ledger = [], maxItems = 6, includeResults =
|
|
|
1807
1724
|
tool: item.tool || item.name,
|
|
1808
1725
|
status: item.status,
|
|
1809
1726
|
at: item.at,
|
|
1810
|
-
|
|
1727
|
+
reason: item.reason,
|
|
1728
|
+
input: item.input,
|
|
1729
|
+
result: includeResults ? compactToolResult(item.result, 180) : undefined,
|
|
1811
1730
|
}));
|
|
1812
1731
|
const compactEntityTimelineForAgent = (timeline = [], maxItems = 8) => pruneByLimit(timeline || [], maxItems).map((item) => cleanContextValue({
|
|
1813
1732
|
entity: item.entity || item.source || item.name,
|
|
@@ -2025,8 +1944,8 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
|
|
|
2025
1944
|
section: 'context_header',
|
|
2026
1945
|
title: 'Tembory context',
|
|
2027
1946
|
value: compactForAgent || compactStateSections
|
|
2028
|
-
? 'Read-only memory. Conversation frame is authoritative for
|
|
2029
|
-
: 'Use this context as read-only memory.
|
|
1947
|
+
? 'Read-only operational memory. Conversation frame is authoritative for user/assistant transcript. Tool history/action ledger are authoritative for tool calls, reasons, inputs, outputs, status and timestamps. The agent prompt owns domain policy and decides which tool to call.'
|
|
1948
|
+
: 'Use this context as read-only operational memory. Do not mention internal section names to the user. Conversation frame is authoritative for the chronological user/assistant transcript. Tool history and action ledger are authoritative for what tools were called, why they were called, what input was sent, what output returned, whether they failed, and when they ran. Memory must not invent domain-specific tool rules; the agent prompt owns tool orchestration policy.',
|
|
2030
1949
|
});
|
|
2031
1950
|
}
|
|
2032
1951
|
sections.push({
|
|
@@ -2162,9 +2081,9 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
|
|
|
2162
2081
|
value: {
|
|
2163
2082
|
blocked_tools: guard.blockedTools,
|
|
2164
2083
|
reasons: guard.reasons,
|
|
2165
|
-
instruction: guard.blockedTools.length
|
|
2084
|
+
instruction: guard.instruction || (guard.blockedTools.length
|
|
2166
2085
|
? `Do not call these tools now: ${guard.blockedTools.join(', ')}. Ask for the missing context or execute the prerequisite step first.`
|
|
2167
|
-
: 'No downstream tool is blocked by missing memory prerequisites.',
|
|
2086
|
+
: 'No downstream tool is blocked by missing memory prerequisites.'),
|
|
2168
2087
|
},
|
|
2169
2088
|
});
|
|
2170
2089
|
sections.push({
|
|
@@ -3690,7 +3609,6 @@ exports.__private = {
|
|
|
3690
3609
|
explicitToolHistoryItemsFromMemory,
|
|
3691
3610
|
toolHistoryFromMemory,
|
|
3692
3611
|
cleanAssistantTranscriptText,
|
|
3693
|
-
availabilityFromRecentMessages,
|
|
3694
3612
|
buildActionDirective,
|
|
3695
3613
|
recentMessageFromMemory,
|
|
3696
3614
|
previousUserFallbackFromWorkingMemory,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-tembory",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
4
4
|
"description": "Tembory node for n8n AI Agents with profile, tools, timeline, graph and semantic memory",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://tembory.com",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"test": "node --test test/*.test.js",
|
|
26
26
|
"smoke:n8n:multiturn": "node scripts/smoke-n8n-multiturn-tools.js",
|
|
27
27
|
"simulate:agent-context": "node scripts/simulate-agent-context.js",
|
|
28
|
-
"
|
|
28
|
+
"simulate:n8n-agent": "node scripts/simulate-n8n-agent-agenda.js",
|
|
29
|
+
"prepublishOnly": "npm test && npm run simulate:n8n-agent"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"dist",
|
|
@@ -27,53 +27,49 @@ const core = require('../dist/nodes/Mem0/Mem0Memory.node.js').__private;
|
|
|
27
27
|
|
|
28
28
|
const toolHistory = [
|
|
29
29
|
{
|
|
30
|
-
id: '
|
|
31
|
-
name: '
|
|
32
|
-
input: '',
|
|
30
|
+
id: 'tool_crm_lookup',
|
|
31
|
+
name: 'crm_buscar_cliente',
|
|
32
|
+
input: '{"phone":"11999998888"}',
|
|
33
33
|
ok: true,
|
|
34
34
|
at: '2026-05-14T05:34:12.460Z',
|
|
35
|
-
source: '
|
|
36
|
-
|
|
35
|
+
source: 'structured_tool_event',
|
|
36
|
+
reason: 'agent prompt requested CRM lookup by phone',
|
|
37
|
+
result: '{"customer_id":"CUS-TEMBORY-LAB-001","name":"Lucas","phone":"11999998888"}',
|
|
37
38
|
},
|
|
38
39
|
{
|
|
39
|
-
id: '
|
|
40
|
-
name: '
|
|
41
|
-
input: '',
|
|
40
|
+
id: 'tool_charge',
|
|
41
|
+
name: 'pagamento_criar_cobranca',
|
|
42
|
+
input: '{"customer_id":"CUS-TEMBORY-LAB-001","amount_cents":9700}',
|
|
42
43
|
ok: true,
|
|
43
44
|
at: '2026-05-14T05:34:34.017Z',
|
|
44
|
-
source: '
|
|
45
|
-
|
|
45
|
+
source: 'structured_tool_event',
|
|
46
|
+
reason: 'agent prompt requested payment creation',
|
|
47
|
+
result: '{"charge_id":"CHG-TEMBORY-LAB-001","customer_id":"CUS-TEMBORY-LAB-001","status":"created"}',
|
|
46
48
|
},
|
|
47
49
|
];
|
|
48
50
|
|
|
49
51
|
const vectorMemories = [
|
|
50
52
|
{
|
|
51
|
-
memory: '[Used tools: Tool:
|
|
53
|
+
memory: '[Used tools: Tool: pagamento_criar_cobranca, Input: {"customer_id":"CUS-TEMBORY-LAB-001"}, Result: {"charge_id":"CHG-TEMBORY-LAB-001","status":"created"}] Cobrança criada para Lucas.',
|
|
52
54
|
created_at: '2026-05-14T05:34:35.373020Z',
|
|
53
55
|
temboryScore: { semanticScore: 0.71, recencyScore: 0.99, hybridScore: 0.81, source: 'semantic' },
|
|
54
56
|
},
|
|
55
|
-
{
|
|
56
|
-
memory: '[Used tools: Tool: agenda_consultar_disponibilidade, Input: , Result: [{"args":{"tool":"agenda_consultar_disponibilidade","available_slots":"2026-05-12T09:00,2026-05-12T14:00,2026-05-13T10:30","timezone":"America/Sao_Paulo","next_step":"escolher_um_horario"}}]] Temos estes horários vagos:\n- 12/05 às 09:00\n- 12/05 às 14:00\n- 13/05 às 10:30',
|
|
57
|
-
created_at: '2026-05-14T05:34:14.717988Z',
|
|
58
|
-
temboryScore: { semanticScore: 0.7, recencyScore: 0.99, hybridScore: 0.8, source: 'semantic' },
|
|
59
|
-
},
|
|
60
|
-
{ memory: 'Oi! Como posso ajudar?', created_at: '2026-05-14T05:33:47.194630Z' },
|
|
61
|
-
{ memory: 'oi', created_at: '2026-05-14T05:33:47.194355Z' },
|
|
57
|
+
{ memory: 'Lucas prefere atendimento financeiro por WhatsApp.', created_at: '2026-05-14T05:33:47.194630Z' },
|
|
62
58
|
];
|
|
63
59
|
|
|
64
60
|
const operationalState = core.deriveOperationalState(toolHistory, {}, [], true);
|
|
65
61
|
const workingMemory = {
|
|
66
|
-
current_goal: 'call
|
|
67
|
-
current_task: 'call
|
|
68
|
-
last_user_intent: '
|
|
69
|
-
last_user_message: '
|
|
70
|
-
next_expected_action: 'call
|
|
62
|
+
current_goal: 'call suporte_abrir_ticket now using prior CRM/payment context',
|
|
63
|
+
current_task: 'call suporte_abrir_ticket now using prior CRM/payment context',
|
|
64
|
+
last_user_intent: 'tool_action_candidate',
|
|
65
|
+
last_user_message: 'abre um ticket com financeiro',
|
|
66
|
+
next_expected_action: 'call suporte_abrir_ticket now using prior CRM/payment context',
|
|
71
67
|
};
|
|
72
|
-
const decisionState = core.deriveDecisionState({ query: '
|
|
68
|
+
const decisionState = core.deriveDecisionState({ query: 'abre um ticket com financeiro', toolHistory, operationalState, workingMemory });
|
|
73
69
|
const memoryCompression = core.deriveMemoryCompression({ toolHistory, operationalState, vectorMemories });
|
|
74
70
|
const common = {
|
|
75
71
|
payloadFormat: 'structured',
|
|
76
|
-
query: '
|
|
72
|
+
query: 'abre um ticket com financeiro',
|
|
77
73
|
userId: 'd48292bd4c9f4eccb9120092e3acd073',
|
|
78
74
|
profileFacts: {},
|
|
79
75
|
workingMemory,
|
|
@@ -86,8 +82,8 @@ const common = {
|
|
|
86
82
|
recentMessages: [],
|
|
87
83
|
toolHistory,
|
|
88
84
|
highlights: [
|
|
89
|
-
'tool
|
|
90
|
-
'tool
|
|
85
|
+
'tool crm_buscar_cliente: {"customer_id":"CUS-TEMBORY-LAB-001"}',
|
|
86
|
+
'tool pagamento_criar_cobranca: {"charge_id":"CHG-TEMBORY-LAB-001"}',
|
|
91
87
|
],
|
|
92
88
|
graph: [],
|
|
93
89
|
diagnostics: {},
|
|
@@ -106,8 +102,8 @@ for (const [name, adv] of modes) {
|
|
|
106
102
|
size: core.contextSizeOfMessages(messages),
|
|
107
103
|
has_next_expected_action: /next_expected_action/.test(text),
|
|
108
104
|
has_do_not_repeat_tools: /do_not_repeat_tools/.test(text),
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
has_duplicate_calling_text: /Calling
|
|
105
|
+
has_customer_id: /CUS-TEMBORY-LAB-001/.test(text),
|
|
106
|
+
has_charge_id: /CHG-TEMBORY-LAB-001/.test(text),
|
|
107
|
+
has_duplicate_calling_text: /Calling\s+[A-Za-z_][A-Za-z0-9_.:-]*/.test(text),
|
|
112
108
|
}, null, 2));
|
|
113
109
|
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const Module = require('node:module');
|
|
6
|
+
|
|
7
|
+
const originalLoad = Module._load;
|
|
8
|
+
Module._load = function patchedLoad(request, parent, isMain) {
|
|
9
|
+
if (request === 'n8n-workflow') {
|
|
10
|
+
return {
|
|
11
|
+
NodeConnectionTypes: {
|
|
12
|
+
AiMemory: 'ai_memory',
|
|
13
|
+
AiLanguageModel: 'ai_languageModel',
|
|
14
|
+
AiEmbedding: 'ai_embedding',
|
|
15
|
+
},
|
|
16
|
+
NodeApiError: class NodeApiError extends Error {},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (request === '@langchain/core/messages') {
|
|
20
|
+
return {
|
|
21
|
+
HumanMessage: class HumanMessage { constructor(content) { this.content = content; } _getType() { return 'human'; } },
|
|
22
|
+
AIMessage: class AIMessage { constructor(content) { this.content = content; } _getType() { return 'ai'; } },
|
|
23
|
+
SystemMessage: class SystemMessage { constructor(content) { this.content = content; } _getType() { return 'system'; } },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return originalLoad.call(this, request, parent, isMain);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const core = require('../dist/nodes/Mem0/Mem0Memory.node.js').__private;
|
|
30
|
+
|
|
31
|
+
const BASE_TIME = Date.parse('2026-05-16T12:00:00.000Z');
|
|
32
|
+
|
|
33
|
+
function at(index) {
|
|
34
|
+
return new Date(BASE_TIME + index * 1000).toISOString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseContext(messages) {
|
|
38
|
+
return messages.map((message) => message.content || '').join('\n\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function requiredToolFromText(text) {
|
|
42
|
+
return /"required_tool":"([^"]+)"/.exec(text)?.[1] || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function previousUserMessageFromText(text) {
|
|
46
|
+
return /"previous_user_message":"((?:\\"|[^"])*)"/.exec(text)?.[1]?.replace(/\\"/g, '"') || '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeText(value) {
|
|
50
|
+
return String(value || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mockTool(name, userMessage) {
|
|
54
|
+
const outputs = {
|
|
55
|
+
crm_buscar_cliente: {
|
|
56
|
+
customer_id: 'CUS-TEMBORY-LAB-001',
|
|
57
|
+
name: 'Lucas',
|
|
58
|
+
phone: '11999998888',
|
|
59
|
+
lifecycle_stage: 'lead_qualified',
|
|
60
|
+
},
|
|
61
|
+
crm_atualizar_pipeline: {
|
|
62
|
+
customer_id: 'CUS-TEMBORY-LAB-001',
|
|
63
|
+
pipeline_id: 'PIPE-VENDAS-001',
|
|
64
|
+
stage: 'proposal',
|
|
65
|
+
updated_from_message: userMessage,
|
|
66
|
+
},
|
|
67
|
+
pagamento_criar_cobranca: {
|
|
68
|
+
charge_id: 'CHG-TEMBORY-LAB-001',
|
|
69
|
+
customer_id: 'CUS-TEMBORY-LAB-001',
|
|
70
|
+
amount_cents: 9700,
|
|
71
|
+
status: 'created',
|
|
72
|
+
},
|
|
73
|
+
suporte_abrir_ticket: {
|
|
74
|
+
ticket_id: 'TCK-TEMBORY-LAB-001',
|
|
75
|
+
customer_id: 'CUS-TEMBORY-LAB-001',
|
|
76
|
+
queue: 'financeiro',
|
|
77
|
+
status: 'opened',
|
|
78
|
+
},
|
|
79
|
+
api_enviar_webhook: {
|
|
80
|
+
webhook_id: 'WH-TEMBORY-LAB-001',
|
|
81
|
+
endpoint: 'https://example.test/hooks/customer',
|
|
82
|
+
status: 'delivered',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
if (!outputs[name]) throw new Error(`Unknown mock tool: ${name}`);
|
|
86
|
+
return outputs[name];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function answerFor({ userMessage, toolCalls, state, contextText }) {
|
|
90
|
+
const normalized = normalizeText(userMessage);
|
|
91
|
+
if (/o que eu ja te disse|o que eu já te disse/.test(normalized)) {
|
|
92
|
+
const facts = core.renderProfileFacts(state.profileFacts);
|
|
93
|
+
return [
|
|
94
|
+
'Você me disse que:',
|
|
95
|
+
facts.name ? `- seu nome é ${facts.name}` : null,
|
|
96
|
+
facts.phone ? `- seu telefone é ${facts.phone}` : null,
|
|
97
|
+
facts.company ? `- sua empresa é ${facts.company}` : null,
|
|
98
|
+
Array.isArray(facts.interests) && facts.interests.length ? `- interesses: ${facts.interests.join(', ')}` : null,
|
|
99
|
+
].filter(Boolean).join('\n');
|
|
100
|
+
}
|
|
101
|
+
if (/quais tools|tools foram|ferramentas foram|outputs/.test(normalized)) {
|
|
102
|
+
if (!state.toolHistory.length) return 'Nenhuma tool foi chamada ainda.';
|
|
103
|
+
return `Foram chamadas ${state.toolHistory.length} tools:\n` + state.toolHistory
|
|
104
|
+
.map((tool, index) => `${index + 1}. ${tool.name}: ${tool.result}`)
|
|
105
|
+
.join('\n');
|
|
106
|
+
}
|
|
107
|
+
if (/qual.*ultima mensagem antes/.test(normalized)) {
|
|
108
|
+
const previous = previousUserMessageFromText(contextText)
|
|
109
|
+
|| core.previousUserFallbackFromWorkingMemory(userMessage, state.workingMemory);
|
|
110
|
+
return previous ? `Sua última mensagem antes desta foi: "${previous}".` : 'Não encontrei mensagem anterior.';
|
|
111
|
+
}
|
|
112
|
+
if (toolCalls.length) {
|
|
113
|
+
return `Tool executada: ${toolCalls.map((tool) => tool.name).join(', ')}.`;
|
|
114
|
+
}
|
|
115
|
+
return userMessage.includes('lucas') ? 'Prazer, Lucas! Como posso ajudar?' : 'Perfeito.';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildTurnContext({ state, userMessage, visibleToolHistory, turnIndex, requiredTool = null }) {
|
|
119
|
+
state.recentMessages.push({ role: 'user', content: userMessage, at: at(turnIndex * 2) });
|
|
120
|
+
state.profileFacts = core.mergeProfileFacts(
|
|
121
|
+
state.profileFacts,
|
|
122
|
+
core.extractProfileFactsFromText(userMessage, 'user_message', at(turnIndex * 2)),
|
|
123
|
+
core.mergeProfileFacts(...state.recentMessages
|
|
124
|
+
.filter((message) => /^(user|human)$/i.test(String(message.role || '')))
|
|
125
|
+
.map((message) => core.extractProfileFactsFromText(message.content, 'recent_user_message', message.at))),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const renderedProfile = core.renderProfileFacts(state.profileFacts);
|
|
129
|
+
const operationalState = core.deriveOperationalState(visibleToolHistory, renderedProfile, state.recentMessages, true);
|
|
130
|
+
const workingMemory = core.deriveWorkingMemory({
|
|
131
|
+
query: userMessage,
|
|
132
|
+
profileFacts: renderedProfile,
|
|
133
|
+
recentMessages: state.recentMessages,
|
|
134
|
+
toolHistory: visibleToolHistory,
|
|
135
|
+
operationalState,
|
|
136
|
+
previous: state.workingMemory,
|
|
137
|
+
});
|
|
138
|
+
if (requiredTool) {
|
|
139
|
+
workingMemory.current_task = `MUST call ${requiredTool} now because the agent prompt selected this tool`;
|
|
140
|
+
workingMemory.next_expected_action = `MUST call ${requiredTool} now because the agent prompt selected this tool`;
|
|
141
|
+
workingMemory.last_user_intent = 'tool_action_candidate';
|
|
142
|
+
}
|
|
143
|
+
const decisionState = core.deriveDecisionState({
|
|
144
|
+
query: userMessage,
|
|
145
|
+
toolHistory: visibleToolHistory,
|
|
146
|
+
operationalState,
|
|
147
|
+
workingMemory,
|
|
148
|
+
});
|
|
149
|
+
const adv = core.applyOperationalPreset({ operationPreset: 'productionBalanced' });
|
|
150
|
+
const contextMessages = core.buildContextMessages({
|
|
151
|
+
payloadFormat: 'structured',
|
|
152
|
+
query: userMessage,
|
|
153
|
+
userId: state.namespace,
|
|
154
|
+
profileFacts: renderedProfile,
|
|
155
|
+
workingMemory,
|
|
156
|
+
decisionState,
|
|
157
|
+
memoryCompression: core.deriveMemoryCompression({
|
|
158
|
+
recentMessages: state.recentMessages,
|
|
159
|
+
toolHistory: visibleToolHistory,
|
|
160
|
+
profileFacts: state.profileFacts,
|
|
161
|
+
operationalState,
|
|
162
|
+
vectorMemories: [],
|
|
163
|
+
}),
|
|
164
|
+
operationalState,
|
|
165
|
+
actionLedger: core.deriveActionLedger(visibleToolHistory, 20, true),
|
|
166
|
+
entityTimeline: core.deriveEntityTimeline(state.profileFacts, [], state.recentMessages, visibleToolHistory),
|
|
167
|
+
vectorMemories: [],
|
|
168
|
+
recentMessages: state.recentMessages,
|
|
169
|
+
toolHistory: visibleToolHistory,
|
|
170
|
+
highlights: [],
|
|
171
|
+
graph: [],
|
|
172
|
+
diagnostics: {},
|
|
173
|
+
adv,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
state.workingMemory = workingMemory;
|
|
177
|
+
state.decisionState = decisionState;
|
|
178
|
+
state.operationalState = operationalState;
|
|
179
|
+
|
|
180
|
+
return { contextText: parseContext(contextMessages), workingMemory, operationalState };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function runScenario({ name, turns, expectedCounts, hideToolHistoryOnTurns = new Set() }) {
|
|
184
|
+
const state = {
|
|
185
|
+
namespace: `local-${name}`,
|
|
186
|
+
recentMessages: [],
|
|
187
|
+
toolHistory: [],
|
|
188
|
+
profileFacts: {},
|
|
189
|
+
workingMemory: {},
|
|
190
|
+
decisionState: {},
|
|
191
|
+
operationalState: {},
|
|
192
|
+
};
|
|
193
|
+
const trace = [];
|
|
194
|
+
|
|
195
|
+
for (let index = 0; index < turns.length; index += 1) {
|
|
196
|
+
const turn = turns[index];
|
|
197
|
+
const turnNumber = index + 1;
|
|
198
|
+
const visibleToolHistory = hideToolHistoryOnTurns.has(turnNumber) ? [] : state.toolHistory;
|
|
199
|
+
const { contextText, workingMemory, operationalState } = buildTurnContext({
|
|
200
|
+
state,
|
|
201
|
+
userMessage: turn.user,
|
|
202
|
+
visibleToolHistory,
|
|
203
|
+
turnIndex: index,
|
|
204
|
+
requiredTool: turn.tool,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
assert.doesNotMatch(contextText, /\[Used tools:|Calling\s+[A-Za-z_][A-Za-z0-9_.:-]*/i, `${name} turn ${turnNumber}: raw tool audit leaked into context`);
|
|
208
|
+
|
|
209
|
+
const toolName = requiredToolFromText(contextText);
|
|
210
|
+
const toolCalls = [];
|
|
211
|
+
if (toolName) {
|
|
212
|
+
const result = mockTool(toolName, turn.user);
|
|
213
|
+
const extracted = core.extractToolCalls({
|
|
214
|
+
output: '',
|
|
215
|
+
__temboryToolCalls: [{
|
|
216
|
+
id: `${name}-turn-${turnNumber}-${toolName}`,
|
|
217
|
+
sequence: 1,
|
|
218
|
+
name: toolName,
|
|
219
|
+
input: JSON.stringify({ user_message: turn.user }),
|
|
220
|
+
result: JSON.stringify([{ args: { tool: toolName, ...result } }]),
|
|
221
|
+
ok: true,
|
|
222
|
+
at: at(index * 2 + 1),
|
|
223
|
+
source: 'local_n8n_agent_simulator',
|
|
224
|
+
reason: `agent_prompt_selected_${toolName}`,
|
|
225
|
+
}],
|
|
226
|
+
});
|
|
227
|
+
state.toolHistory = core.applyToolHistoryWindow(state.toolHistory.concat(extracted), 3600, 20);
|
|
228
|
+
toolCalls.push(...extracted);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const answer = core.cleanAssistantTranscriptText(answerFor({ userMessage: turn.user, toolCalls, state, contextText }));
|
|
232
|
+
state.recentMessages.push({ role: 'assistant', content: answer, at: at(index * 2 + 1) });
|
|
233
|
+
state.recentMessages = core.pruneConversationMessagesPreserveAnchors(state.recentMessages, 30);
|
|
234
|
+
|
|
235
|
+
trace.push({
|
|
236
|
+
turn: turnNumber,
|
|
237
|
+
user: turn.user,
|
|
238
|
+
requiredTool: toolName,
|
|
239
|
+
calledTools: toolCalls.map((tool) => tool.name),
|
|
240
|
+
toolState: operationalState.tool_state,
|
|
241
|
+
nextExpected: workingMemory.next_expected_action,
|
|
242
|
+
answer,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const [toolName, expectedCount] of Object.entries(expectedCounts)) {
|
|
247
|
+
assert.equal(state.toolHistory.filter((tool) => tool.name === toolName).length, expectedCount, `${name}: unexpected count for ${toolName}`);
|
|
248
|
+
}
|
|
249
|
+
assert.match(trace.at(-1).answer, /quais tools foram chamadas/i, `${name}: previous message recall failed`);
|
|
250
|
+
|
|
251
|
+
const finalOperational = core.deriveOperationalState(state.toolHistory, core.renderProfileFacts(state.profileFacts), state.recentMessages, true);
|
|
252
|
+
const health = core.deriveContextHealth({
|
|
253
|
+
userId: state.namespace,
|
|
254
|
+
project: 'local-agent-simulation',
|
|
255
|
+
vectorMemories: [{ memory: 'local simulation' }],
|
|
256
|
+
recentMessages: state.recentMessages,
|
|
257
|
+
toolHistory: state.toolHistory,
|
|
258
|
+
workingMemory: state.workingMemory,
|
|
259
|
+
decisionState: state.decisionState,
|
|
260
|
+
memoryCompression: core.deriveMemoryCompression({
|
|
261
|
+
recentMessages: state.recentMessages,
|
|
262
|
+
toolHistory: state.toolHistory,
|
|
263
|
+
profileFacts: state.profileFacts,
|
|
264
|
+
operationalState: finalOperational,
|
|
265
|
+
vectorMemories: [],
|
|
266
|
+
}),
|
|
267
|
+
operationalState: finalOperational,
|
|
268
|
+
diagnostics: { connectedAi: { languageModel: true, embedding: true, errors: [] } },
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
assert.ok(health.quality_score >= 970, `${name}: health score too low: ${health.quality_score}`);
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
name,
|
|
275
|
+
ok: true,
|
|
276
|
+
toolHistory: state.toolHistory.map((tool) => tool.name),
|
|
277
|
+
profileFacts: core.renderProfileFacts(state.profileFacts),
|
|
278
|
+
qualityScore: health.quality_score,
|
|
279
|
+
status: health.status,
|
|
280
|
+
trace,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const results = [
|
|
285
|
+
runScenario({
|
|
286
|
+
name: 'generic-crm-payment-support-flow',
|
|
287
|
+
turns: [
|
|
288
|
+
{ user: 'oi meu nome eh lucas' },
|
|
289
|
+
{ user: 'meu telefone eh 11999998888' },
|
|
290
|
+
{ user: 'minha empresa chama Elefai' },
|
|
291
|
+
{ user: 'busca meu cadastro pelo telefone', tool: 'crm_buscar_cliente' },
|
|
292
|
+
{ user: 'atualiza meu pipeline para proposta', tool: 'crm_atualizar_pipeline' },
|
|
293
|
+
{ user: 'cria uma cobrança para mim', tool: 'pagamento_criar_cobranca' },
|
|
294
|
+
{ user: 'abre um ticket com o financeiro', tool: 'suporte_abrir_ticket' },
|
|
295
|
+
{ user: 'envia webhook para meu sistema', tool: 'api_enviar_webhook' },
|
|
296
|
+
{ user: 'quais tools foram chamadas e quais outputs elas deram?' },
|
|
297
|
+
{ user: 'qual foi minha ultima mensagem antes desta?' },
|
|
298
|
+
],
|
|
299
|
+
expectedCounts: {
|
|
300
|
+
crm_buscar_cliente: 1,
|
|
301
|
+
crm_atualizar_pipeline: 1,
|
|
302
|
+
pagamento_criar_cobranca: 1,
|
|
303
|
+
suporte_abrir_ticket: 1,
|
|
304
|
+
api_enviar_webhook: 1,
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
runScenario({
|
|
308
|
+
name: 'retrieval-gap-still-uses-agent-selected-tools',
|
|
309
|
+
hideToolHistoryOnTurns: new Set([5, 6]),
|
|
310
|
+
turns: [
|
|
311
|
+
{ user: 'oi meu nome eh lucas' },
|
|
312
|
+
{ user: 'busca meu cadastro pelo telefone', tool: 'crm_buscar_cliente' },
|
|
313
|
+
{ user: 'quais tools foram chamadas?' },
|
|
314
|
+
{ user: 'cria uma cobrança para mim', tool: 'pagamento_criar_cobranca' },
|
|
315
|
+
{ user: 'abre ticket mesmo se o histórico estruturado não vier agora', tool: 'suporte_abrir_ticket' },
|
|
316
|
+
{ user: 'quais tools foram chamadas e quais outputs elas deram?' },
|
|
317
|
+
{ user: 'qual foi minha ultima mensagem antes desta?' },
|
|
318
|
+
],
|
|
319
|
+
expectedCounts: {
|
|
320
|
+
crm_buscar_cliente: 1,
|
|
321
|
+
pagamento_criar_cobranca: 1,
|
|
322
|
+
suporte_abrir_ticket: 1,
|
|
323
|
+
},
|
|
324
|
+
}),
|
|
325
|
+
];
|
|
326
|
+
|
|
327
|
+
console.log(JSON.stringify({ ok: true, scenarios: results }, null, 2));
|
|
@@ -30,31 +30,31 @@ const core = require('../dist/nodes/Mem0/Mem0Memory.node.js').__private;
|
|
|
30
30
|
|
|
31
31
|
const turns = [
|
|
32
32
|
{
|
|
33
|
-
user: '
|
|
34
|
-
tools: [{ name: '
|
|
33
|
+
user: 'Busca meu cadastro no CRM pelo telefone',
|
|
34
|
+
tools: [{ name: 'crm_buscar_cliente', input: { phone: '11999998888' }, result: { customer_id: 'CUS-TEMBORY-001' } }],
|
|
35
35
|
},
|
|
36
|
-
{ user: 'Pode usar
|
|
36
|
+
{ user: 'Pode usar esse cadastro', tools: [] },
|
|
37
37
|
{
|
|
38
|
-
user: '
|
|
39
|
-
tools: [{ name: '
|
|
38
|
+
user: 'Atualiza o pipeline para proposta',
|
|
39
|
+
tools: [{ name: 'crm_atualizar_pipeline', input: { customer_id: 'CUS-TEMBORY-001', stage: 'proposal' }, result: { pipeline_id: 'PIPE-TEMBORY-001', stage: 'proposal' } }],
|
|
40
40
|
},
|
|
41
|
-
{ user: 'Qual
|
|
41
|
+
{ user: 'Qual atualização ficou registrada?', tools: [] },
|
|
42
42
|
{
|
|
43
|
-
user: '
|
|
44
|
-
tools: [{ name: '
|
|
43
|
+
user: 'Cria uma cobrança para esse cliente',
|
|
44
|
+
tools: [{ name: 'pagamento_criar_cobranca', input: { customer_id: 'CUS-TEMBORY-001' }, result: { charge_id: 'CHG-TEMBORY-001' } }],
|
|
45
45
|
},
|
|
46
|
-
{ user: 'Qual foi o codigo
|
|
46
|
+
{ user: 'Qual foi o codigo da cobrança?', tools: [] },
|
|
47
47
|
{
|
|
48
|
-
user: '
|
|
49
|
-
tools: [{ name: '
|
|
48
|
+
user: 'Abre ticket para o financeiro',
|
|
49
|
+
tools: [{ name: 'suporte_abrir_ticket', input: { customer_id: 'CUS-TEMBORY-001', queue: 'financeiro' }, result: { ticket_id: 'TCK-TEMBORY-001' } }],
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
|
-
user: '
|
|
53
|
-
tools: [{ name: '
|
|
52
|
+
user: 'Envia webhook para meu sistema',
|
|
53
|
+
tools: [{ name: 'api_enviar_webhook', input: { customer_id: 'CUS-TEMBORY-001' }, result: { webhook_id: 'WH-TEMBORY-001', status: 'delivered' } }],
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
|
-
user: '
|
|
57
|
-
tools: [{ name: '
|
|
56
|
+
user: 'Atualiza o CRM com o webhook enviado',
|
|
57
|
+
tools: [{ name: 'crm_registrar_evento', input: { customer_id: 'CUS-TEMBORY-001', event: 'webhook_delivered' }, result: { event_id: 'EVT-TEMBORY-001' } }],
|
|
58
58
|
},
|
|
59
59
|
{ user: 'Resume as tools usadas e o estado atual', tools: [] },
|
|
60
60
|
{ user: 'O que voce sabe sem chamar ferramenta?', tools: [] },
|
|
@@ -103,14 +103,14 @@ for (const [index, turn] of turns.entries()) {
|
|
|
103
103
|
toolHistory,
|
|
104
104
|
profileFacts: {},
|
|
105
105
|
operationalState,
|
|
106
|
-
vectorMemories: [{ memory: '
|
|
106
|
+
vectorMemories: [{ memory: 'EVT-TEMBORY-001 registrado' }],
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
const health = core.deriveContextHealth({
|
|
111
111
|
userId: namespace,
|
|
112
112
|
project: 'smoke',
|
|
113
|
-
vectorMemories: [{ memory: '
|
|
113
|
+
vectorMemories: [{ memory: 'EVT-TEMBORY-001 registrado' }],
|
|
114
114
|
recentMessages,
|
|
115
115
|
toolHistory,
|
|
116
116
|
workingMemory,
|
|
@@ -122,9 +122,10 @@ const health = core.deriveContextHealth({
|
|
|
122
122
|
|
|
123
123
|
assert.equal(recentMessages.length, 12);
|
|
124
124
|
assert.equal(toolHistory.length, 6);
|
|
125
|
-
assert.equal(operationalState.
|
|
126
|
-
assert.equal(
|
|
127
|
-
assert.
|
|
125
|
+
assert.equal(operationalState.tool_state.counts_by_name.crm_buscar_cliente, 1);
|
|
126
|
+
assert.equal(operationalState.tool_state.counts_by_name.crm_registrar_evento, 1);
|
|
127
|
+
assert.equal(decisionState.tool_decision_state.last_successful_tool.name, 'crm_registrar_evento');
|
|
128
|
+
assert.ok(decisionState.do_not_repeat_tools.includes('crm_registrar_evento'));
|
|
128
129
|
assert.ok(health.quality_score >= 970);
|
|
129
130
|
|
|
130
131
|
console.log(JSON.stringify({
|
|
@@ -134,5 +135,5 @@ console.log(JSON.stringify({
|
|
|
134
135
|
toolEvents: toolHistory.length,
|
|
135
136
|
qualityScore: health.quality_score,
|
|
136
137
|
status: health.status,
|
|
137
|
-
|
|
138
|
+
latestTool: operationalState.tool_state.last_successful_tool,
|
|
138
139
|
}, null, 2));
|