n8n-nodes-tembory 1.0.26 → 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,21 @@
2
2
 
3
3
  Node de memoria operacional da Tembory para agentes de IA no n8n.
4
4
 
5
- Versao atual: `1.0.26`.
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.
14
+
15
+ ## 1.0.27
16
+
17
+ - Aumenta a janela deterministica de conversa para 30 mensagens por padrao em todos os presets de producao.
18
+ - Entrega `conversation_history_chronological` e `all_user_messages_chronological` no frame de conversa.
19
+ - Para conversas curtas/medias, o historico completo recente vira a fonte primaria; vetor e resumo ficam como complemento.
6
20
 
7
21
  ## 1.0.26
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) => {
@@ -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;
@@ -925,7 +955,7 @@ const applyOperationalPreset = (advanced = {}) => {
925
955
  topK: 8,
926
956
  lastN: 12,
927
957
  toolHistoryLastN: 15,
928
- recentMessagesLastN: 8,
958
+ recentMessagesLastN: 30,
929
959
  },
930
960
  productionBalanced: {
931
961
  summarySource: 'auto',
@@ -958,7 +988,7 @@ const applyOperationalPreset = (advanced = {}) => {
958
988
  topK: 6,
959
989
  lastN: 8,
960
990
  toolHistoryLastN: 10,
961
- recentMessagesLastN: 6,
991
+ recentMessagesLastN: 30,
962
992
  vectorMemoryMaxChars: 360,
963
993
  contextMaxChars: 10000,
964
994
  connectedModelSummaryMaxChars: 1200,
@@ -995,7 +1025,7 @@ const applyOperationalPreset = (advanced = {}) => {
995
1025
  topK: 4,
996
1026
  lastN: 4,
997
1027
  toolHistoryLastN: 5,
998
- recentMessagesLastN: 4,
1028
+ recentMessagesLastN: 30,
999
1029
  vectorMemoryMaxChars: 260,
1000
1030
  contextMaxChars: 7000,
1001
1031
  connectedModelSummaryMaxChars: 1000,
@@ -1033,7 +1063,7 @@ const applyOperationalPreset = (advanced = {}) => {
1033
1063
  lastN: 4,
1034
1064
  maxReturn: 6,
1035
1065
  toolHistoryLastN: 6,
1036
- recentMessagesLastN: 2,
1066
+ recentMessagesLastN: 30,
1037
1067
  vectorMemoryMaxChars: 220,
1038
1068
  contextMaxChars: 7000,
1039
1069
  connectedModelSummaryMaxChars: 1200,
@@ -1063,7 +1093,7 @@ const applyOperationalPreset = (advanced = {}) => {
1063
1093
  topK: 10,
1064
1094
  lastN: 20,
1065
1095
  toolHistoryLastN: 20,
1066
- recentMessagesLastN: 12,
1096
+ recentMessagesLastN: 30,
1067
1097
  payloadFormat: 'auditJson',
1068
1098
  },
1069
1099
  };
@@ -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)
@@ -1225,21 +1267,24 @@ const buildConversationFrame = ({ query = '', recentMessages = [], workingMemory
1225
1267
  const previousUser = previousUserMessageForQuery(query, recentMessages);
1226
1268
  const firstUser = firstUserMessageFromConversation(recentMessages);
1227
1269
  const previousUserMessage = previousUser ? truncate(previousUser.content, 500) : previousUserFallbackFromWorkingMemory(query, workingMemory);
1228
- const chronological = pruneByLimit(recentMessages || [], 10).map((msg) => ({
1270
+ const chronological = (recentMessages || []).map((msg) => ({
1229
1271
  role: msg.role,
1230
1272
  content: truncate(msg.content, 500),
1231
1273
  at: msg.at,
1232
1274
  }));
1275
+ const userMessagesChronological = chronological.filter((msg) => /^(user|human)$/i.test(String(msg.role || '')));
1233
1276
  const frame = cleanContextValue({
1234
1277
  current_user_message: currentUser ? truncate(currentUser.content, 500) : truncate(query, 500),
1235
1278
  first_user_message: firstUser ? truncate(firstUser.content, 500) : null,
1236
1279
  previous_user_message: previousUserMessage,
1280
+ conversation_history_chronological: chronological,
1237
1281
  recent_messages_chronological: chronological,
1282
+ all_user_messages_chronological: userMessagesChronological,
1238
1283
  recent_user_messages: users
1239
1284
  .filter((msg) => normalizeIntentText(msg.content).trim() !== normalizedQuery)
1240
1285
  .slice(0, 5)
1241
1286
  .map((msg) => ({ role: msg.role, content: truncate(msg.content, 500), at: msg.at })),
1242
- instruction: 'This is the authoritative short-term conversation frame. If the user asks about first/current/previous/last client messages, answer from first_user_message/current_user_message/previous_user_message/recent_user_messages before using vector memories, summaries, operational state, or tool history.',
1287
+ instruction: 'This is the authoritative short-term conversation frame. If the user asks about first/current/previous/last client messages or what they already said, answer from first_user_message/current_user_message/previous_user_message/conversation_history_chronological/all_user_messages_chronological before using vector memories, summaries, operational state, or tool history.',
1243
1288
  });
1244
1289
  if (!previousUserMessage)
1245
1290
  frame.previous_user_message = null;
@@ -1732,7 +1777,9 @@ const compactVectorMemoriesForAgent = (vectorMemories = [], toolHistory = [], ma
1732
1777
  return (vectorMemories || [])
1733
1778
  .map((memory) => contextMemoryText(memory, 220))
1734
1779
  .filter(Boolean)
1780
+ .filter((text) => !/^\[recent_message\]/i.test(text))
1735
1781
  .filter((text) => !hasStructuredTools || !/^\[tool_events_extracted\]/i.test(text))
1782
+ .filter((text) => !hasStructuredTools || !/^\[Used tools:/i.test(text))
1736
1783
  .slice(0, maxItems);
1737
1784
  };
1738
1785
  const modelResponseText = (response) => {
@@ -1934,8 +1981,8 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
1934
1981
  section: 'context_header',
1935
1982
  title: 'Tembory context',
1936
1983
  value: compactForAgent || compactStateSections
1937
- ? 'Read-only memory. Conversation frame is authoritative for first/current/previous client messages. Follow next_expected_action when present. Before calling downstream tools, verify required prior tool context in tool_history or operational_state. Do not repeat tools listed in do_not_repeat_tools.'
1938
- : 'Use this context as read-only memory. Prefer it over guessing. Do not mention internal section names to the user. The Conversation frame is authoritative for first, current, previous, and recent client messages. Treat next_expected_action as an instruction, not as a suggestion. If it says to call a tool now, call that tool instead of asking the user the same question again. If the user asks to continue, chooses a slot, says ok/sim, reserve, confirm, update, cancel, or performs any downstream action that depends on a prior tool result, first verify the required prior result in tool_history, recent_messages, or vector memories. If the required prior result is absent, do not call the downstream tool; ask for the missing context or call the appropriate prerequisite tool.',
1984
+ ? 'Read-only memory. Conversation frame is authoritative for the full recent transcript, including first/current/previous client messages. Follow next_expected_action when present. Before calling downstream tools, verify required prior tool context in tool_history or operational_state. Do not repeat tools listed in do_not_repeat_tools.'
1985
+ : 'Use this context as read-only memory. Prefer it over guessing. Do not mention internal section names to the user. The Conversation frame is authoritative for the full recent transcript, including first, current, previous, and recent client messages. Treat next_expected_action as an instruction, not as a suggestion. If it says to call a tool now, call that tool instead of asking the user the same question again. If the user asks to continue, chooses a slot, says ok/sim, reserve, confirm, update, cancel, or performs any downstream action that depends on a prior tool result, first verify the required prior result in tool_history, recent_messages, or vector memories. If the required prior result is absent, do not call the downstream tool; ask for the missing context or call the appropriate prerequisite tool.',
1939
1986
  });
1940
1987
  }
1941
1988
  sections.push({
@@ -2358,7 +2405,7 @@ class Mem0Memory {
2358
2405
  values: [
2359
2406
  { displayName: 'Incluir Profile Facts', name: 'includeProfileFacts', type: 'boolean', default: true },
2360
2407
  { displayName: 'Incluir Mensagens Recentes', name: 'includeRecentMessages', type: 'boolean', default: true },
2361
- { displayName: 'Últimas N Mensagens', name: 'recentMessagesLastN', type: 'number', default: 6 },
2408
+ { displayName: 'Últimas N Mensagens', name: 'recentMessagesLastN', type: 'number', default: 30 },
2362
2409
  { displayName: 'Incluir Highlights Recentes', name: 'includeRecentHighlights', type: 'boolean', default: true },
2363
2410
  { displayName: 'Máximo de Highlights', name: 'recentHighlightsMaxItems', type: 'number', default: 6 },
2364
2411
  { displayName: 'Incluir Grafo', name: 'includeRelations', type: 'boolean', default: true },
@@ -2513,7 +2560,7 @@ class Mem0Memory {
2513
2560
  { displayName: 'Incluir Entity Timeline', name: 'includeEntityTimeline', type: 'boolean', default: true, description: 'Injeta uma timeline compacta de entidades, fatos de perfil, relações do grafo e eventos importantes da sessão.' },
2514
2561
  { displayName: 'Incluir Compressão de Memória', name: 'includeMemoryCompression', type: 'boolean', default: true, description: 'Injeta resumos compactos de turno, sessão, entidades e workflow para reduzir ruído.' },
2515
2562
  { displayName: 'Incluir Mensagens Recentes', name: 'includeRecentMessages', type: 'boolean', default: true },
2516
- { displayName: 'Últimas N Mensagens', name: 'recentMessagesLastN', type: 'number', default: 8 },
2563
+ { displayName: 'Últimas N Mensagens', name: 'recentMessagesLastN', type: 'number', default: 30 },
2517
2564
  { displayName: 'Incluir Highlights Recentes', name: 'includeRecentHighlights', type: 'boolean', default: true },
2518
2565
  { displayName: 'Máximo de Highlights', name: 'recentHighlightsMaxItems', type: 'number', default: 6 },
2519
2566
  ],
@@ -2602,7 +2649,7 @@ class Mem0Memory {
2602
2649
  saveContext: async (inputValues = {}, outputValues = {}) => {
2603
2650
  loadCache.clear();
2604
2651
  const input = pickText(inputValues, ['input', 'chatInput', 'text', 'query', 'question']);
2605
- const output = pickText(outputValues, ['output', 'response', 'text', 'answer']);
2652
+ const output = cleanAssistantTranscriptText(pickText(outputValues, ['output', 'response', 'text', 'answer']));
2606
2653
  if (input)
2607
2654
  currentMessages.push(toBaseMessage({ role: 'user', content: input }));
2608
2655
  if (output)
@@ -2638,10 +2685,12 @@ class Mem0Memory {
2638
2685
  if (inputParts.length)
2639
2686
  inputValues.input = inputParts.join('\n');
2640
2687
  const toolContext = toolCalls.length
2641
- ? `[Used tools: ${toolCalls.map((tool) => `Tool: ${tool.name}, Input: ${tool.input}, Result: ${tool.result}`).join('; ')}] `
2642
- : '';
2643
- if (outputParts.length || toolContext)
2644
- 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;
2645
2694
  if (inputValues.input || outputValues.output)
2646
2695
  await Mem0Memory.prototype.saveContextForItem.call(this, itemIndex, inputValues, outputValues);
2647
2696
  }
@@ -2652,7 +2701,8 @@ class Mem0Memory {
2652
2701
  const store = getMemoryStore(this);
2653
2702
  const key = userKeyFrom(threadId, adv, project);
2654
2703
  const input = stripThreadTestPrefix(pickText(inputValues, ['input', 'chatInput', 'text', 'query', 'question']));
2655
- const output = pickText(outputValues, ['output', 'response', 'text', 'answer']);
2704
+ const rawOutput = pickText(outputValues, ['output', 'response', 'text', 'answer']);
2705
+ const output = cleanAssistantTranscriptText(rawOutput);
2656
2706
  const toolCalls = extractToolCalls(outputValues);
2657
2707
  const recentForMem0 = [];
2658
2708
  const profileFromTurn = extractProfileFactsFromText(input, 'user_message', nowIso());
@@ -2774,6 +2824,8 @@ class Mem0Memory {
2774
2824
  kind: 'tool_facts',
2775
2825
  source: 'n8n_connected_embedding',
2776
2826
  }, ids));
2827
+ }
2828
+ if (adv.includeToolHistory !== false && toolCalls.length) {
2777
2829
  for (const tool of toolCalls) {
2778
2830
  clientMemories.push(await createClientVectorMemory(connectedEmbedding, encodeToolCall(tool, threadId), {
2779
2831
  kind: 'tool_history',
@@ -3585,9 +3637,11 @@ exports.__private = {
3585
3637
  toolHistoryItemsFromMemory,
3586
3638
  explicitToolHistoryItemsFromMemory,
3587
3639
  toolHistoryFromMemory,
3640
+ cleanAssistantTranscriptText,
3588
3641
  recentMessageFromMemory,
3589
3642
  previousUserFallbackFromWorkingMemory,
3590
3643
  firstUserMessageFromConversation,
3644
+ sortConversationChronological,
3591
3645
  pruneConversationMessagesPreserveAnchors,
3592
3646
  dedupeToolHistory,
3593
3647
  applyToolHistoryWindow,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tembory",
3
- "version": "1.0.26",
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",