n8n-nodes-tembory 1.0.27 → 1.0.29

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,21 @@
2
2
 
3
3
  Node de memoria operacional da Tembory para agentes de IA no n8n.
4
4
 
5
- Versao atual: `1.0.27`.
5
+ Versao atual: `1.0.29`.
6
+
7
+ ## 1.0.29
8
+
9
+ - Considera disponibilidade recente no transcript como contexto operacional valido quando `tool_history` ainda nao foi recuperado no turno.
10
+ - Evita repetir `agenda_consultar_disponibilidade` apos o agente ja ter respondido vagas no historico recente.
11
+ - Direciona selecao de horario como "quero dia 13" para `agenda_pre_reservar_horario` quando ha vagas recentes, mesmo sem marker estruturado no contexto atual.
12
+
13
+ ## 1.0.28
14
+
15
+ - Limpa blocos tecnicos de tool (`Used tools` / `Calling ...`) antes de salvar mensagens do assistente no transcript.
16
+ - Mantem tool history estruturado separado do texto da conversa, inclusive no modo com embedding conectado pelo n8n.
17
+ - Ordena o frame de conversa com mensagem do usuario antes da resposta do assistente quando o timestamp empata.
18
+ - Extrai fatos basicos do usuario como nome, idade, cidade e interesses a partir de mensagens normais.
19
+ - Reduz duplicacao no contexto compacto removendo markers de recent message/tool quando ja ha secoes estruturadas.
6
20
 
7
21
  ## 1.0.27
8
22
 
@@ -190,6 +190,15 @@ const withTemboryScore = (m, temboryScore) => {
190
190
  return { ...m, temboryScore };
191
191
  };
192
192
  const normalizeFactValue = (value) => String(value || '').replace(/\*\*/g, '').replace(/\s+/g, ' ').replace(/[.,;:]+$/g, '').trim();
193
+ const cleanAssistantTranscriptText = (value) => {
194
+ let text = String(value || '');
195
+ if (!text.trim())
196
+ return '';
197
+ text = text.replace(/^\s*\[Used tools:[\s\S]*?\]\]\s*/i, '');
198
+ text = text.replace(/^Calling\s+[A-Za-z0-9_.:-]+\s+with input:\s*\{[^\n]*\}\s*/gim, '');
199
+ text = text.replace(/^\s*\[tool_events_extracted\][^\n]*\n?/gim, '');
200
+ return text.replace(/\n{3,}/g, '\n\n').trim();
201
+ };
193
202
  const profileSourceRank = (source = '') => {
194
203
  if (/^user_message$/i.test(source))
195
204
  return 100;
@@ -294,9 +303,17 @@ const extractProfileFactsFromText = (text, source = 'message', at = nowIso()) =>
294
303
  const phone = /(?:telefone|tel|celular|whatsapp|whats)\s*(?:e|é|:)?\s*(\+?\d[\d\s().-]{7,}\d)/i.exec(content) || /(?:^|\s)(\+?55\s*)?(?:\(?\d{2}\)?\s*)?\d{4,5}[-.\s]?\d{4}(?:\s|$)/.exec(content);
295
304
  if (phone && canExtractStrongProfileFacts)
296
305
  setProfileFact(facts, 'phone', (phone[1] || phone[0]).trim(), source, at);
297
- const name = /(?:meu nome (?:e)|me chamo|sou o|sou a)\s+([A-ZÀ-Ú][A-Za-zÀ-ÿ]+(?:\s+[A-ZÀ-Ú][A-Za-zÀ-ÿ]+){0,3})/i.exec(content);
306
+ const name = /(?:meu nome (?:e|é|eh|he)|me chamo|sou o|sou a)\s+([A-ZÀ-Ú][A-Za-zÀ-ÿ]+(?:\s+[A-ZÀ-Ú][A-Za-zÀ-ÿ]+){0,3})/i.exec(content);
298
307
  if (name && canExtractStrongProfileFacts)
299
308
  setProfileFact(facts, 'name', name[1], source, at);
309
+ const age = /(?:tenho|idade\s*(?:e|é|:)?|sou de)\s+(\d{1,3})\s*anos?\b/i.exec(content);
310
+ if (age && canExtractStrongProfileFacts)
311
+ setProfileFact(facts, 'age', age[1], source, at);
312
+ const city = /(?:moro em|sou de|vivo em|resido em)\s+([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ\s'.-]{1,80})/i.exec(content);
313
+ if (city && canExtractStrongProfileFacts) {
314
+ const cityValue = city[1].replace(/\s+(?:e|mas|porque|pois|com|pra|para)\b.*$/i, '').trim();
315
+ setProfileFact(facts, 'city', cityValue, source, at);
316
+ }
300
317
  const company = /(?:sou|trabalho|falo|venho)\s+(?:da|do|na|no|pela|pelo)\s+([A-ZÀ-Ú][A-Za-zÀ-ÿ0-9&._ -]{1,60})/i.exec(content) || /(?:empresa|companhia)\s*(?:e|é|:)?\s*([A-ZÀ-Ú][A-Za-zÀ-ÿ0-9&._ -]{1,60})/i.exec(content);
301
318
  const companyValue = company === null || company === void 0 ? void 0 : company[1].replace(/\s+e\s+(?:meu|minha|telefone|tel|email|e-mail)\b.*$/i, '').trim();
302
319
  if (companyValue && canExtractStrongProfileFacts && isPlausibleCompanyValue(companyValue))
@@ -318,6 +335,9 @@ const extractProfileFactsFromText = (text, source = 'message', at = nowIso()) =>
318
335
  const interest = /(?:interesse|interessado|preciso|quero|busco)\s+(?:em|de|por)?\s*(.{4,120})/i.exec(content);
319
336
  if (interest)
320
337
  addProfileListFact(facts, 'interests', interest[1], source, at);
338
+ const likes = /(?:gosto de|curto|adoro)\s+(.{3,120})/i.exec(content);
339
+ if (likes)
340
+ addProfileListFact(facts, 'interests', likes[1], source, at);
321
341
  return facts;
322
342
  };
323
343
  const mergeProfileFacts = (...factSets) => {
@@ -388,11 +408,32 @@ const safeParseToolPayload = (value) => {
388
408
  const compactToolPayload = (value) => truncate(typeof value === 'string' ? value : safeStringify(value), 900);
389
409
  const maybeToolResult = (tool, includeResults = true) => includeResults === false ? undefined : compactToolPayload(safeParseToolPayload(tool === null || tool === void 0 ? void 0 : tool.result));
390
410
  const containsSelectedSlot = (text = '') => /\b(?:às|as|das|para)?\s*\d{1,2}(?::\d{2}|h\d{0,2})?\b/i.test(String(text || '')) || /\b\d{1,2}\/\d{1,2}(?:\/\d{2,4})?\b/.test(String(text || ''));
411
+ const availabilityFromRecentMessages = (recentMessages = []) => {
412
+ const messages = Array.isArray(recentMessages) ? recentMessages : [];
413
+ const candidates = [...messages].reverse().filter((message) => {
414
+ const role = String(message.role || '').toLowerCase();
415
+ const text = String(message.content || message.text || message.memory || '');
416
+ if (role && role !== 'assistant' && role !== 'ai' && role !== 'system')
417
+ return false;
418
+ return /\b(?:tem|tenho|há|ha)\s+vagas?\b|\bop[cç][oõ]es?\s+dispon[ií]veis\b|\bavailable_slots\b/i.test(text)
419
+ && (/\b\d{1,2}\/\d{1,2}(?:\/\d{2,4})?\b/.test(text) || /\b\d{1,2}:\d{2}\b/.test(text));
420
+ });
421
+ const latest = candidates[0];
422
+ if (!latest)
423
+ return null;
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
+ };
391
430
  const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessages = [], includeResults = true) => {
392
431
  const tools = Array.isArray(toolHistory) ? toolHistory : [];
393
432
  const successfulTools = tools.filter((tool) => tool.ok !== false);
394
433
  const byName = (name) => successfulTools.filter((tool) => tool.name === name);
395
434
  const availability = byName('agenda_consultar_disponibilidade');
435
+ const recentAvailability = availability.length ? null : availabilityFromRecentMessages(recentMessages);
436
+ const hasAvailability = availability.length > 0 || Boolean(recentAvailability);
396
437
  const reservations = byName('agenda_pre_reservar_horario');
397
438
  const confirmations = byName('agenda_confirmar_agendamento');
398
439
  const lastTool = tools[tools.length - 1] || null;
@@ -400,18 +441,18 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
400
441
  const lastConfirmation = confirmations[confirmations.length - 1] || null;
401
442
  const hasPendingReservation = Boolean(lastReservation && (!lastConfirmation || String(lastReservation.at || '') > String(lastConfirmation.at || '')));
402
443
  const blockedWithoutContext = [];
403
- if (!availability.length)
444
+ if (!hasAvailability)
404
445
  blockedWithoutContext.push('agenda_pre_reservar_horario');
405
446
  if (!reservations.length)
406
447
  blockedWithoutContext.push('agenda_confirmar_agendamento');
407
448
  const guidance = [];
408
- if (!availability.length)
449
+ if (!hasAvailability)
409
450
  guidance.push('No availability result is known for this session; consult availability before reserving or confirming.');
410
451
  else if (hasPendingReservation)
411
452
  guidance.push('A pre-reservation exists after the latest confirmation; confirmation can use the latest pre-reservation context.');
412
453
  else if (lastConfirmation)
413
454
  guidance.push('The latest reservation appears confirmed; do not confirm again unless the user explicitly asks to repeat or change it.');
414
- else if (availability.length)
455
+ else if (hasAvailability)
415
456
  guidance.push('Availability is known; if the user chooses one listed slot, reserve without repeating availability.');
416
457
  return {
417
458
  profile_complete: Boolean(profileFacts && profileFacts.name && profileFacts.company && profileFacts.email && profileFacts.phone),
@@ -425,11 +466,12 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
425
466
  agenda_confirmar_agendamento: confirmations.length,
426
467
  },
427
468
  agenda_state: {
428
- has_availability: availability.length > 0,
469
+ has_availability: hasAvailability,
429
470
  has_pre_reservation: reservations.length > 0,
430
471
  has_confirmation: confirmations.length > 0,
431
472
  has_pending_pre_reservation: hasPendingReservation,
432
- latest_availability_result: availability.length ? maybeToolResult(availability[availability.length - 1], includeResults) : null,
473
+ latest_availability_result: availability.length ? maybeToolResult(availability[availability.length - 1], includeResults) : (recentAvailability && includeResults !== false ? recentAvailability.text : null),
474
+ latest_availability_source: availability.length ? 'tool_history' : (recentAvailability ? recentAvailability.source : null),
433
475
  latest_pre_reservation_result: lastReservation ? maybeToolResult(lastReservation, includeResults) : null,
434
476
  latest_confirmation_result: lastConfirmation ? maybeToolResult(lastConfirmation, includeResults) : null,
435
477
  },
@@ -821,6 +863,16 @@ const extractToolCalls = (outputValues = {}) => {
821
863
  source: meta.source || 'intermediate_steps',
822
864
  }, calls.length + 1, { at }));
823
865
  };
866
+ if (Array.isArray(outputValues.__temboryToolCalls)) {
867
+ for (const tool of outputValues.__temboryToolCalls) {
868
+ push(tool.name || tool.tool || tool.toolName, tool.input || tool.toolInput || tool.args || '', tool.result || tool.output || tool.observation || '', tool.ok !== false, {
869
+ id: tool.id || tool.callId || tool.call_id || tool.toolCallId || tool.tool_call_id,
870
+ turnId: tool.turnId || tool.turn_id,
871
+ sequence: tool.sequence,
872
+ source: tool.source || 'n8n_tool_message',
873
+ });
874
+ }
875
+ }
824
876
  readDeep(outputValues, (obj) => {
825
877
  const name = obj.tool || obj.toolName || obj.name;
826
878
  const hasInput = obj.toolInput !== undefined || obj.input !== undefined || obj.args !== undefined;
@@ -1116,9 +1168,21 @@ const appendCurrentUserMessage = (items = [], query = '') => {
1116
1168
  return items || [];
1117
1169
  return (items || []).concat([{ role: 'user', content: truncate(content, 2000), at: nowIso(), source: 'current_input' }]);
1118
1170
  };
1171
+ const conversationRoleRank = (role = '') => /^(user|human)$/i.test(String(role)) ? 0 : /^(assistant|ai)$/i.test(String(role)) ? 1 : 2;
1172
+ const sortConversationChronological = (items = []) => [...(items || [])].sort((a, b) => {
1173
+ const atA = new Date(a.at || 0).getTime();
1174
+ const atB = new Date(b.at || 0).getTime();
1175
+ if (atA !== atB)
1176
+ return atA - atB;
1177
+ const roleA = conversationRoleRank(a.role);
1178
+ const roleB = conversationRoleRank(b.role);
1179
+ if (roleA !== roleB)
1180
+ return roleA - roleB;
1181
+ return String(a.content || '').localeCompare(String(b.content || ''));
1182
+ });
1119
1183
  const pruneConversationMessagesPreserveAnchors = (items = [], limit = 8) => {
1120
1184
  const normalizedLimit = Math.max(1, Number(limit) || 1);
1121
- const chronological = [...(items || [])].sort((a, b) => new Date(a.at || 0).getTime() - new Date(b.at || 0).getTime());
1185
+ const chronological = sortConversationChronological(items);
1122
1186
  const firstUser = chronological.find((msg) => /^(user|human)$/i.test(String(msg.role || '')));
1123
1187
  const tail = pruneByLimit(chronological, normalizedLimit);
1124
1188
  if (!firstUser)
@@ -1735,7 +1799,9 @@ const compactVectorMemoriesForAgent = (vectorMemories = [], toolHistory = [], ma
1735
1799
  return (vectorMemories || [])
1736
1800
  .map((memory) => contextMemoryText(memory, 220))
1737
1801
  .filter(Boolean)
1802
+ .filter((text) => !/^\[recent_message\]/i.test(text))
1738
1803
  .filter((text) => !hasStructuredTools || !/^\[tool_events_extracted\]/i.test(text))
1804
+ .filter((text) => !hasStructuredTools || !/^\[Used tools:/i.test(text))
1739
1805
  .slice(0, maxItems);
1740
1806
  };
1741
1807
  const modelResponseText = (response) => {
@@ -2605,7 +2671,7 @@ class Mem0Memory {
2605
2671
  saveContext: async (inputValues = {}, outputValues = {}) => {
2606
2672
  loadCache.clear();
2607
2673
  const input = pickText(inputValues, ['input', 'chatInput', 'text', 'query', 'question']);
2608
- const output = pickText(outputValues, ['output', 'response', 'text', 'answer']);
2674
+ const output = cleanAssistantTranscriptText(pickText(outputValues, ['output', 'response', 'text', 'answer']));
2609
2675
  if (input)
2610
2676
  currentMessages.push(toBaseMessage({ role: 'user', content: input }));
2611
2677
  if (output)
@@ -2641,10 +2707,12 @@ class Mem0Memory {
2641
2707
  if (inputParts.length)
2642
2708
  inputValues.input = inputParts.join('\n');
2643
2709
  const toolContext = toolCalls.length
2644
- ? `[Used tools: ${toolCalls.map((tool) => `Tool: ${tool.name}, Input: ${tool.input}, Result: ${tool.result}`).join('; ')}] `
2645
- : '';
2646
- if (outputParts.length || toolContext)
2647
- outputValues.output = `${toolContext}${outputParts.join('\n')}`.trim();
2710
+ ? toolCalls
2711
+ : [];
2712
+ if (outputParts.length)
2713
+ outputValues.output = outputParts.join('\n').trim();
2714
+ if (toolContext.length)
2715
+ outputValues.__temboryToolCalls = toolContext;
2648
2716
  if (inputValues.input || outputValues.output)
2649
2717
  await Mem0Memory.prototype.saveContextForItem.call(this, itemIndex, inputValues, outputValues);
2650
2718
  }
@@ -2655,7 +2723,8 @@ class Mem0Memory {
2655
2723
  const store = getMemoryStore(this);
2656
2724
  const key = userKeyFrom(threadId, adv, project);
2657
2725
  const input = stripThreadTestPrefix(pickText(inputValues, ['input', 'chatInput', 'text', 'query', 'question']));
2658
- const output = pickText(outputValues, ['output', 'response', 'text', 'answer']);
2726
+ const rawOutput = pickText(outputValues, ['output', 'response', 'text', 'answer']);
2727
+ const output = cleanAssistantTranscriptText(rawOutput);
2659
2728
  const toolCalls = extractToolCalls(outputValues);
2660
2729
  const recentForMem0 = [];
2661
2730
  const profileFromTurn = extractProfileFactsFromText(input, 'user_message', nowIso());
@@ -2777,6 +2846,8 @@ class Mem0Memory {
2777
2846
  kind: 'tool_facts',
2778
2847
  source: 'n8n_connected_embedding',
2779
2848
  }, ids));
2849
+ }
2850
+ if (adv.includeToolHistory !== false && toolCalls.length) {
2780
2851
  for (const tool of toolCalls) {
2781
2852
  clientMemories.push(await createClientVectorMemory(connectedEmbedding, encodeToolCall(tool, threadId), {
2782
2853
  kind: 'tool_history',
@@ -3588,9 +3659,12 @@ exports.__private = {
3588
3659
  toolHistoryItemsFromMemory,
3589
3660
  explicitToolHistoryItemsFromMemory,
3590
3661
  toolHistoryFromMemory,
3662
+ cleanAssistantTranscriptText,
3663
+ availabilityFromRecentMessages,
3591
3664
  recentMessageFromMemory,
3592
3665
  previousUserFallbackFromWorkingMemory,
3593
3666
  firstUserMessageFromConversation,
3667
+ sortConversationChronological,
3594
3668
  pruneConversationMessagesPreserveAnchors,
3595
3669
  dedupeToolHistory,
3596
3670
  applyToolHistoryWindow,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tembory",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
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",