n8n-nodes-tembory 1.0.27 → 1.0.28

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,15 @@
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.28`.
6
+
7
+ ## 1.0.28
8
+
9
+ - Limpa blocos tecnicos de tool (`Used tools` / `Calling ...`) antes de salvar mensagens do assistente no transcript.
10
+ - Mantem tool history estruturado separado do texto da conversa, inclusive no modo com embedding conectado pelo n8n.
11
+ - Ordena o frame de conversa com mensagem do usuario antes da resposta do assistente quando o timestamp empata.
12
+ - Extrai fatos basicos do usuario como nome, idade, cidade e interesses a partir de mensagens normais.
13
+ - Reduz duplicacao no contexto compacto removendo markers de recent message/tool quando ja ha secoes estruturadas.
6
14
 
7
15
  ## 1.0.27
8
16
 
@@ -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) => {
@@ -821,6 +841,16 @@ const extractToolCalls = (outputValues = {}) => {
821
841
  source: meta.source || 'intermediate_steps',
822
842
  }, calls.length + 1, { at }));
823
843
  };
844
+ if (Array.isArray(outputValues.__temboryToolCalls)) {
845
+ for (const tool of outputValues.__temboryToolCalls) {
846
+ push(tool.name || tool.tool || tool.toolName, tool.input || tool.toolInput || tool.args || '', tool.result || tool.output || tool.observation || '', tool.ok !== false, {
847
+ id: tool.id || tool.callId || tool.call_id || tool.toolCallId || tool.tool_call_id,
848
+ turnId: tool.turnId || tool.turn_id,
849
+ sequence: tool.sequence,
850
+ source: tool.source || 'n8n_tool_message',
851
+ });
852
+ }
853
+ }
824
854
  readDeep(outputValues, (obj) => {
825
855
  const name = obj.tool || obj.toolName || obj.name;
826
856
  const hasInput = obj.toolInput !== undefined || obj.input !== undefined || obj.args !== undefined;
@@ -1116,9 +1146,21 @@ const appendCurrentUserMessage = (items = [], query = '') => {
1116
1146
  return items || [];
1117
1147
  return (items || []).concat([{ role: 'user', content: truncate(content, 2000), at: nowIso(), source: 'current_input' }]);
1118
1148
  };
1149
+ const conversationRoleRank = (role = '') => /^(user|human)$/i.test(String(role)) ? 0 : /^(assistant|ai)$/i.test(String(role)) ? 1 : 2;
1150
+ const sortConversationChronological = (items = []) => [...(items || [])].sort((a, b) => {
1151
+ const atA = new Date(a.at || 0).getTime();
1152
+ const atB = new Date(b.at || 0).getTime();
1153
+ if (atA !== atB)
1154
+ return atA - atB;
1155
+ const roleA = conversationRoleRank(a.role);
1156
+ const roleB = conversationRoleRank(b.role);
1157
+ if (roleA !== roleB)
1158
+ return roleA - roleB;
1159
+ return String(a.content || '').localeCompare(String(b.content || ''));
1160
+ });
1119
1161
  const pruneConversationMessagesPreserveAnchors = (items = [], limit = 8) => {
1120
1162
  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());
1163
+ const chronological = sortConversationChronological(items);
1122
1164
  const firstUser = chronological.find((msg) => /^(user|human)$/i.test(String(msg.role || '')));
1123
1165
  const tail = pruneByLimit(chronological, normalizedLimit);
1124
1166
  if (!firstUser)
@@ -1735,7 +1777,9 @@ const compactVectorMemoriesForAgent = (vectorMemories = [], toolHistory = [], ma
1735
1777
  return (vectorMemories || [])
1736
1778
  .map((memory) => contextMemoryText(memory, 220))
1737
1779
  .filter(Boolean)
1780
+ .filter((text) => !/^\[recent_message\]/i.test(text))
1738
1781
  .filter((text) => !hasStructuredTools || !/^\[tool_events_extracted\]/i.test(text))
1782
+ .filter((text) => !hasStructuredTools || !/^\[Used tools:/i.test(text))
1739
1783
  .slice(0, maxItems);
1740
1784
  };
1741
1785
  const modelResponseText = (response) => {
@@ -2605,7 +2649,7 @@ class Mem0Memory {
2605
2649
  saveContext: async (inputValues = {}, outputValues = {}) => {
2606
2650
  loadCache.clear();
2607
2651
  const input = pickText(inputValues, ['input', 'chatInput', 'text', 'query', 'question']);
2608
- const output = pickText(outputValues, ['output', 'response', 'text', 'answer']);
2652
+ const output = cleanAssistantTranscriptText(pickText(outputValues, ['output', 'response', 'text', 'answer']));
2609
2653
  if (input)
2610
2654
  currentMessages.push(toBaseMessage({ role: 'user', content: input }));
2611
2655
  if (output)
@@ -2641,10 +2685,12 @@ class Mem0Memory {
2641
2685
  if (inputParts.length)
2642
2686
  inputValues.input = inputParts.join('\n');
2643
2687
  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();
2688
+ ? toolCalls
2689
+ : [];
2690
+ if (outputParts.length)
2691
+ outputValues.output = outputParts.join('\n').trim();
2692
+ if (toolContext.length)
2693
+ outputValues.__temboryToolCalls = toolContext;
2648
2694
  if (inputValues.input || outputValues.output)
2649
2695
  await Mem0Memory.prototype.saveContextForItem.call(this, itemIndex, inputValues, outputValues);
2650
2696
  }
@@ -2655,7 +2701,8 @@ class Mem0Memory {
2655
2701
  const store = getMemoryStore(this);
2656
2702
  const key = userKeyFrom(threadId, adv, project);
2657
2703
  const input = stripThreadTestPrefix(pickText(inputValues, ['input', 'chatInput', 'text', 'query', 'question']));
2658
- const output = pickText(outputValues, ['output', 'response', 'text', 'answer']);
2704
+ const rawOutput = pickText(outputValues, ['output', 'response', 'text', 'answer']);
2705
+ const output = cleanAssistantTranscriptText(rawOutput);
2659
2706
  const toolCalls = extractToolCalls(outputValues);
2660
2707
  const recentForMem0 = [];
2661
2708
  const profileFromTurn = extractProfileFactsFromText(input, 'user_message', nowIso());
@@ -2777,6 +2824,8 @@ class Mem0Memory {
2777
2824
  kind: 'tool_facts',
2778
2825
  source: 'n8n_connected_embedding',
2779
2826
  }, ids));
2827
+ }
2828
+ if (adv.includeToolHistory !== false && toolCalls.length) {
2780
2829
  for (const tool of toolCalls) {
2781
2830
  clientMemories.push(await createClientVectorMemory(connectedEmbedding, encodeToolCall(tool, threadId), {
2782
2831
  kind: 'tool_history',
@@ -3588,9 +3637,11 @@ exports.__private = {
3588
3637
  toolHistoryItemsFromMemory,
3589
3638
  explicitToolHistoryItemsFromMemory,
3590
3639
  toolHistoryFromMemory,
3640
+ cleanAssistantTranscriptText,
3591
3641
  recentMessageFromMemory,
3592
3642
  previousUserFallbackFromWorkingMemory,
3593
3643
  firstUserMessageFromConversation,
3644
+ sortConversationChronological,
3594
3645
  pruneConversationMessagesPreserveAnchors,
3595
3646
  dedupeToolHistory,
3596
3647
  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.28",
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",