n8n-nodes-tembory 1.0.31 → 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.
|
@@ -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,74 +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
|
-
};
|
|
429
|
-
};
|
|
430
|
-
const preReservationFromRecentMessages = (recentMessages = []) => {
|
|
431
|
-
const messages = Array.isArray(recentMessages) ? recentMessages : [];
|
|
432
|
-
const candidates = [...messages].reverse().filter((message) => {
|
|
433
|
-
const role = String(message.role || '').toLowerCase();
|
|
434
|
-
const text = String(message.content || message.text || message.memory || '');
|
|
435
|
-
if (role && role !== 'assistant' && role !== 'ai' && role !== 'system')
|
|
436
|
-
return false;
|
|
437
|
-
return /\b(pr[eé][-\s]?reservad[oa]|pre[-\s]?reservad[oa]|reservation_id|RES-[A-Z0-9-]+)\b/i.test(text)
|
|
438
|
-
&& !/\b(confirmad[oa]|confirmation_id|CONF-[A-Z0-9-]+)\b/i.test(text);
|
|
439
|
-
});
|
|
440
|
-
const latest = candidates[0];
|
|
441
|
-
if (!latest)
|
|
442
|
-
return null;
|
|
443
|
-
return {
|
|
444
|
-
text: truncate(String(latest.content || latest.text || latest.memory || ''), 900),
|
|
445
|
-
at: latest.at || latest.created_at || latest.createdAt || nowIso(),
|
|
446
|
-
source: latest.source || 'recent_message',
|
|
447
|
-
};
|
|
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 '';
|
|
448
425
|
};
|
|
449
426
|
const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessages = [], includeResults = true) => {
|
|
450
427
|
const tools = Array.isArray(toolHistory) ? toolHistory : [];
|
|
451
428
|
const successfulTools = tools.filter((tool) => tool.ok !== false);
|
|
452
|
-
const
|
|
453
|
-
const
|
|
454
|
-
const
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
+
}
|
|
460
462
|
const lastTool = tools[tools.length - 1] || null;
|
|
461
|
-
const lastReservation = reservations[reservations.length - 1] || null;
|
|
462
|
-
const lastConfirmation = confirmations[confirmations.length - 1] || null;
|
|
463
|
-
const hasPendingReservation = Boolean((lastReservation && (!lastConfirmation || String(lastReservation.at || '') > String(lastConfirmation.at || ''))) || (recentPreReservation && !lastConfirmation));
|
|
464
463
|
const blockedWithoutContext = [];
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
blockedWithoutContext.push('agenda_confirmar_agendamento');
|
|
469
|
-
const guidance = [];
|
|
470
|
-
if (!hasAvailability)
|
|
471
|
-
guidance.push('No availability result is known for this session; consult availability before reserving or confirming.');
|
|
472
|
-
else if (hasPendingReservation)
|
|
473
|
-
guidance.push('A pre-reservation exists after the latest confirmation; confirmation can use the latest pre-reservation context.');
|
|
474
|
-
else if (lastConfirmation)
|
|
475
|
-
guidance.push('The latest reservation appears confirmed; do not confirm again unless the user explicitly asks to repeat or change it.');
|
|
476
|
-
else if (hasAvailability)
|
|
477
|
-
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
|
+
];
|
|
478
467
|
return {
|
|
479
468
|
profile_complete: Boolean(profileFacts && profileFacts.name && profileFacts.company && profileFacts.email && profileFacts.phone),
|
|
480
469
|
last_tool: lastTool ? { name: lastTool.name, ok: lastTool.ok, at: lastTool.at } : null,
|
|
@@ -482,20 +471,22 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
|
|
|
482
471
|
total: tools.length,
|
|
483
472
|
ok: successfulTools.length,
|
|
484
473
|
failed: tools.length - successfulTools.length,
|
|
485
|
-
|
|
486
|
-
agenda_pre_reservar_horario: reservations.length,
|
|
487
|
-
agenda_confirmar_agendamento: confirmations.length,
|
|
474
|
+
by_name: toolCountsByName,
|
|
488
475
|
},
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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,
|
|
499
490
|
},
|
|
500
491
|
blocked_without_context: Array.from(new Set(blockedWithoutContext)),
|
|
501
492
|
guidance,
|
|
@@ -510,6 +501,7 @@ const deriveActionLedger = (toolHistory = [], maxItems = 20, includeResults = tr
|
|
|
510
501
|
kind: 'tool_call',
|
|
511
502
|
name: tool.name || 'unknown_tool',
|
|
512
503
|
status: tool.ok === false ? 'failed' : 'ok',
|
|
504
|
+
reason: truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 260),
|
|
513
505
|
input: compactToolPayload(safeParseToolPayload(tool.input)),
|
|
514
506
|
result: maybeToolResult(tool, includeResults),
|
|
515
507
|
at: tool.at || null,
|
|
@@ -686,7 +678,7 @@ const toolHistoryFromMemory = (item) => {
|
|
|
686
678
|
source: 'semantic_fact',
|
|
687
679
|
};
|
|
688
680
|
}
|
|
689
|
-
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;
|
|
690
682
|
if (namedTool) {
|
|
691
683
|
return {
|
|
692
684
|
id: meta.id || meta.call_id || meta.callId || '',
|
|
@@ -699,26 +691,6 @@ const toolHistoryFromMemory = (item) => {
|
|
|
699
691
|
source: 'semantic_named_tool',
|
|
700
692
|
};
|
|
701
693
|
}
|
|
702
|
-
const text = meta.kind === 'tool_history' ? String(content || '').toLowerCase() : '';
|
|
703
|
-
const inferredAgendaTool = text.includes('confirm') && text.includes('agendamento')
|
|
704
|
-
? 'agenda_confirmar_agendamento'
|
|
705
|
-
: text.includes('pré-reserva') || text.includes('pre-reserva') || text.includes('pre reserva') || text.includes('pré reserva')
|
|
706
|
-
? 'agenda_pre_reservar_horario'
|
|
707
|
-
: (text.includes('horários disponíveis') || text.includes('horarios disponiveis') || text.includes('available') || text.includes('disponibilidade')) && text.includes('agenda')
|
|
708
|
-
? 'agenda_consultar_disponibilidade'
|
|
709
|
-
: '';
|
|
710
|
-
if (inferredAgendaTool) {
|
|
711
|
-
return {
|
|
712
|
-
id: meta.id || meta.call_id || meta.callId || '',
|
|
713
|
-
turnId: meta.turn_id || meta.turnId || '',
|
|
714
|
-
name: inferredAgendaTool,
|
|
715
|
-
input: '',
|
|
716
|
-
ok: true,
|
|
717
|
-
result: truncate(content || '', 1000),
|
|
718
|
-
at: meta.at || item.created_at || item.createdAt || nowIso(),
|
|
719
|
-
source: 'semantic_inference',
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
694
|
if (meta.kind !== 'tool_history')
|
|
723
695
|
return null;
|
|
724
696
|
const name = meta.name || meta.tool || meta.toolName;
|
|
@@ -1268,17 +1240,18 @@ const normalizeIntentText = (value = '') => String(value || '')
|
|
|
1268
1240
|
.toLowerCase()
|
|
1269
1241
|
.normalize('NFD')
|
|
1270
1242
|
.replace(/[\u0300-\u036f]/g, '');
|
|
1271
|
-
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));
|
|
1272
1244
|
const isConversationRecallQuery = (value = '') => {
|
|
1273
1245
|
const text = normalizeIntentText(value);
|
|
1274
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)
|
|
1275
1247
|
|| /\b(o que|oq)\b.{0,30}\b(eu)\b.{0,30}\b(falei|disse|perguntei|mandei)\b/.test(text)
|
|
1276
1248
|
|| /\b(nao|nao)\b.{0,20}\b(lembra|sabe)\b.{0,60}\b(msg|msgs|mensagem|pergunta|falei|disse|perguntei)\b/.test(text);
|
|
1277
1249
|
};
|
|
1278
|
-
const
|
|
1250
|
+
const isOperationalStatusQuery = (value = '') => {
|
|
1279
1251
|
const text = normalizeIntentText(value);
|
|
1280
|
-
return /\b(
|
|
1281
|
-
|| /\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);
|
|
1282
1255
|
};
|
|
1283
1256
|
const recentUserMessages = (recentMessages = []) => [...(recentMessages || [])]
|
|
1284
1257
|
.filter((msg) => /^(user|human)$/i.test(String(msg.role || '')))
|
|
@@ -1336,8 +1309,8 @@ const buildConversationFrame = ({ query = '', recentMessages = [], workingMemory
|
|
|
1336
1309
|
};
|
|
1337
1310
|
const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalState = {}, workingMemory = {} }) => {
|
|
1338
1311
|
const recall = isConversationRecallQuery(query);
|
|
1339
|
-
const
|
|
1340
|
-
if (!recall && !
|
|
1312
|
+
const operationalStatus = isOperationalStatusQuery(query);
|
|
1313
|
+
if (!recall && !operationalStatus)
|
|
1341
1314
|
return null;
|
|
1342
1315
|
const previousUser = previousUserMessageForQuery(query, recentMessages);
|
|
1343
1316
|
const firstUser = firstUserMessageFromConversation(recentMessages);
|
|
@@ -1345,31 +1318,22 @@ const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalSta
|
|
|
1345
1318
|
.filter((msg) => normalizeIntentText(msg.content).trim() !== normalizeIntentText(query).trim())
|
|
1346
1319
|
.slice(0, 3)
|
|
1347
1320
|
.map((msg) => ({ role: msg.role, content: truncate(msg.content, 500), at: msg.at }));
|
|
1348
|
-
const
|
|
1321
|
+
const toolState = (operationalState || {}).tool_state || {};
|
|
1349
1322
|
const previousUserMessage = previousUser ? truncate(previousUser.content, 500) : previousUserFallbackFromWorkingMemory(query, workingMemory);
|
|
1350
1323
|
const focus = cleanContextValue({
|
|
1351
1324
|
current_user_request: truncate(query, 500),
|
|
1352
|
-
intent: recall ? 'conversation_recall' : '
|
|
1325
|
+
intent: recall ? 'conversation_recall' : 'tool_or_status_question',
|
|
1353
1326
|
first_user_message: firstUser ? truncate(firstUser.content, 500) : null,
|
|
1354
1327
|
previous_user_message: previousUserMessage,
|
|
1355
1328
|
recent_user_messages: users,
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
? 'pending_pre_reservation'
|
|
1361
|
-
: agenda.has_pre_reservation
|
|
1362
|
-
? 'pre_reserved'
|
|
1363
|
-
: agenda.has_availability
|
|
1364
|
-
? 'availability_known_not_scheduled'
|
|
1365
|
-
: 'none',
|
|
1366
|
-
has_confirmation: Boolean(agenda.has_confirmation && !agenda.has_pending_pre_reservation),
|
|
1367
|
-
has_pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
|
|
1368
|
-
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 || {},
|
|
1369
1333
|
} : undefined,
|
|
1370
1334
|
instruction: recall
|
|
1371
|
-
? 'Answer the user meta-question from previous_user_message/recent_user_messages and the chronological transcript. Do not
|
|
1372
|
-
: '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.',
|
|
1373
1337
|
});
|
|
1374
1338
|
if (!previousUserMessage)
|
|
1375
1339
|
focus.previous_user_message = null;
|
|
@@ -1377,47 +1341,29 @@ const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalSta
|
|
|
1377
1341
|
};
|
|
1378
1342
|
const buildActionDirective = ({ workingMemory = {}, operationalState = {} }) => {
|
|
1379
1343
|
const next = String((workingMemory || {}).next_expected_action || '');
|
|
1380
|
-
const
|
|
1381
|
-
if (!
|
|
1344
|
+
const tool = pickRequiredToolFromAction(next);
|
|
1345
|
+
if (!tool || !/\bcall\b|\bMUST\b/i.test(next))
|
|
1382
1346
|
return null;
|
|
1383
|
-
const
|
|
1384
|
-
const
|
|
1347
|
+
const toolState = (operationalState || {}).tool_state || {};
|
|
1348
|
+
const latestForTool = ((toolState.latest_by_name || {})[tool]) || null;
|
|
1385
1349
|
return cleanContextValue({
|
|
1386
1350
|
required_tool: tool,
|
|
1387
1351
|
next_expected_action: next,
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
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 || {},
|
|
1394
1357
|
},
|
|
1395
|
-
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.`,
|
|
1396
1359
|
});
|
|
1397
1360
|
};
|
|
1398
1361
|
const inferToolGuard = ({ query, recentMessages, toolHistory, vectorMemories }) => {
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
const hasPreReservationTool = names.has('agenda_pre_reservar_horario');
|
|
1405
|
-
const hasAnyContext = names.size > 0 || hasRecentContext || hasVectorContext;
|
|
1406
|
-
const blocked = [];
|
|
1407
|
-
const reasons = [];
|
|
1408
|
-
if (!hasAnyContext) {
|
|
1409
|
-
blocked.push('agenda_pre_reservar_horario', 'agenda_confirmar_agendamento');
|
|
1410
|
-
reasons.push('no prior availability or pre-reservation context was found for this session');
|
|
1411
|
-
}
|
|
1412
|
-
if (hasConfirmIntent(text) && !hasPreReservationTool) {
|
|
1413
|
-
blocked.push('agenda_confirmar_agendamento');
|
|
1414
|
-
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');
|
|
1415
|
-
}
|
|
1416
|
-
if ((/pre.?reserv|pre.?reserv|reservar/.test(text)) && !hasAvailabilityTool) {
|
|
1417
|
-
blocked.push('agenda_pre_reservar_horario');
|
|
1418
|
-
reasons.push('reservation requested but no structured agenda_consultar_disponibilidade tool result was found');
|
|
1419
|
-
}
|
|
1420
|
-
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
|
+
};
|
|
1421
1367
|
};
|
|
1422
1368
|
const inferUserIntent = (query = '', recentMessages = []) => {
|
|
1423
1369
|
const latestUser = [...(recentMessages || [])].reverse().find((msg) => /^(user|human)$/i.test(String(msg.role || '')));
|
|
@@ -1426,20 +1372,14 @@ const inferUserIntent = (query = '', recentMessages = []) => {
|
|
|
1426
1372
|
const normalizedQuery = normalizeIntentText(query).trim();
|
|
1427
1373
|
if (isConversationRecallQuery(query))
|
|
1428
1374
|
return 'conversation_recall';
|
|
1429
|
-
if (
|
|
1430
|
-
return '
|
|
1375
|
+
if (isOperationalStatusQuery(query))
|
|
1376
|
+
return 'operational_status_question';
|
|
1431
1377
|
if (/^(ok|sim|pode|pode sim|isso|isso mesmo|confirmo|confirmar)$/.test(normalizedQuery))
|
|
1432
1378
|
return 'affirm';
|
|
1433
|
-
if (
|
|
1434
|
-
return '
|
|
1435
|
-
if (
|
|
1436
|
-
return '
|
|
1437
|
-
if (/\b(pre.?reserv|pre.?reserv|reservar|segurar|marcar)\b/.test(text))
|
|
1438
|
-
return 'pre_reserve';
|
|
1439
|
-
if (/\b(disponibilidade|horarios?|agenda(?:r|mento)?|quando pode|tem vaga|quero agendar|gostaria de agendar|preciso agendar)\b/.test(text))
|
|
1440
|
-
return 'check_availability';
|
|
1441
|
-
if (/\b(cancel|desmarcar|remarcar|alterar|mudar)\b/.test(text))
|
|
1442
|
-
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';
|
|
1443
1383
|
if (/\b(meu nome|email|telefone|empresa|prefiro|preferencia)\b/.test(text))
|
|
1444
1384
|
return 'profile_update';
|
|
1445
1385
|
if (text.trim())
|
|
@@ -1447,41 +1387,14 @@ const inferUserIntent = (query = '', recentMessages = []) => {
|
|
|
1447
1387
|
return 'unknown';
|
|
1448
1388
|
};
|
|
1449
1389
|
const deriveNextExpectedAction = (intent, operationalState = {}) => {
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
if (agenda.has_pending_pre_reservation)
|
|
1453
|
-
return 'MUST call agenda_confirmar_agendamento now; do not ask the same confirmation question again';
|
|
1454
|
-
if (agenda.has_availability)
|
|
1455
|
-
return 'MUST call agenda_pre_reservar_horario now using the selected or latest discussed slot; do not ask again';
|
|
1456
|
-
return 'call agenda_consultar_disponibilidade before reserving';
|
|
1457
|
-
}
|
|
1458
|
-
if (intent === 'confirm') {
|
|
1459
|
-
if (agenda.has_pending_pre_reservation)
|
|
1460
|
-
return 'MUST call agenda_confirmar_agendamento now using the latest pre-reservation context; do not ask again';
|
|
1461
|
-
if (agenda.has_availability)
|
|
1462
|
-
return 'MUST call agenda_pre_reservar_horario first using the selected or latest discussed slot; do not ask for permission again';
|
|
1463
|
-
return 'do not call agenda_confirmar_agendamento; call agenda_consultar_disponibilidade first because no availability is known';
|
|
1464
|
-
}
|
|
1465
|
-
if (intent === 'select_slot') {
|
|
1466
|
-
if (agenda.has_availability)
|
|
1467
|
-
return 'MUST call agenda_pre_reservar_horario now using the selected slot; do not ask for confirmation before pre-reserving';
|
|
1468
|
-
return 'call agenda_consultar_disponibilidade before pre-reserving';
|
|
1469
|
-
}
|
|
1470
|
-
if (intent === 'pre_reserve') {
|
|
1471
|
-
if (agenda.has_availability)
|
|
1472
|
-
return 'MUST call agenda_pre_reservar_horario now using one of the known available slots';
|
|
1473
|
-
return 'call agenda_consultar_disponibilidade before pre-reserving';
|
|
1474
|
-
}
|
|
1475
|
-
if (intent === 'check_availability')
|
|
1476
|
-
return agenda.has_availability ? 'reuse known availability unless the user asks to refresh' : 'call agenda_consultar_disponibilidade';
|
|
1477
|
-
if (intent === 'change_or_cancel')
|
|
1478
|
-
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';
|
|
1479
1392
|
if (intent === 'profile_update')
|
|
1480
1393
|
return 'save stable profile facts and continue the conversation';
|
|
1481
1394
|
if (intent === 'conversation_recall')
|
|
1482
|
-
return 'answer directly using
|
|
1483
|
-
if (intent === '
|
|
1484
|
-
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';
|
|
1485
1398
|
return 'answer using retrieved context and avoid unnecessary tool calls';
|
|
1486
1399
|
};
|
|
1487
1400
|
const deriveWorkingMemory = ({ query = '', profileFacts = {}, recentMessages = [], toolHistory = [], operationalState = {}, previous = {} }) => {
|
|
@@ -1506,11 +1419,11 @@ const deriveWorkingMemory = ({ query = '', profileFacts = {}, recentMessages = [
|
|
|
1506
1419
|
};
|
|
1507
1420
|
};
|
|
1508
1421
|
const deriveDecisionState = ({ query = '', toolHistory = [], operationalState = {}, workingMemory = {} }) => {
|
|
1509
|
-
const agenda = operationalState.agenda_state || {};
|
|
1510
1422
|
const decisions = [];
|
|
1511
1423
|
const doNotRepeatTools = [];
|
|
1512
1424
|
const intent = workingMemory.last_user_intent || inferUserIntent(query, []);
|
|
1513
1425
|
const now = nowIso();
|
|
1426
|
+
const toolState = operationalState.tool_state || {};
|
|
1514
1427
|
const pushDecision = (id, decision, reason, appliesTo, extra = {}) => {
|
|
1515
1428
|
decisions.push({
|
|
1516
1429
|
id,
|
|
@@ -1523,41 +1436,25 @@ const deriveDecisionState = ({ query = '', toolHistory = [], operationalState =
|
|
|
1523
1436
|
created_at: extra.created_at || now,
|
|
1524
1437
|
valid_until: extra.valid_until || null,
|
|
1525
1438
|
superseded_by: extra.superseded_by || null,
|
|
1439
|
+
tool: extra.tool,
|
|
1526
1440
|
});
|
|
1527
1441
|
};
|
|
1528
|
-
|
|
1529
|
-
doNotRepeatTools.push(
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
pushDecision(
|
|
1535
|
-
}
|
|
1536
|
-
if (agenda.has_confirmation && !agenda.has_pending_pre_reservation) {
|
|
1537
|
-
doNotRepeatTools.push('agenda_confirmar_agendamento');
|
|
1538
|
-
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 });
|
|
1539
|
-
}
|
|
1540
|
-
const reservationStatus = agenda.has_confirmation && !agenda.has_pending_pre_reservation
|
|
1541
|
-
? 'confirmed'
|
|
1542
|
-
: agenda.has_pending_pre_reservation
|
|
1543
|
-
? 'pending_pre_reservation'
|
|
1544
|
-
: agenda.has_pre_reservation
|
|
1545
|
-
? 'pre_reserved'
|
|
1546
|
-
: agenda.has_availability
|
|
1547
|
-
? 'availability_known'
|
|
1548
|
-
: '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 });
|
|
1549
1449
|
return {
|
|
1550
1450
|
active_decisions: decisions,
|
|
1551
1451
|
do_not_repeat_tools: Array.from(new Set(doNotRepeatTools)),
|
|
1552
1452
|
current_intent: intent,
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
reschedule_requested: intent === 'change_or_cancel' && Boolean(agenda.has_confirmation || agenda.has_pre_reservation),
|
|
1559
|
-
cancel_requested: /\b(cancelar|cancele|desmarcar|remover)\b/i.test(String(query || '')),
|
|
1560
|
-
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)),
|
|
1561
1458
|
evaluated_at: now,
|
|
1562
1459
|
},
|
|
1563
1460
|
conflict_policy: 'prefer active decisions with newer timestamps; ignore superseded or expired memories unless auditing',
|
|
@@ -1608,7 +1505,12 @@ const deriveMemoryCompression = ({ recentMessages = [], toolHistory = [], profil
|
|
|
1608
1505
|
session_summary: {
|
|
1609
1506
|
messages: lastMessages.length,
|
|
1610
1507
|
tools: lastTools,
|
|
1611
|
-
|
|
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
|
+
} : {},
|
|
1612
1514
|
},
|
|
1613
1515
|
entity_summary: profile,
|
|
1614
1516
|
workflow_summary: {
|
|
@@ -1654,7 +1556,6 @@ const deriveContextHealth = ({ userId = '', project = '', vectorMemories = [], r
|
|
|
1654
1556
|
score += weights[key] || 0;
|
|
1655
1557
|
}
|
|
1656
1558
|
const missing = Object.entries(checks).filter(([, ok]) => !ok).map(([key]) => key);
|
|
1657
|
-
const agenda = operationalState.agenda_state || {};
|
|
1658
1559
|
return {
|
|
1659
1560
|
kind: 'tembory.context_health.v1',
|
|
1660
1561
|
namespace: userId || null,
|
|
@@ -1669,11 +1570,10 @@ const deriveContextHealth = ({ userId = '', project = '', vectorMemories = [], r
|
|
|
1669
1570
|
tool_history: Array.isArray(toolHistory) ? toolHistory.length : 0,
|
|
1670
1571
|
active_decisions: Array.isArray(decisionState.active_decisions) ? decisionState.active_decisions.length : 0,
|
|
1671
1572
|
},
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
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) || {},
|
|
1677
1577
|
},
|
|
1678
1578
|
generated_at: nowIso(),
|
|
1679
1579
|
};
|
|
@@ -1684,13 +1584,13 @@ const contextMemoryText = (memory, max = 700) => {
|
|
|
1684
1584
|
if (tools.length) {
|
|
1685
1585
|
const names = tools.map((tool) => `${tool.name || 'tool'}:${tool.ok === false ? 'failed' : 'ok'}`).join(', ');
|
|
1686
1586
|
const ids = tools.map((tool) => tool.result || tool.input || '').join(' ');
|
|
1687
|
-
const idMatch = /(
|
|
1587
|
+
const idMatch = /([A-Za-z_][A-Za-z0-9_.:-]*id)["':\s]+([A-Za-z0-9_.:-]+)/i.exec(ids);
|
|
1688
1588
|
return truncate(`[tool_events_extracted] ${names}${idMatch ? ` ${idMatch[1]}=${idMatch[2]}` : ''}. See Tool history for structured details.`, max);
|
|
1689
1589
|
}
|
|
1690
1590
|
return truncate(text, max);
|
|
1691
1591
|
};
|
|
1692
1592
|
const approxTokenCount = (text) => Math.ceil(String(text || '').length / 4);
|
|
1693
|
-
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'];
|
|
1694
1594
|
const pickImportantFields = (value) => {
|
|
1695
1595
|
if (value === null || value === undefined)
|
|
1696
1596
|
return {};
|
|
@@ -1720,9 +1620,7 @@ const compactToolResult = (result, max = 360) => {
|
|
|
1720
1620
|
return undefined;
|
|
1721
1621
|
try {
|
|
1722
1622
|
const parsed = JSON.parse(text);
|
|
1723
|
-
|
|
1724
|
-
if (Object.keys(picked).length)
|
|
1725
|
-
return truncate(safeStringify(picked), max);
|
|
1623
|
+
return truncate(safeStringify(parsed), max);
|
|
1726
1624
|
}
|
|
1727
1625
|
catch { }
|
|
1728
1626
|
const picked = {};
|
|
@@ -1741,8 +1639,9 @@ const compactToolHistoryForAgent = (toolHistory = [], maxItems = 6, includeResul
|
|
|
1741
1639
|
name: tool.name,
|
|
1742
1640
|
status: tool.ok === false ? 'failed' : 'ok',
|
|
1743
1641
|
at: tool.at,
|
|
1744
|
-
|
|
1745
|
-
|
|
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,
|
|
1746
1645
|
}));
|
|
1747
1646
|
const cleanContextValue = (value) => {
|
|
1748
1647
|
if (Array.isArray(value)) {
|
|
@@ -1786,7 +1685,7 @@ const compactDecisionStateForAgent = (state = {}) => cleanContextValue({
|
|
|
1786
1685
|
at: decision.at || decision.updated_at,
|
|
1787
1686
|
})),
|
|
1788
1687
|
do_not_repeat_tools: state.do_not_repeat_tools,
|
|
1789
|
-
|
|
1688
|
+
tool_decision_state: state.tool_decision_state,
|
|
1790
1689
|
latest_tool: state.latest_tool ? cleanContextValue({
|
|
1791
1690
|
name: state.latest_tool.name,
|
|
1792
1691
|
status: state.latest_tool.status || (state.latest_tool.ok === false ? 'failed' : 'ok'),
|
|
@@ -1795,19 +1694,21 @@ const compactDecisionStateForAgent = (state = {}) => cleanContextValue({
|
|
|
1795
1694
|
conflict_policy: state.conflict_policy,
|
|
1796
1695
|
});
|
|
1797
1696
|
const compactOperationalStateForAgent = (state = {}) => {
|
|
1798
|
-
const
|
|
1697
|
+
const counts = state.tool_counts || {};
|
|
1698
|
+
const toolState = state.tool_state || {};
|
|
1799
1699
|
return {
|
|
1800
1700
|
last_tool: state.last_tool || null,
|
|
1801
|
-
tool_counts:
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
has_confirmation: Boolean(agenda.has_confirmation),
|
|
1806
|
-
has_pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
|
|
1807
|
-
latest_availability: compactToolResult(agenda.latest_availability_result),
|
|
1808
|
-
latest_pre_reservation: compactToolResult(agenda.latest_pre_reservation_result),
|
|
1809
|
-
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,
|
|
1810
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,
|
|
1811
1712
|
blocked_without_context: state.blocked_without_context || [],
|
|
1812
1713
|
guidance: (state.guidance || []).slice(0, 3),
|
|
1813
1714
|
};
|
|
@@ -1815,12 +1716,6 @@ const compactOperationalStateForAgent = (state = {}) => {
|
|
|
1815
1716
|
const compactMemoryCompressionForAgent = (compression = {}) => ({
|
|
1816
1717
|
turn_summary: (compression.turn_summary || []).slice(-3),
|
|
1817
1718
|
session_tools: (((compression.session_summary || {}).tools) || []).slice(-6),
|
|
1818
|
-
agenda_state: ((compression.session_summary || {}).agenda_state) ? {
|
|
1819
|
-
has_availability: Boolean(compression.session_summary.agenda_state.has_availability),
|
|
1820
|
-
has_pre_reservation: Boolean(compression.session_summary.agenda_state.has_pre_reservation),
|
|
1821
|
-
has_confirmation: Boolean(compression.session_summary.agenda_state.has_confirmation),
|
|
1822
|
-
has_pending_pre_reservation: Boolean(compression.session_summary.agenda_state.has_pending_pre_reservation),
|
|
1823
|
-
} : undefined,
|
|
1824
1719
|
active_memory_count: ((compression.workflow_summary || {}).active_memory_count) || 0,
|
|
1825
1720
|
});
|
|
1826
1721
|
const compactActionLedgerForAgent = (ledger = [], maxItems = 6, includeResults = true) => pruneByLimit(ledger || [], maxItems).map((item) => cleanContextValue({
|
|
@@ -1829,7 +1724,9 @@ const compactActionLedgerForAgent = (ledger = [], maxItems = 6, includeResults =
|
|
|
1829
1724
|
tool: item.tool || item.name,
|
|
1830
1725
|
status: item.status,
|
|
1831
1726
|
at: item.at,
|
|
1832
|
-
|
|
1727
|
+
reason: item.reason,
|
|
1728
|
+
input: item.input,
|
|
1729
|
+
result: includeResults ? compactToolResult(item.result, 180) : undefined,
|
|
1833
1730
|
}));
|
|
1834
1731
|
const compactEntityTimelineForAgent = (timeline = [], maxItems = 8) => pruneByLimit(timeline || [], maxItems).map((item) => cleanContextValue({
|
|
1835
1732
|
entity: item.entity || item.source || item.name,
|
|
@@ -2047,8 +1944,8 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
|
|
|
2047
1944
|
section: 'context_header',
|
|
2048
1945
|
title: 'Tembory context',
|
|
2049
1946
|
value: compactForAgent || compactStateSections
|
|
2050
|
-
? 'Read-only memory. Conversation frame is authoritative for
|
|
2051
|
-
: '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.',
|
|
2052
1949
|
});
|
|
2053
1950
|
}
|
|
2054
1951
|
sections.push({
|
|
@@ -2184,9 +2081,9 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
|
|
|
2184
2081
|
value: {
|
|
2185
2082
|
blocked_tools: guard.blockedTools,
|
|
2186
2083
|
reasons: guard.reasons,
|
|
2187
|
-
instruction: guard.blockedTools.length
|
|
2084
|
+
instruction: guard.instruction || (guard.blockedTools.length
|
|
2188
2085
|
? `Do not call these tools now: ${guard.blockedTools.join(', ')}. Ask for the missing context or execute the prerequisite step first.`
|
|
2189
|
-
: 'No downstream tool is blocked by missing memory prerequisites.',
|
|
2086
|
+
: 'No downstream tool is blocked by missing memory prerequisites.'),
|
|
2190
2087
|
},
|
|
2191
2088
|
});
|
|
2192
2089
|
sections.push({
|
|
@@ -3712,8 +3609,6 @@ exports.__private = {
|
|
|
3712
3609
|
explicitToolHistoryItemsFromMemory,
|
|
3713
3610
|
toolHistoryFromMemory,
|
|
3714
3611
|
cleanAssistantTranscriptText,
|
|
3715
|
-
availabilityFromRecentMessages,
|
|
3716
|
-
preReservationFromRecentMessages,
|
|
3717
3612
|
buildActionDirective,
|
|
3718
3613
|
recentMessageFromMemory,
|
|
3719
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));
|