n8n-nodes-tembory 1.0.13 → 1.0.15
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 +17 -1
- package/dist/nodes/Mem0/Mem0Memory.node.js +319 -23
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -2,7 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
Node de memoria operacional da Tembory para agentes de IA no n8n.
|
|
4
4
|
|
|
5
|
-
Versao atual: `1.0.
|
|
5
|
+
Versao atual: `1.0.15`.
|
|
6
|
+
|
|
7
|
+
## 1.0.15
|
|
8
|
+
|
|
9
|
+
- Reorganiza a interface do node em `Configuração Tembory`, com grupos para Essenciais, Mais Inteligência, Controles Avançados de Busca, Resumo e Estado Persistente, e Diagnóstico e Auditoria.
|
|
10
|
+
- Mantém a coleção legada `Avançado` para compatibilidade com workflows já existentes.
|
|
11
|
+
- Define `Produção Balanceada` como preset padrão para novas configurações.
|
|
12
|
+
- Prioriza IDs estáveis de thread/conversa/contato/cliente antes de `sessionId` na expressão padrão do `ID da Thread`.
|
|
13
|
+
- Adiciona teste para validar a mesclagem dos grupos da nova UI com os presets operacionais.
|
|
14
|
+
|
|
15
|
+
## 1.0.14
|
|
16
|
+
|
|
17
|
+
- Adiciona Active Summary persistente por namespace/thread para continuar conversas depois de minutos, horas ou dias.
|
|
18
|
+
- Separa cache tecnico curto do resumo SLM da memoria ativa persistente.
|
|
19
|
+
- Aumenta os defaults do resumo SLM nos presets de producao para preservar mais inteligencia antes de comprimir para o agente.
|
|
20
|
+
- Adiciona diagnosticos para saber se o contexto veio de SLM novo, active summary ou cache tecnico.
|
|
21
|
+
- Expõe controles avancados de Active Summary e cache tecnico no node.
|
|
6
22
|
|
|
7
23
|
O Tembory entrega contexto rico para o AI Agent sem depender apenas do historico textual da conversa. Ele combina memoria semantica, working memory, decision state, fatos estaveis do lead, historico de tools, estado operacional, action ledger, timeline de entidades, compressao de memoria, grafo, mensagens recentes e diagnosticos.
|
|
8
24
|
|
|
@@ -862,14 +862,16 @@ const getMemoryStore = (ctx) => {
|
|
|
862
862
|
data.tembory.workingMemory = data.tembory.workingMemory || {};
|
|
863
863
|
data.tembory.decisionState = data.tembory.decisionState || {};
|
|
864
864
|
data.tembory.memoryCompression = data.tembory.memoryCompression || {};
|
|
865
|
+
data.tembory.activeSummary = data.tembory.activeSummary || {};
|
|
865
866
|
data.tembory.connectedModelSummaryCache = data.tembory.connectedModelSummaryCache || {};
|
|
866
867
|
return data.tembory;
|
|
867
868
|
}
|
|
868
869
|
catch {
|
|
869
|
-
global.__temboryMemory = global.__temboryMemory || { toolHistory: {}, recentMessages: {}, profileFacts: {}, workingMemory: {}, decisionState: {}, memoryCompression: {}, connectedModelSummaryCache: {} };
|
|
870
|
+
global.__temboryMemory = global.__temboryMemory || { toolHistory: {}, recentMessages: {}, profileFacts: {}, workingMemory: {}, decisionState: {}, memoryCompression: {}, activeSummary: {}, connectedModelSummaryCache: {} };
|
|
870
871
|
global.__temboryMemory.workingMemory = global.__temboryMemory.workingMemory || {};
|
|
871
872
|
global.__temboryMemory.decisionState = global.__temboryMemory.decisionState || {};
|
|
872
873
|
global.__temboryMemory.memoryCompression = global.__temboryMemory.memoryCompression || {};
|
|
874
|
+
global.__temboryMemory.activeSummary = global.__temboryMemory.activeSummary || {};
|
|
873
875
|
global.__temboryMemory.connectedModelSummaryCache = global.__temboryMemory.connectedModelSummaryCache || {};
|
|
874
876
|
return global.__temboryMemory;
|
|
875
877
|
}
|
|
@@ -881,7 +883,7 @@ const userKeyFrom = (threadId, adv, project = '') => {
|
|
|
881
883
|
return namespace ? `${namespace}::${base}` : base;
|
|
882
884
|
};
|
|
883
885
|
const applyOperationalPreset = (advanced = {}) => {
|
|
884
|
-
const preset = String(advanced.operationPreset || '
|
|
886
|
+
const preset = String(advanced.operationPreset || 'productionBalanced');
|
|
885
887
|
const presets = {
|
|
886
888
|
diagnostic: {
|
|
887
889
|
summarySource: 'auto',
|
|
@@ -912,6 +914,12 @@ const applyOperationalPreset = (advanced = {}) => {
|
|
|
912
914
|
productionBalanced: {
|
|
913
915
|
summarySource: 'auto',
|
|
914
916
|
includeConnectedModelSummary: true,
|
|
917
|
+
includeActiveSummary: true,
|
|
918
|
+
persistActiveSummary: true,
|
|
919
|
+
activeSummaryMaxChars: 1800,
|
|
920
|
+
activeSummaryRetentionDays: 30,
|
|
921
|
+
enableTransientSummaryCache: true,
|
|
922
|
+
transientSummaryCacheTTLSeconds: 300,
|
|
915
923
|
compactStateSections: true,
|
|
916
924
|
includeContextHeader: true,
|
|
917
925
|
includeSummary: true,
|
|
@@ -937,12 +945,18 @@ const applyOperationalPreset = (advanced = {}) => {
|
|
|
937
945
|
recentMessagesLastN: 6,
|
|
938
946
|
vectorMemoryMaxChars: 360,
|
|
939
947
|
contextMaxChars: 10000,
|
|
940
|
-
connectedModelSummaryMaxChars:
|
|
941
|
-
connectedModelSummaryInputMaxChars:
|
|
948
|
+
connectedModelSummaryMaxChars: 1200,
|
|
949
|
+
connectedModelSummaryInputMaxChars: 4200,
|
|
942
950
|
},
|
|
943
951
|
productionCheap: {
|
|
944
952
|
summarySource: 'activeContext',
|
|
945
953
|
includeConnectedModelSummary: true,
|
|
954
|
+
includeActiveSummary: true,
|
|
955
|
+
persistActiveSummary: true,
|
|
956
|
+
activeSummaryMaxChars: 1500,
|
|
957
|
+
activeSummaryRetentionDays: 30,
|
|
958
|
+
enableTransientSummaryCache: true,
|
|
959
|
+
transientSummaryCacheTTLSeconds: 300,
|
|
946
960
|
compactStateSections: true,
|
|
947
961
|
includeContextHeader: true,
|
|
948
962
|
includeSummary: false,
|
|
@@ -968,12 +982,18 @@ const applyOperationalPreset = (advanced = {}) => {
|
|
|
968
982
|
recentMessagesLastN: 4,
|
|
969
983
|
vectorMemoryMaxChars: 260,
|
|
970
984
|
contextMaxChars: 7000,
|
|
971
|
-
connectedModelSummaryMaxChars:
|
|
972
|
-
connectedModelSummaryInputMaxChars:
|
|
985
|
+
connectedModelSummaryMaxChars: 1000,
|
|
986
|
+
connectedModelSummaryInputMaxChars: 3600,
|
|
973
987
|
},
|
|
974
988
|
productionNano: {
|
|
975
989
|
summarySource: 'auto',
|
|
976
990
|
includeConnectedModelSummary: true,
|
|
991
|
+
includeActiveSummary: true,
|
|
992
|
+
persistActiveSummary: true,
|
|
993
|
+
activeSummaryMaxChars: 1600,
|
|
994
|
+
activeSummaryRetentionDays: 30,
|
|
995
|
+
enableTransientSummaryCache: true,
|
|
996
|
+
transientSummaryCacheTTLSeconds: 300,
|
|
977
997
|
compactForAgent: true,
|
|
978
998
|
includeContextHeader: true,
|
|
979
999
|
includeSummary: true,
|
|
@@ -999,9 +1019,9 @@ const applyOperationalPreset = (advanced = {}) => {
|
|
|
999
1019
|
toolHistoryLastN: 6,
|
|
1000
1020
|
recentMessagesLastN: 2,
|
|
1001
1021
|
vectorMemoryMaxChars: 220,
|
|
1002
|
-
contextMaxChars:
|
|
1003
|
-
connectedModelSummaryMaxChars:
|
|
1004
|
-
connectedModelSummaryInputMaxChars:
|
|
1022
|
+
contextMaxChars: 7000,
|
|
1023
|
+
connectedModelSummaryMaxChars: 1200,
|
|
1024
|
+
connectedModelSummaryInputMaxChars: 4200,
|
|
1005
1025
|
},
|
|
1006
1026
|
audit: {
|
|
1007
1027
|
summarySource: 'auto',
|
|
@@ -1033,6 +1053,30 @@ const applyOperationalPreset = (advanced = {}) => {
|
|
|
1033
1053
|
};
|
|
1034
1054
|
return { ...(presets[preset === 'lab' ? 'diagnostic' : preset] || {}), ...advanced };
|
|
1035
1055
|
};
|
|
1056
|
+
const flattenAdvancedGroups = (groups = {}) => {
|
|
1057
|
+
const flattened = {};
|
|
1058
|
+
const groupNames = ['essentials', 'intelligence', 'retrievalControls', 'summaryState', 'diagnosticsAudit'];
|
|
1059
|
+
for (const groupName of groupNames) {
|
|
1060
|
+
const value = groups === null || groups === void 0 ? void 0 : groups[groupName];
|
|
1061
|
+
const groupValue = Array.isArray(value) ? value[0] : value;
|
|
1062
|
+
if (groupValue && typeof groupValue === 'object')
|
|
1063
|
+
Object.assign(flattened, groupValue);
|
|
1064
|
+
}
|
|
1065
|
+
return flattened;
|
|
1066
|
+
};
|
|
1067
|
+
const readAdvancedOptions = (ctx, itemIndex) => {
|
|
1068
|
+
let legacy = {};
|
|
1069
|
+
let grouped = {};
|
|
1070
|
+
try {
|
|
1071
|
+
legacy = ctx.getNodeParameter('advanced', itemIndex, {}) || {};
|
|
1072
|
+
}
|
|
1073
|
+
catch { }
|
|
1074
|
+
try {
|
|
1075
|
+
grouped = flattenAdvancedGroups(ctx.getNodeParameter('advancedGroups', itemIndex, {}) || {});
|
|
1076
|
+
}
|
|
1077
|
+
catch { }
|
|
1078
|
+
return { ...legacy, ...grouped };
|
|
1079
|
+
};
|
|
1036
1080
|
const pruneByLimit = (items, limit) => items.slice(Math.max(0, items.length - Math.max(1, Number(limit) || 1)));
|
|
1037
1081
|
const dedupeRecentMessages = (items) => {
|
|
1038
1082
|
const seen = new Set();
|
|
@@ -1582,7 +1626,34 @@ const cleanModelSummaryText = (value, max = 900) => {
|
|
|
1582
1626
|
.trim();
|
|
1583
1627
|
return truncate(text, max);
|
|
1584
1628
|
};
|
|
1585
|
-
const
|
|
1629
|
+
const activeSummaryIsFresh = (entry, adv = {}) => {
|
|
1630
|
+
if (!entry || !entry.summary)
|
|
1631
|
+
return false;
|
|
1632
|
+
const retentionDays = Number(adv.activeSummaryRetentionDays ?? 30);
|
|
1633
|
+
if (retentionDays <= 0)
|
|
1634
|
+
return true;
|
|
1635
|
+
return Date.now() - Number(entry.updatedAt || entry.at || 0) < retentionDays * 86400000;
|
|
1636
|
+
};
|
|
1637
|
+
const readActiveSummary = (store, key, adv = {}) => {
|
|
1638
|
+
if (adv.includeActiveSummary === false)
|
|
1639
|
+
return '';
|
|
1640
|
+
const entry = store.activeSummary?.[key];
|
|
1641
|
+
if (!activeSummaryIsFresh(entry, adv))
|
|
1642
|
+
return '';
|
|
1643
|
+
return truncate(String(entry.summary || ''), Number(adv.activeSummaryMaxChars || 1600));
|
|
1644
|
+
};
|
|
1645
|
+
const writeActiveSummary = (store, key, summary, adv = {}) => {
|
|
1646
|
+
if (!store || !key || adv.persistActiveSummary === false || !summary)
|
|
1647
|
+
return false;
|
|
1648
|
+
store.activeSummary = store.activeSummary || {};
|
|
1649
|
+
store.activeSummary[key] = {
|
|
1650
|
+
summary: truncate(String(summary), Number(adv.activeSummaryMaxChars || 1600)),
|
|
1651
|
+
updatedAt: Date.now(),
|
|
1652
|
+
source: 'slm',
|
|
1653
|
+
};
|
|
1654
|
+
return true;
|
|
1655
|
+
};
|
|
1656
|
+
const buildConnectedModelSummaryInput = ({ query, activeSummary, profileFacts, workingMemory, decisionState, memoryCompression, operationalState, toolHistory, recentMessages, vectorMemories, highlights, adv }) => {
|
|
1586
1657
|
const source = String(adv.summarySource || 'auto');
|
|
1587
1658
|
if (source === 'off' || source === 'disabled' || adv.includeConnectedModelSummary === false)
|
|
1588
1659
|
return '';
|
|
@@ -1590,6 +1661,7 @@ const buildConnectedModelSummaryInput = ({ query, profileFacts, workingMemory, d
|
|
|
1590
1661
|
const includeActive = source === 'auto' || source === 'activeContext' || source === 'active';
|
|
1591
1662
|
const payload = cleanContextValue({
|
|
1592
1663
|
query: String(query || ''),
|
|
1664
|
+
existing_active_summary: activeSummary ? truncate(activeSummary, Number(adv.activeSummaryMaxChars || 1600)) : undefined,
|
|
1593
1665
|
active_context: includeActive ? {
|
|
1594
1666
|
profile_facts: renderProfileFacts(profileFacts),
|
|
1595
1667
|
working_memory: compactWorkingMemoryForAgent(workingMemory || {}),
|
|
@@ -1618,10 +1690,10 @@ const invokeConnectedModelSummary = async (connectedLanguageModel, summaryInput,
|
|
|
1618
1690
|
const response = await connectedLanguageModel.invoke([
|
|
1619
1691
|
toBaseMessage({
|
|
1620
1692
|
role: 'user',
|
|
1621
|
-
content: `
|
|
1693
|
+
content: `Update the Tembory active summary for the next agent turn. Return only concise Portuguese bullets, no JSON and no markdown table. Preserve IDs, dates, tool names, confirmed decisions, pending actions, constraints, contradictions, and do-not-repeat instructions. Prefer durable useful context over raw logs. Do not invent facts.\n\nContext:\n${summaryInput}`,
|
|
1622
1694
|
}),
|
|
1623
1695
|
]);
|
|
1624
|
-
return cleanModelSummaryText(response, Number(adv.connectedModelSummaryMaxChars ||
|
|
1696
|
+
return cleanModelSummaryText(response, Number(adv.connectedModelSummaryMaxChars || 1200));
|
|
1625
1697
|
};
|
|
1626
1698
|
const contextSizeOfMessages = (messages = []) => {
|
|
1627
1699
|
const perMessage = (messages || []).map((message, index) => {
|
|
@@ -1653,6 +1725,7 @@ const wrapTemboryMemory = (memory, ctx, memoryKey) => new Proxy(memory, {
|
|
|
1653
1725
|
context: response.temboryContext,
|
|
1654
1726
|
contextText: response.temboryContextText,
|
|
1655
1727
|
summary: response.temborySummary,
|
|
1728
|
+
activeSummary: response.temboryActiveSummary,
|
|
1656
1729
|
connectedModelSummary: response.temboryConnectedModelSummary,
|
|
1657
1730
|
workingMemory: response.temboryWorkingMemory,
|
|
1658
1731
|
decisionState: response.temboryDecisionState,
|
|
@@ -1700,7 +1773,7 @@ const wrapTemboryMemory = (memory, ctx, memoryKey) => new Proxy(memory, {
|
|
|
1700
1773
|
return target[prop];
|
|
1701
1774
|
},
|
|
1702
1775
|
});
|
|
1703
|
-
const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, workingMemory, decisionState, memoryCompression, operationalState, actionLedger, entityTimeline, vectorMemories, recentMessages, toolHistory, highlights, graph, diagnostics, connectedModelSummary, adv }) => {
|
|
1776
|
+
const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, workingMemory, decisionState, memoryCompression, operationalState, actionLedger, entityTimeline, vectorMemories, recentMessages, toolHistory, highlights, graph, diagnostics, activeSummary, connectedModelSummary, adv }) => {
|
|
1704
1777
|
const includeHeader = adv.includeContextHeader !== false;
|
|
1705
1778
|
const includeSummary = adv.includeSummary !== false;
|
|
1706
1779
|
const includeScores = adv.includeScores !== false;
|
|
@@ -1724,6 +1797,9 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
|
|
|
1724
1797
|
if (connectedModelSummary && adv.includeConnectedModelSummary !== false) {
|
|
1725
1798
|
sections.push({ section: 'connected_model_summary', title: 'SLM summary', value: connectedModelSummary });
|
|
1726
1799
|
}
|
|
1800
|
+
else if (activeSummary && adv.includeActiveSummary !== false) {
|
|
1801
|
+
sections.push({ section: 'active_summary', title: 'Active summary', value: activeSummary });
|
|
1802
|
+
}
|
|
1727
1803
|
sections.push({
|
|
1728
1804
|
section: 'working_memory',
|
|
1729
1805
|
title: 'Working memory',
|
|
@@ -1792,6 +1868,9 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
|
|
|
1792
1868
|
if (connectedModelSummary && adv.includeConnectedModelSummary !== false) {
|
|
1793
1869
|
sections.push({ section: 'connected_model_summary', title: 'SLM summary', value: connectedModelSummary });
|
|
1794
1870
|
}
|
|
1871
|
+
else if (activeSummary && adv.includeActiveSummary !== false) {
|
|
1872
|
+
sections.push({ section: 'active_summary', title: 'Active summary', value: activeSummary });
|
|
1873
|
+
}
|
|
1795
1874
|
sections.push({
|
|
1796
1875
|
section: 'working_memory',
|
|
1797
1876
|
title: 'Working memory',
|
|
@@ -2003,7 +2082,7 @@ class Mem0Memory {
|
|
|
2003
2082
|
displayName: 'ID da Thread',
|
|
2004
2083
|
name: 'threadId',
|
|
2005
2084
|
type: 'string',
|
|
2006
|
-
default: '={{ $json.threadId || $json.
|
|
2085
|
+
default: '={{ $json.threadId || $json.body?.threadId || $json.body?.conversationId || $json.conversationId || $json.body?.messages?.[0]?.contactId || $json.body?.contactId || $json.contactId || $json.customerId || $json.body?.customerId || $json.from || $json.sender || $json.sessionId || $executionId }}',
|
|
2007
2086
|
description: 'Identificador estável desta conversa/thread. Usado como user_id se nenhum User ID explícito for informado.',
|
|
2008
2087
|
},
|
|
2009
2088
|
{
|
|
@@ -2060,11 +2139,186 @@ class Mem0Memory {
|
|
|
2060
2139
|
description: 'Como o contexto recuperado será entregue ao agente antes da LLM.',
|
|
2061
2140
|
},
|
|
2062
2141
|
{
|
|
2063
|
-
displayName: '
|
|
2142
|
+
displayName: 'Configuração Tembory',
|
|
2143
|
+
name: 'advancedGroups',
|
|
2144
|
+
type: 'fixedCollection',
|
|
2145
|
+
placeholder: 'Adicionar Grupo',
|
|
2146
|
+
default: {
|
|
2147
|
+
essentials: {
|
|
2148
|
+
operationPreset: 'productionBalanced',
|
|
2149
|
+
includeContextHeader: true,
|
|
2150
|
+
compactStateSections: true,
|
|
2151
|
+
contextMaxChars: 10000,
|
|
2152
|
+
includeToolHistory: true,
|
|
2153
|
+
includeToolResults: true,
|
|
2154
|
+
includeWorkingMemory: true,
|
|
2155
|
+
includeDecisionState: true,
|
|
2156
|
+
includeOperationalState: true,
|
|
2157
|
+
includeMemoryCompression: true,
|
|
2158
|
+
},
|
|
2159
|
+
intelligence: {
|
|
2160
|
+
includeProfileFacts: true,
|
|
2161
|
+
includeRecentMessages: true,
|
|
2162
|
+
recentMessagesLastN: 6,
|
|
2163
|
+
includeRecentHighlights: true,
|
|
2164
|
+
recentHighlightsMaxItems: 6,
|
|
2165
|
+
includeRelations: true,
|
|
2166
|
+
maxRelations: 10,
|
|
2167
|
+
includeActionLedger: true,
|
|
2168
|
+
includeEntityTimeline: true,
|
|
2169
|
+
},
|
|
2170
|
+
retrievalControls: {
|
|
2171
|
+
topK: 6,
|
|
2172
|
+
rerank: true,
|
|
2173
|
+
lastN: 8,
|
|
2174
|
+
alpha: 0.65,
|
|
2175
|
+
halfLifeHours: 48,
|
|
2176
|
+
maxReturn: 12,
|
|
2177
|
+
mmr: true,
|
|
2178
|
+
mmrLambda: 0.5,
|
|
2179
|
+
},
|
|
2180
|
+
summaryState: {
|
|
2181
|
+
includeSummary: true,
|
|
2182
|
+
includeConnectedModelSummary: true,
|
|
2183
|
+
summarySource: 'auto',
|
|
2184
|
+
connectedModelSummaryInputMaxChars: 4200,
|
|
2185
|
+
connectedModelSummaryMaxChars: 1200,
|
|
2186
|
+
includeActiveSummary: true,
|
|
2187
|
+
persistActiveSummary: true,
|
|
2188
|
+
activeSummaryMaxChars: 1800,
|
|
2189
|
+
activeSummaryRetentionDays: 30,
|
|
2190
|
+
enableTransientSummaryCache: true,
|
|
2191
|
+
transientSummaryCacheTTLSeconds: 300,
|
|
2192
|
+
transientSummaryCacheMaxItems: 50,
|
|
2193
|
+
summaryMaxFacts: 4,
|
|
2194
|
+
},
|
|
2195
|
+
diagnosticsAudit: {
|
|
2196
|
+
includeScores: false,
|
|
2197
|
+
includeDiagnostics: false,
|
|
2198
|
+
persistToolFactsToMem0: false,
|
|
2199
|
+
includeToolHistorySemanticFallback: false,
|
|
2200
|
+
toolHistoryLastN: 10,
|
|
2201
|
+
toolHistoryTTLSeconds: 3600,
|
|
2202
|
+
},
|
|
2203
|
+
},
|
|
2204
|
+
typeOptions: {
|
|
2205
|
+
multipleValues: false,
|
|
2206
|
+
},
|
|
2207
|
+
description: 'Grupos recomendados para producao. Todos os recursos continuam disponiveis, mas separados por finalidade.',
|
|
2208
|
+
options: [
|
|
2209
|
+
{
|
|
2210
|
+
name: 'essentials',
|
|
2211
|
+
displayName: 'Essenciais',
|
|
2212
|
+
values: [
|
|
2213
|
+
{
|
|
2214
|
+
displayName: 'Preset Operacional',
|
|
2215
|
+
name: 'operationPreset',
|
|
2216
|
+
type: 'options',
|
|
2217
|
+
options: [
|
|
2218
|
+
{ name: 'Custom', value: 'custom' },
|
|
2219
|
+
{ name: 'Diagnóstico Completo', value: 'diagnostic' },
|
|
2220
|
+
{ name: 'Produção Balanceada', value: 'productionBalanced' },
|
|
2221
|
+
{ name: 'Produção Econômica', value: 'productionCheap' },
|
|
2222
|
+
{ name: 'Produção Nano/SLM', value: 'productionNano' },
|
|
2223
|
+
{ name: 'Auditoria', value: 'audit' },
|
|
2224
|
+
],
|
|
2225
|
+
default: 'productionBalanced',
|
|
2226
|
+
description: 'Preenche defaults seguros para contexto, historico de tools e memoria ativa.',
|
|
2227
|
+
},
|
|
2228
|
+
{ displayName: 'Incluir Cabeçalho de Contexto', name: 'includeContextHeader', type: 'boolean', default: true },
|
|
2229
|
+
{ displayName: 'Organizar Seções de Produção', name: 'compactStateSections', type: 'boolean', default: true },
|
|
2230
|
+
{ displayName: 'Máximo de Caracteres do Contexto', name: 'contextMaxChars', type: 'number', default: 10000 },
|
|
2231
|
+
{ displayName: 'Incluir Tool History', name: 'includeToolHistory', type: 'boolean', default: true },
|
|
2232
|
+
{ displayName: 'Incluir Resultado Das Tools', name: 'includeToolResults', type: 'boolean', default: true },
|
|
2233
|
+
{ displayName: 'Incluir Working Memory', name: 'includeWorkingMemory', type: 'boolean', default: true },
|
|
2234
|
+
{ displayName: 'Incluir Decision State', name: 'includeDecisionState', type: 'boolean', default: true },
|
|
2235
|
+
{ displayName: 'Incluir Estado Operacional', name: 'includeOperationalState', type: 'boolean', default: true },
|
|
2236
|
+
{ displayName: 'Incluir Compressão de Memória', name: 'includeMemoryCompression', type: 'boolean', default: true },
|
|
2237
|
+
],
|
|
2238
|
+
},
|
|
2239
|
+
{
|
|
2240
|
+
name: 'intelligence',
|
|
2241
|
+
displayName: 'Mais Inteligência',
|
|
2242
|
+
values: [
|
|
2243
|
+
{ displayName: 'Incluir Profile Facts', name: 'includeProfileFacts', type: 'boolean', default: true },
|
|
2244
|
+
{ displayName: 'Incluir Mensagens Recentes', name: 'includeRecentMessages', type: 'boolean', default: true },
|
|
2245
|
+
{ displayName: 'Últimas N Mensagens', name: 'recentMessagesLastN', type: 'number', default: 6 },
|
|
2246
|
+
{ displayName: 'Incluir Highlights Recentes', name: 'includeRecentHighlights', type: 'boolean', default: true },
|
|
2247
|
+
{ displayName: 'Máximo de Highlights', name: 'recentHighlightsMaxItems', type: 'number', default: 6 },
|
|
2248
|
+
{ displayName: 'Incluir Grafo', name: 'includeRelations', type: 'boolean', default: true },
|
|
2249
|
+
{ displayName: 'Máximo de Relações', name: 'maxRelations', type: 'number', default: 10 },
|
|
2250
|
+
{ displayName: 'Incluir Action Ledger', name: 'includeActionLedger', type: 'boolean', default: true },
|
|
2251
|
+
{ displayName: 'Incluir Entity Timeline', name: 'includeEntityTimeline', type: 'boolean', default: true },
|
|
2252
|
+
],
|
|
2253
|
+
},
|
|
2254
|
+
{
|
|
2255
|
+
name: 'retrievalControls',
|
|
2256
|
+
displayName: 'Controles Avançados de Busca',
|
|
2257
|
+
values: [
|
|
2258
|
+
{ displayName: 'Top K', name: 'topK', type: 'number', typeOptions: { minValue: 1 }, default: 6 },
|
|
2259
|
+
{ displayName: 'Rerank', name: 'rerank', type: 'boolean', default: true },
|
|
2260
|
+
{ displayName: 'Fields (lista separada por vírgula)', name: 'fields', type: 'string', default: '' },
|
|
2261
|
+
{ displayName: 'Filters (JSON)', name: 'filters', type: 'json', default: '{}' },
|
|
2262
|
+
{ displayName: 'Last N (recentes)', name: 'lastN', type: 'number', default: 8 },
|
|
2263
|
+
{ displayName: 'Alpha (peso semântico)', name: 'alpha', type: 'number', typeOptions: { minValue: 0, maxValue: 1, numberPrecision: 2 }, default: 0.65 },
|
|
2264
|
+
{ displayName: 'Half-life (horas)', name: 'halfLifeHours', type: 'number', typeOptions: { minValue: 1 }, default: 48 },
|
|
2265
|
+
{ displayName: 'Máximo a retornar', name: 'maxReturn', type: 'number', typeOptions: { minValue: 1 }, default: 12 },
|
|
2266
|
+
{ displayName: 'MMR (diversidade)', name: 'mmr', type: 'boolean', default: true },
|
|
2267
|
+
{ displayName: 'MMR Lambda', name: 'mmrLambda', type: 'number', typeOptions: { minValue: 0, maxValue: 1, numberPrecision: 2 }, default: 0.5 },
|
|
2268
|
+
{ displayName: 'Compactar Contexto Para Agente', name: 'compactForAgent', type: 'boolean', default: false },
|
|
2269
|
+
],
|
|
2270
|
+
},
|
|
2271
|
+
{
|
|
2272
|
+
name: 'summaryState',
|
|
2273
|
+
displayName: 'Resumo e Estado Persistente',
|
|
2274
|
+
values: [
|
|
2275
|
+
{ displayName: 'Incluir Resumo', name: 'includeSummary', type: 'boolean', default: true },
|
|
2276
|
+
{ displayName: 'Incluir Resumo do SLM', name: 'includeConnectedModelSummary', type: 'boolean', default: true },
|
|
2277
|
+
{
|
|
2278
|
+
displayName: 'Fonte do Resumo do SLM',
|
|
2279
|
+
name: 'summarySource',
|
|
2280
|
+
type: 'options',
|
|
2281
|
+
options: [
|
|
2282
|
+
{ name: 'Automático', value: 'auto' },
|
|
2283
|
+
{ name: 'Contexto Ativo', value: 'activeContext' },
|
|
2284
|
+
{ name: 'Somente Vetores', value: 'vectorOnly' },
|
|
2285
|
+
{ name: 'Desligado', value: 'off' },
|
|
2286
|
+
],
|
|
2287
|
+
default: 'auto',
|
|
2288
|
+
},
|
|
2289
|
+
{ displayName: 'Máximo de Caracteres de Entrada do SLM', name: 'connectedModelSummaryInputMaxChars', type: 'number', default: 4200 },
|
|
2290
|
+
{ displayName: 'Máximo de Caracteres do Resumo do SLM', name: 'connectedModelSummaryMaxChars', type: 'number', default: 1200 },
|
|
2291
|
+
{ displayName: 'Incluir Active Summary', name: 'includeActiveSummary', type: 'boolean', default: true },
|
|
2292
|
+
{ displayName: 'Persistir Active Summary', name: 'persistActiveSummary', type: 'boolean', default: true },
|
|
2293
|
+
{ displayName: 'Máximo de Caracteres do Active Summary', name: 'activeSummaryMaxChars', type: 'number', default: 1800 },
|
|
2294
|
+
{ displayName: 'Retenção do Active Summary (Dias)', name: 'activeSummaryRetentionDays', type: 'number', default: 30 },
|
|
2295
|
+
{ displayName: 'Ativar Cache Técnico do Resumo SLM', name: 'enableTransientSummaryCache', type: 'boolean', default: true },
|
|
2296
|
+
{ displayName: 'TTL do Cache Técnico SLM (Segundos)', name: 'transientSummaryCacheTTLSeconds', type: 'number', default: 300 },
|
|
2297
|
+
{ displayName: 'Máximo de Itens no Cache Técnico SLM', name: 'transientSummaryCacheMaxItems', type: 'number', default: 50 },
|
|
2298
|
+
{ displayName: 'Máximo de Fatos no Resumo', name: 'summaryMaxFacts', type: 'number', default: 4 },
|
|
2299
|
+
],
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
name: 'diagnosticsAudit',
|
|
2303
|
+
displayName: 'Diagnóstico e Auditoria',
|
|
2304
|
+
values: [
|
|
2305
|
+
{ displayName: 'Incluir Scores', name: 'includeScores', type: 'boolean', default: false },
|
|
2306
|
+
{ displayName: 'Incluir Diagnóstico', name: 'includeDiagnostics', type: 'boolean', default: false },
|
|
2307
|
+
{ displayName: 'Últimas N Tools', name: 'toolHistoryLastN', type: 'number', default: 10 },
|
|
2308
|
+
{ displayName: 'TTL Das Tools (Segundos)', name: 'toolHistoryTTLSeconds', type: 'number', default: 3600 },
|
|
2309
|
+
{ displayName: 'Persistir Tool Facts no Backend', name: 'persistToolFactsToMem0', type: 'boolean', default: false },
|
|
2310
|
+
{ displayName: 'Fallback Semântico Para Tools', name: 'includeToolHistorySemanticFallback', type: 'boolean', default: false },
|
|
2311
|
+
],
|
|
2312
|
+
},
|
|
2313
|
+
],
|
|
2314
|
+
},
|
|
2315
|
+
{
|
|
2316
|
+
displayName: 'Avançado Legado (Compatibilidade)',
|
|
2064
2317
|
name: 'advanced',
|
|
2065
2318
|
type: 'collection',
|
|
2066
2319
|
placeholder: 'Opções',
|
|
2067
2320
|
default: {},
|
|
2321
|
+
description: 'Campos legados preservados para workflows existentes. Para novas configuracoes, use Configuração Tembory.',
|
|
2068
2322
|
options: [
|
|
2069
2323
|
{ displayName: 'User ID', name: 'userId', type: 'string', default: '' },
|
|
2070
2324
|
{
|
|
@@ -2079,7 +2333,7 @@ class Mem0Memory {
|
|
|
2079
2333
|
{ name: 'Produção Nano/SLM', value: 'productionNano' },
|
|
2080
2334
|
{ name: 'Auditoria', value: 'audit' },
|
|
2081
2335
|
],
|
|
2082
|
-
default: '
|
|
2336
|
+
default: 'productionBalanced',
|
|
2083
2337
|
description: 'Preenche defaults seguros para contexto, diagnostico, historico de tools e mensagens recentes. Valores definidos manualmente continuam tendo prioridade.',
|
|
2084
2338
|
},
|
|
2085
2339
|
{ displayName: 'Agent ID', name: 'agentId', type: 'string', default: '' },
|
|
@@ -2115,7 +2369,14 @@ class Mem0Memory {
|
|
|
2115
2369
|
description: 'Define se o SLM resume working memory/tool state, memórias vetoriais, ambos ou nada.',
|
|
2116
2370
|
},
|
|
2117
2371
|
{ displayName: 'Máximo de Caracteres de Entrada do SLM', name: 'connectedModelSummaryInputMaxChars', type: 'number', default: 3000 },
|
|
2118
|
-
{ displayName: 'Máximo de Caracteres do Resumo do SLM', name: 'connectedModelSummaryMaxChars', type: 'number', default:
|
|
2372
|
+
{ displayName: 'Máximo de Caracteres do Resumo do SLM', name: 'connectedModelSummaryMaxChars', type: 'number', default: 1200 },
|
|
2373
|
+
{ displayName: 'Incluir Active Summary', name: 'includeActiveSummary', type: 'boolean', default: true, description: 'Carrega o resumo ativo persistente da thread/sessão quando disponível.' },
|
|
2374
|
+
{ displayName: 'Persistir Active Summary', name: 'persistActiveSummary', type: 'boolean', default: true, description: 'Salva o resumo atualizado pelo SLM para continuar a conversa depois de minutos, horas ou dias.' },
|
|
2375
|
+
{ displayName: 'Máximo de Caracteres do Active Summary', name: 'activeSummaryMaxChars', type: 'number', default: 1600 },
|
|
2376
|
+
{ displayName: 'Retenção do Active Summary (Dias)', name: 'activeSummaryRetentionDays', type: 'number', default: 30, description: 'Use 0 para não expirar pelo node. Não é cache técnico; é memória ativa persistente.' },
|
|
2377
|
+
{ displayName: 'Ativar Cache Técnico do Resumo SLM', name: 'enableTransientSummaryCache', type: 'boolean', default: true, description: 'Evita chamadas duplicadas ao SLM para o mesmo pacote de contexto. Não substitui memória persistente.' },
|
|
2378
|
+
{ displayName: 'TTL do Cache Técnico SLM (Segundos)', name: 'transientSummaryCacheTTLSeconds', type: 'number', default: 300 },
|
|
2379
|
+
{ displayName: 'Máximo de Itens no Cache Técnico SLM', name: 'transientSummaryCacheMaxItems', type: 'number', default: 50 },
|
|
2119
2380
|
{ displayName: 'Máximo de Fatos no Resumo', name: 'summaryMaxFacts', type: 'number', default: 4 },
|
|
2120
2381
|
{ displayName: 'Incluir Scores', name: 'includeScores', type: 'boolean', default: true },
|
|
2121
2382
|
{ displayName: 'Incluir Diagnóstico', name: 'includeDiagnostics', type: 'boolean', default: false },
|
|
@@ -2270,7 +2531,7 @@ class Mem0Memory {
|
|
|
2270
2531
|
async saveContextForItem(itemIndex, inputValues = {}, outputValues = {}) {
|
|
2271
2532
|
const threadId = this.getNodeParameter('threadId', itemIndex);
|
|
2272
2533
|
const project = this.getNodeParameter('project', itemIndex, '');
|
|
2273
|
-
const adv = applyOperationalPreset(this
|
|
2534
|
+
const adv = applyOperationalPreset(readAdvancedOptions(this, itemIndex));
|
|
2274
2535
|
const store = getMemoryStore(this);
|
|
2275
2536
|
const key = userKeyFrom(threadId, adv, project);
|
|
2276
2537
|
const input = pickText(inputValues, ['input', 'chatInput', 'text', 'query', 'question']);
|
|
@@ -2474,7 +2735,7 @@ class Mem0Memory {
|
|
|
2474
2735
|
let payloadFormat = this.getNodeParameter('payloadFormat', itemIndex, 'structured');
|
|
2475
2736
|
const threadId = this.getNodeParameter('threadId', itemIndex);
|
|
2476
2737
|
const project = this.getNodeParameter('project', itemIndex, '');
|
|
2477
|
-
const adv = applyOperationalPreset(this
|
|
2738
|
+
const adv = applyOperationalPreset(readAdvancedOptions(this, itemIndex));
|
|
2478
2739
|
payloadFormat = adv.payloadFormat || payloadFormat;
|
|
2479
2740
|
const store = getMemoryStore(this);
|
|
2480
2741
|
const key = userKeyFrom(threadId, adv, project);
|
|
@@ -2770,11 +3031,20 @@ class Mem0Memory {
|
|
|
2770
3031
|
vectorMemories,
|
|
2771
3032
|
maxItems: adv.compressionMaxItems || 6,
|
|
2772
3033
|
});
|
|
3034
|
+
const loadedActiveSummary = readActiveSummary(store, key, adv);
|
|
2773
3035
|
let connectedModelSummary = '';
|
|
3036
|
+
const summaryDiagnostics = {
|
|
3037
|
+
source: 'none',
|
|
3038
|
+
transientCacheHit: false,
|
|
3039
|
+
activeSummaryLoaded: Boolean(loadedActiveSummary),
|
|
3040
|
+
activeSummaryUpdated: false,
|
|
3041
|
+
summaryChars: 0,
|
|
3042
|
+
};
|
|
2774
3043
|
if (connectedLanguageModel && typeof connectedLanguageModel.invoke === 'function' && adv.includeSummary !== false && adv.includeConnectedModelSummary !== false) {
|
|
2775
3044
|
try {
|
|
2776
3045
|
const summaryInput = buildConnectedModelSummaryInput({
|
|
2777
3046
|
query,
|
|
3047
|
+
activeSummary: loadedActiveSummary,
|
|
2778
3048
|
profileFacts,
|
|
2779
3049
|
workingMemory,
|
|
2780
3050
|
decisionState,
|
|
@@ -2790,21 +3060,26 @@ class Mem0Memory {
|
|
|
2790
3060
|
key,
|
|
2791
3061
|
source: adv.summarySource || 'auto',
|
|
2792
3062
|
input: summaryInput,
|
|
2793
|
-
max: adv.connectedModelSummaryMaxChars ||
|
|
3063
|
+
max: adv.connectedModelSummaryMaxChars || 1200,
|
|
2794
3064
|
}) : '';
|
|
2795
3065
|
const cached = cacheKey ? store.connectedModelSummaryCache[cacheKey] : null;
|
|
2796
|
-
|
|
3066
|
+
const transientCacheEnabled = adv.enableTransientSummaryCache !== false;
|
|
3067
|
+
const transientTtlSeconds = Number(adv.transientSummaryCacheTTLSeconds || adv.connectedModelSummaryCacheTTLSeconds || 300);
|
|
3068
|
+
if (transientCacheEnabled && cached && cached.summary && Date.now() - Number(cached.at || 0) < transientTtlSeconds * 1000) {
|
|
2797
3069
|
connectedModelSummary = cached.summary;
|
|
2798
3070
|
connectedAi.languageModelSummaryCached = true;
|
|
3071
|
+
summaryDiagnostics.source = 'transient_cache';
|
|
3072
|
+
summaryDiagnostics.transientCacheHit = true;
|
|
2799
3073
|
}
|
|
2800
3074
|
else {
|
|
2801
3075
|
connectedModelSummary = await invokeConnectedModelSummary(connectedLanguageModel, summaryInput, adv);
|
|
2802
3076
|
if (cacheKey && connectedModelSummary) {
|
|
2803
3077
|
store.connectedModelSummaryCache[cacheKey] = { summary: connectedModelSummary, at: Date.now() };
|
|
2804
3078
|
const keys = Object.keys(store.connectedModelSummaryCache);
|
|
2805
|
-
for (const oldKey of keys.slice(0, Math.max(0, keys.length - Number(adv.connectedModelSummaryCacheMaxItems || 50))))
|
|
3079
|
+
for (const oldKey of keys.slice(0, Math.max(0, keys.length - Number(adv.transientSummaryCacheMaxItems || adv.connectedModelSummaryCacheMaxItems || 50))))
|
|
2806
3080
|
delete store.connectedModelSummaryCache[oldKey];
|
|
2807
3081
|
}
|
|
3082
|
+
summaryDiagnostics.source = connectedModelSummary ? 'fresh_slm' : 'none';
|
|
2808
3083
|
}
|
|
2809
3084
|
connectedAi.languageModelSummary = Boolean(connectedModelSummary);
|
|
2810
3085
|
}
|
|
@@ -2812,6 +3087,16 @@ class Mem0Memory {
|
|
|
2812
3087
|
connectedAi.errors.push(`languageModel.invoke: ${error.message || String(error)}`);
|
|
2813
3088
|
}
|
|
2814
3089
|
}
|
|
3090
|
+
if (!connectedModelSummary && loadedActiveSummary) {
|
|
3091
|
+
connectedModelSummary = loadedActiveSummary;
|
|
3092
|
+
summaryDiagnostics.source = 'active_summary';
|
|
3093
|
+
}
|
|
3094
|
+
if (connectedModelSummary) {
|
|
3095
|
+
summaryDiagnostics.summaryChars = String(connectedModelSummary).length;
|
|
3096
|
+
if (summaryDiagnostics.source === 'fresh_slm') {
|
|
3097
|
+
summaryDiagnostics.activeSummaryUpdated = writeActiveSummary(store, key, connectedModelSummary, adv);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
2815
3100
|
const diagnostics = {
|
|
2816
3101
|
vectorMemories: vectorMemories.length,
|
|
2817
3102
|
recentMessages: recentMessages.length,
|
|
@@ -2820,6 +3105,7 @@ class Mem0Memory {
|
|
|
2820
3105
|
project: project || null,
|
|
2821
3106
|
memoryNamespace: key,
|
|
2822
3107
|
connectedAi,
|
|
3108
|
+
activeSummary: summaryDiagnostics,
|
|
2823
3109
|
};
|
|
2824
3110
|
const contextHealth = deriveContextHealth({
|
|
2825
3111
|
userId: key,
|
|
@@ -2858,6 +3144,7 @@ class Mem0Memory {
|
|
|
2858
3144
|
highlights,
|
|
2859
3145
|
graph,
|
|
2860
3146
|
diagnostics,
|
|
3147
|
+
activeSummary: loadedActiveSummary,
|
|
2861
3148
|
connectedModelSummary,
|
|
2862
3149
|
adv,
|
|
2863
3150
|
});
|
|
@@ -2904,6 +3191,9 @@ class Mem0Memory {
|
|
|
2904
3191
|
includeOperationalState: adv.includeOperationalState !== false,
|
|
2905
3192
|
includeActionLedger: adv.includeActionLedger !== false,
|
|
2906
3193
|
includeEntityTimeline: adv.includeEntityTimeline !== false,
|
|
3194
|
+
includeActiveSummary: adv.includeActiveSummary !== false,
|
|
3195
|
+
persistActiveSummary: adv.persistActiveSummary !== false,
|
|
3196
|
+
enableTransientSummaryCache: adv.enableTransientSummaryCache !== false,
|
|
2907
3197
|
compactForAgent: Boolean(adv.compactForAgent),
|
|
2908
3198
|
includeWorkingMemory: adv.includeWorkingMemory !== false,
|
|
2909
3199
|
includeDecisionState: adv.includeDecisionState !== false,
|
|
@@ -2912,6 +3202,7 @@ class Mem0Memory {
|
|
|
2912
3202
|
},
|
|
2913
3203
|
context: contextText,
|
|
2914
3204
|
summary,
|
|
3205
|
+
activeSummary: loadedActiveSummary,
|
|
2915
3206
|
connectedModelSummary,
|
|
2916
3207
|
contextHealth,
|
|
2917
3208
|
contextQualityScore: contextHealth.quality_score,
|
|
@@ -2959,6 +3250,7 @@ class Mem0Memory {
|
|
|
2959
3250
|
temboryContext: audit,
|
|
2960
3251
|
temboryContextText: contextText,
|
|
2961
3252
|
temborySummary: summary,
|
|
3253
|
+
temboryActiveSummary: loadedActiveSummary,
|
|
2962
3254
|
temboryConnectedModelSummary: connectedModelSummary,
|
|
2963
3255
|
temboryContextHealth: contextHealth,
|
|
2964
3256
|
temboryContextQualityScore: contextHealth.quality_score,
|
|
@@ -2987,7 +3279,7 @@ class Mem0Memory {
|
|
|
2987
3279
|
const retrievalMode = this.getNodeParameter('retrievalMode', i);
|
|
2988
3280
|
const threadId = this.getNodeParameter('threadId', i);
|
|
2989
3281
|
const project = this.getNodeParameter('project', i, '');
|
|
2990
|
-
const adv = applyOperationalPreset(this
|
|
3282
|
+
const adv = applyOperationalPreset(readAdvancedOptions(this, i));
|
|
2991
3283
|
const key = userKeyFrom(threadId, adv, project);
|
|
2992
3284
|
const query = asSearchQuery(this.getNodeParameter('query', i, ''));
|
|
2993
3285
|
const connectedEmbedding = await getConnectedEmbedding(this, i);
|
|
@@ -3069,6 +3361,7 @@ exports.__private = {
|
|
|
3069
3361
|
encodeRecentMessage,
|
|
3070
3362
|
userKeyFrom,
|
|
3071
3363
|
applyOperationalPreset,
|
|
3364
|
+
flattenAdvancedGroups,
|
|
3072
3365
|
asSearchQuery,
|
|
3073
3366
|
canonicalToolInput,
|
|
3074
3367
|
buildContextMessages,
|
|
@@ -3094,6 +3387,9 @@ exports.__private = {
|
|
|
3094
3387
|
compactToolResult,
|
|
3095
3388
|
compactToolHistoryForAgent,
|
|
3096
3389
|
compactOperationalStateForAgent,
|
|
3390
|
+
activeSummaryIsFresh,
|
|
3391
|
+
readActiveSummary,
|
|
3392
|
+
writeActiveSummary,
|
|
3097
3393
|
buildConnectedModelSummaryInput,
|
|
3098
3394
|
cleanModelSummaryText,
|
|
3099
3395
|
invokeConnectedModelSummary,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-tembory",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
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",
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
"n8n",
|
|
49
49
|
"tembory",
|
|
50
50
|
"tembory-memory",
|
|
51
|
-
"elephant-brain",
|
|
52
51
|
"memory",
|
|
53
52
|
"ai",
|
|
54
53
|
"llm",
|