n8n-nodes-smart-memory 1.0.0
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/.eslintrc.js +29 -0
- package/.prettierrc +10 -0
- package/README.md +45 -0
- package/dist/SmartMemory.d.ts +13 -0
- package/dist/SmartMemory.d.ts.map +1 -0
- package/dist/SmartMemory.js +163 -0
- package/dist/SmartMemory.js.map +1 -0
- package/dist/SmartMemory.node.d.ts +6 -0
- package/dist/SmartMemory.node.d.ts.map +1 -0
- package/dist/SmartMemory.node.js +136 -0
- package/dist/SmartMemory.node.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/MemoryInterfaces.d.ts +40 -0
- package/dist/interfaces/MemoryInterfaces.d.ts.map +1 -0
- package/dist/interfaces/MemoryInterfaces.js +3 -0
- package/dist/interfaces/MemoryInterfaces.js.map +1 -0
- package/dist/storage/GoogleSheetsStorage.d.ts +8 -0
- package/dist/storage/GoogleSheetsStorage.d.ts.map +1 -0
- package/dist/storage/GoogleSheetsStorage.js +20 -0
- package/dist/storage/GoogleSheetsStorage.js.map +1 -0
- package/dist/storage/InMemoryStorage.d.ts +9 -0
- package/dist/storage/InMemoryStorage.d.ts.map +1 -0
- package/dist/storage/InMemoryStorage.js +19 -0
- package/dist/storage/InMemoryStorage.js.map +1 -0
- package/dist/storage/MemoryStorage.d.ts +7 -0
- package/dist/storage/MemoryStorage.d.ts.map +1 -0
- package/dist/storage/MemoryStorage.js +3 -0
- package/dist/storage/MemoryStorage.js.map +1 -0
- package/dist/storage/RedisStorage.d.ts +8 -0
- package/dist/storage/RedisStorage.d.ts.map +1 -0
- package/dist/storage/RedisStorage.js +20 -0
- package/dist/storage/RedisStorage.js.map +1 -0
- package/dist/utils/ExpressionEvaluator.d.ts +4 -0
- package/dist/utils/ExpressionEvaluator.d.ts.map +1 -0
- package/dist/utils/ExpressionEvaluator.js +33 -0
- package/dist/utils/ExpressionEvaluator.js.map +1 -0
- package/jest.config.js +18 -0
- package/package.json +51 -0
- package/src/SmartMemory.node.ts +153 -0
- package/src/SmartMemory.ts +195 -0
- package/src/__tests__/SmartMemory.test.ts +157 -0
- package/src/__tests__/setup.ts +18 -0
- package/src/__tests__/storage/InMemoryStorage.test.ts +70 -0
- package/src/index.ts +3 -0
- package/src/interfaces/MemoryInterfaces.ts +42 -0
- package/src/storage/GoogleSheetsStorage.ts +20 -0
- package/src/storage/InMemoryStorage.ts +18 -0
- package/src/storage/MemoryStorage.ts +7 -0
- package/src/storage/RedisStorage.ts +20 -0
- package/src/utils/ExpressionEvaluator.ts +27 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RedisStorage = void 0;
|
|
4
|
+
class RedisStorage {
|
|
5
|
+
async getMemory(_chatId) {
|
|
6
|
+
// TODO: Implement Redis storage
|
|
7
|
+
console.log('Redis storage not implemented yet');
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
async saveMemory(_chatId, _memory) {
|
|
11
|
+
// TODO: Implement Redis storage
|
|
12
|
+
console.log('Redis storage not implemented yet');
|
|
13
|
+
}
|
|
14
|
+
async clearMemory(_chatId) {
|
|
15
|
+
// TODO: Implement Redis storage
|
|
16
|
+
console.log('Redis storage not implemented yet');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.RedisStorage = RedisStorage;
|
|
20
|
+
//# sourceMappingURL=RedisStorage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RedisStorage.js","sourceRoot":"","sources":["../../src/storage/RedisStorage.ts"],"names":[],"mappings":";;;AAGA,MAAa,YAAY;IACvB,KAAK,CAAC,SAAS,CAAC,OAAe;QAC7B,gCAAgC;QAChC,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;QACjD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,OAAe,EAAE,OAA6B;QAC7D,gCAAgC;QAChC,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;IACnD,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAe;QAC/B,gCAAgC;QAChC,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;IACnD,CAAC;CACF;AAhBD,oCAgBC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpressionEvaluator.d.ts","sourceRoot":"","sources":["../../src/utils/ExpressionEvaluator.ts"],"names":[],"mappings":"AAAA,qBAAa,mBAAmB;IAC9B,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;CAyBxD"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExpressionEvaluator = void 0;
|
|
4
|
+
class ExpressionEvaluator {
|
|
5
|
+
static evaluate(_data, expression) {
|
|
6
|
+
// Simple expression evaluator - in real implementation, use n8n's expression system
|
|
7
|
+
// Examples: "={{ $json.message.from.first_name + ' ' + $json.message.from.last_name }}"
|
|
8
|
+
// "={{ $json.message.from.username }}"
|
|
9
|
+
try {
|
|
10
|
+
// This would integrate with n8n's expression system
|
|
11
|
+
if (expression.startsWith('={{ ') && expression.endsWith(' }}')) {
|
|
12
|
+
// Extract the expression part
|
|
13
|
+
const expr = expression.slice(3, -2);
|
|
14
|
+
// Simple evaluation for common cases
|
|
15
|
+
if (expr.includes('$json.message.from.first_name') && expr.includes('$json.message.from.last_name')) {
|
|
16
|
+
const firstName = _data.message?.from?.first_name || '';
|
|
17
|
+
const lastName = _data.message?.from?.last_name || '';
|
|
18
|
+
return `${firstName} ${lastName}`.trim();
|
|
19
|
+
}
|
|
20
|
+
if (expr.includes('$json.message.from.username')) {
|
|
21
|
+
return _data.message?.from?.username || '';
|
|
22
|
+
}
|
|
23
|
+
return expr;
|
|
24
|
+
}
|
|
25
|
+
return expression;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.ExpressionEvaluator = ExpressionEvaluator;
|
|
33
|
+
//# sourceMappingURL=ExpressionEvaluator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpressionEvaluator.js","sourceRoot":"","sources":["../../src/utils/ExpressionEvaluator.ts"],"names":[],"mappings":";;;AAAA,MAAa,mBAAmB;IAC9B,MAAM,CAAC,QAAQ,CAAC,KAAU,EAAE,UAAkB;QAC5C,oFAAoF;QACpF,wFAAwF;QACxF,iDAAiD;QACjD,IAAI,CAAC;YACH,oDAAoD;YACpD,IAAI,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBAChE,8BAA8B;gBAC9B,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBACrC,qCAAqC;gBACrC,IAAI,IAAI,CAAC,QAAQ,CAAC,+BAA+B,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,8BAA8B,CAAC,EAAE,CAAC;oBACpG,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,UAAU,IAAI,EAAE,CAAC;oBACxD,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,SAAS,IAAI,EAAE,CAAC;oBACtD,OAAO,GAAG,SAAS,IAAI,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;gBAC3C,CAAC;gBACD,IAAI,IAAI,CAAC,QAAQ,CAAC,6BAA6B,CAAC,EAAE,CAAC;oBACjD,OAAO,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC;gBAC7C,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,UAAU,CAAC;QACpB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;CACF;AA1BD,kDA0BC"}
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: 'ts-jest',
|
|
3
|
+
testEnvironment: 'node',
|
|
4
|
+
roots: ['<rootDir>/src'],
|
|
5
|
+
testMatch: ['**/__tests__/**/*.test.ts'],
|
|
6
|
+
transform: {
|
|
7
|
+
'^.+\\.ts$': 'ts-jest',
|
|
8
|
+
},
|
|
9
|
+
collectCoverageFrom: [
|
|
10
|
+
'src/**/*.ts',
|
|
11
|
+
'!src/**/*.d.ts',
|
|
12
|
+
'!src/**/*.test.ts',
|
|
13
|
+
'!src/index.ts',
|
|
14
|
+
],
|
|
15
|
+
coverageDirectory: 'coverage',
|
|
16
|
+
coverageReporters: ['text', 'lcov', 'html'],
|
|
17
|
+
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts'],
|
|
18
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-smart-memory",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Smart memory management node for n8n with support for multiple storage backends",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch",
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"lint": "eslint src --ext .ts",
|
|
12
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
13
|
+
"format": "prettier --write src/**/*.ts",
|
|
14
|
+
"format:check": "prettier --check src/**/*.ts"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"n8n",
|
|
18
|
+
"memory",
|
|
19
|
+
"chatbot",
|
|
20
|
+
"llm",
|
|
21
|
+
"telegram"
|
|
22
|
+
],
|
|
23
|
+
"author": "Your Name",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"n8n-core": "^1.113.0",
|
|
27
|
+
"n8n-workflow": "^1.111.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/jest": "^29.5.12",
|
|
31
|
+
"@types/node": "^20.12.7",
|
|
32
|
+
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
33
|
+
"@typescript-eslint/parser": "^6.21.0",
|
|
34
|
+
"eslint": "^8.57.0",
|
|
35
|
+
"eslint-config-prettier": "^9.1.0",
|
|
36
|
+
"eslint-plugin-prettier": "^5.1.3",
|
|
37
|
+
"jest": "^29.7.0",
|
|
38
|
+
"jest-environment-node": "^29.7.0",
|
|
39
|
+
"prettier": "^3.2.5",
|
|
40
|
+
"ts-jest": "^29.1.2",
|
|
41
|
+
"typescript": "^5.4.5"
|
|
42
|
+
},
|
|
43
|
+
"n8n": {
|
|
44
|
+
"nodes": {
|
|
45
|
+
"Smart Memory": "./dist/SmartMemory.node.js"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IExecuteFunctions,
|
|
3
|
+
INodeExecutionData,
|
|
4
|
+
INodeType,
|
|
5
|
+
INodeTypeDescription,
|
|
6
|
+
} from 'n8n-workflow';
|
|
7
|
+
import { PureMemoryManager } from './SmartMemory';
|
|
8
|
+
import { MemoryManagerConfig } from './interfaces/MemoryInterfaces';
|
|
9
|
+
|
|
10
|
+
// Helper function outside the class
|
|
11
|
+
function estimateTokenCount(memory: any): number {
|
|
12
|
+
// Estimate tokens for the entire memory structure
|
|
13
|
+
const text = JSON.stringify(memory);
|
|
14
|
+
return Math.ceil(text.length / 4);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class SmartMemory implements INodeType {
|
|
18
|
+
description: INodeTypeDescription = {
|
|
19
|
+
displayName: 'Smart Memory',
|
|
20
|
+
name: 'smartMemory',
|
|
21
|
+
icon: 'fa:memory',
|
|
22
|
+
group: ['transform'],
|
|
23
|
+
version: 1.0,
|
|
24
|
+
description: 'Configuration-driven memory management without prompt manipulation',
|
|
25
|
+
defaults: {
|
|
26
|
+
name: 'Smart Memory',
|
|
27
|
+
},
|
|
28
|
+
inputs: ['main'],
|
|
29
|
+
outputs: ['main'],
|
|
30
|
+
properties: [
|
|
31
|
+
{
|
|
32
|
+
displayName: 'Memory Window Size',
|
|
33
|
+
name: 'memoryWindowSize',
|
|
34
|
+
type: 'number',
|
|
35
|
+
typeOptions: {
|
|
36
|
+
minValue: 5,
|
|
37
|
+
maxValue: 50,
|
|
38
|
+
},
|
|
39
|
+
default: 15,
|
|
40
|
+
description: 'Number of messages to keep in memory',
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
displayName: 'Assistant Role',
|
|
45
|
+
name: 'assistantRole',
|
|
46
|
+
type: 'string',
|
|
47
|
+
default: 'Assistant',
|
|
48
|
+
description: 'How the assistant should be identified in memory',
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
displayName: 'User Display Name Expression',
|
|
53
|
+
name: 'userDisplayNameExpression',
|
|
54
|
+
type: 'string',
|
|
55
|
+
default: '={{ $json.message.from.first_name + " " + $json.message.from.last_name }}',
|
|
56
|
+
description: 'Expression to combine user name fields',
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
displayName: 'User Username Expression',
|
|
61
|
+
name: 'userUsernameExpression',
|
|
62
|
+
type: 'string',
|
|
63
|
+
default: '={{ $json.message.from.username }}',
|
|
64
|
+
description: 'Expression to get username (leave empty to disable)',
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
{
|
|
68
|
+
displayName: 'Include Reply Context',
|
|
69
|
+
name: 'includeReplyContext',
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
default: true,
|
|
72
|
+
description: 'Include information about replied-to messages',
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
displayName: 'Storage Backend',
|
|
77
|
+
name: 'storageBackend',
|
|
78
|
+
type: 'options',
|
|
79
|
+
options: [
|
|
80
|
+
{ name: 'In-Memory', value: 'memory' },
|
|
81
|
+
{ name: 'Google Sheets', value: 'googleSheets' },
|
|
82
|
+
{ name: 'Redis', value: 'redis' },
|
|
83
|
+
],
|
|
84
|
+
default: 'memory',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
90
|
+
const items = this.getInputData();
|
|
91
|
+
const returnData: INodeExecutionData[] = [];
|
|
92
|
+
|
|
93
|
+
// Build configuration from node parameters
|
|
94
|
+
const config: MemoryManagerConfig = {
|
|
95
|
+
memoryWindowSize: this.getNodeParameter('memoryWindowSize', 0) as number,
|
|
96
|
+
storageBackend: this.getNodeParameter('storageBackend', 0) as 'memory' | 'googleSheets' | 'redis',
|
|
97
|
+
assistantRole: this.getNodeParameter('assistantRole', 0) as string,
|
|
98
|
+
userFieldMapping: {
|
|
99
|
+
displayName: this.getNodeParameter('userDisplayNameExpression', 0) as string,
|
|
100
|
+
username: this.getNodeParameter('userUsernameExpression', 0) as string,
|
|
101
|
+
},
|
|
102
|
+
includeReplyContext: this.getNodeParameter('includeReplyContext', 0) as boolean,
|
|
103
|
+
enableSemanticMemory: false,
|
|
104
|
+
enableEpisodicMemory: false,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Initialize memory manager
|
|
108
|
+
const memoryManager = new PureMemoryManager(config);
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < items.length; i++) {
|
|
111
|
+
try {
|
|
112
|
+
// Process the message
|
|
113
|
+
if (!items[i]?.json) {
|
|
114
|
+
throw new Error('Invalid input data: missing json property');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const jsonData = items[i]!.json as any;
|
|
118
|
+
if (!jsonData || !jsonData.message || !jsonData.message.chat) {
|
|
119
|
+
throw new Error('Invalid message format: missing message or chat data');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const chatId = jsonData.message.chat.id;
|
|
123
|
+
let memory = await memoryManager.processMessage(jsonData);
|
|
124
|
+
|
|
125
|
+
// Apply memory window size
|
|
126
|
+
memory.messages = memory.messages.slice(-config.memoryWindowSize);
|
|
127
|
+
|
|
128
|
+
// Build output - just the memory data, no prompt manipulation
|
|
129
|
+
const output = {
|
|
130
|
+
chatId,
|
|
131
|
+
chatType: memory.chatInfo.chatType,
|
|
132
|
+
memory: memory,
|
|
133
|
+
messageCount: memory.messages.length,
|
|
134
|
+
tokenEstimate: estimateTokenCount(memory),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
returnData.push({
|
|
138
|
+
json: output,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
143
|
+
returnData.push({
|
|
144
|
+
json: {
|
|
145
|
+
error: errorMessage,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return [returnData];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { CleanMemoryStructure, MemoryManagerConfig } from './interfaces/MemoryInterfaces';
|
|
2
|
+
import { InMemoryStorage } from './storage/InMemoryStorage';
|
|
3
|
+
// Import placeholder implementations for now
|
|
4
|
+
// import { GoogleSheetsStorage } from './storage/GoogleSheetsStorage';
|
|
5
|
+
// import { RedisStorage } from './storage/RedisStorage';
|
|
6
|
+
|
|
7
|
+
// Placeholder implementations
|
|
8
|
+
class GoogleSheetsStorage {
|
|
9
|
+
async getMemory(_chatId: string): Promise<CleanMemoryStructure | null> {
|
|
10
|
+
throw new Error('Google Sheets storage not implemented yet');
|
|
11
|
+
}
|
|
12
|
+
async saveMemory(_chatId: string, _memory: CleanMemoryStructure): Promise<void> {
|
|
13
|
+
throw new Error('Google Sheets storage not implemented yet');
|
|
14
|
+
}
|
|
15
|
+
async clearMemory(_chatId: string): Promise<void> {
|
|
16
|
+
throw new Error('Google Sheets storage not implemented yet');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class RedisStorage {
|
|
21
|
+
async getMemory(_chatId: string): Promise<CleanMemoryStructure | null> {
|
|
22
|
+
throw new Error('Redis storage not implemented yet');
|
|
23
|
+
}
|
|
24
|
+
async saveMemory(_chatId: string, _memory: CleanMemoryStructure): Promise<void> {
|
|
25
|
+
throw new Error('Redis storage not implemented yet');
|
|
26
|
+
}
|
|
27
|
+
async clearMemory(_chatId: string): Promise<void> {
|
|
28
|
+
throw new Error('Redis storage not implemented yet');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class PureMemoryManager {
|
|
33
|
+
private config: MemoryManagerConfig;
|
|
34
|
+
private storage: any; // Use proper interface
|
|
35
|
+
|
|
36
|
+
constructor(config: MemoryManagerConfig) {
|
|
37
|
+
this.config = config;
|
|
38
|
+
|
|
39
|
+
// Initialize storage backend
|
|
40
|
+
switch (config.storageBackend) {
|
|
41
|
+
case 'memory':
|
|
42
|
+
this.storage = new InMemoryStorage();
|
|
43
|
+
break;
|
|
44
|
+
case 'googleSheets':
|
|
45
|
+
this.storage = new GoogleSheetsStorage();
|
|
46
|
+
break;
|
|
47
|
+
case 'redis':
|
|
48
|
+
this.storage = new RedisStorage();
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async processMessage(inputData: any): Promise<CleanMemoryStructure> {
|
|
54
|
+
const chatId = inputData.message?.chat?.id;
|
|
55
|
+
const existingMemory = await this.getMemory(chatId);
|
|
56
|
+
|
|
57
|
+
if (!existingMemory) {
|
|
58
|
+
const newMemory = await this.createNewMemory(inputData);
|
|
59
|
+
// Save the new memory
|
|
60
|
+
await this.storage.saveMemory(chatId, newMemory);
|
|
61
|
+
return newMemory;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const updatedMemory = await this.updateExistingMemory(existingMemory, inputData);
|
|
65
|
+
// Save the updated memory
|
|
66
|
+
await this.storage.saveMemory(chatId, updatedMemory);
|
|
67
|
+
return updatedMemory;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async getMemory(chatId: string): Promise<CleanMemoryStructure | null> {
|
|
71
|
+
return await this.storage.getMemory(chatId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async createNewMemory(inputData: any): Promise<CleanMemoryStructure> {
|
|
75
|
+
const chat = inputData.message?.chat;
|
|
76
|
+
const user = inputData.message?.from;
|
|
77
|
+
const isBotResponse = !!inputData.bot_response;
|
|
78
|
+
|
|
79
|
+
const memory: CleanMemoryStructure = {
|
|
80
|
+
chatInfo: {
|
|
81
|
+
chatId: chat.id,
|
|
82
|
+
chatType: chat.type,
|
|
83
|
+
chatName: chat.title || chat.username || 'Private Chat',
|
|
84
|
+
},
|
|
85
|
+
messages: []
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Only add private user info for private chats
|
|
89
|
+
if (chat.type === 'private' && !isBotResponse && user) {
|
|
90
|
+
const displayName = this.evaluateExpression(inputData, this.config.userFieldMapping.displayName);
|
|
91
|
+
const username = this.config.userFieldMapping.username ?
|
|
92
|
+
this.evaluateExpression(inputData, this.config.userFieldMapping.username) :
|
|
93
|
+
undefined;
|
|
94
|
+
|
|
95
|
+
// Create the object conditionally to avoid exactOptionalPropertyTypes issue
|
|
96
|
+
const privateUserInfo: any = {
|
|
97
|
+
userId: user.id,
|
|
98
|
+
displayName,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (username !== undefined) {
|
|
102
|
+
privateUserInfo.username = username;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
memory.privateUserInfo = privateUserInfo;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Add the first message
|
|
109
|
+
return await this.addMessageToMemory(memory, inputData);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private async updateExistingMemory(
|
|
113
|
+
memory: CleanMemoryStructure,
|
|
114
|
+
inputData: any
|
|
115
|
+
): Promise<CleanMemoryStructure> {
|
|
116
|
+
return await this.addMessageToMemory(memory, inputData);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private async addMessageToMemory(
|
|
120
|
+
memory: CleanMemoryStructure,
|
|
121
|
+
inputData: any
|
|
122
|
+
): Promise<CleanMemoryStructure> {
|
|
123
|
+
const message = inputData.message;
|
|
124
|
+
const isBotResponse = !!inputData.bot_response;
|
|
125
|
+
|
|
126
|
+
const newMessage = {
|
|
127
|
+
role: isBotResponse ? this.config.assistantRole : 'user',
|
|
128
|
+
content: message.text || message.caption || inputData.bot_response || '',
|
|
129
|
+
timestamp: new Date().toISOString(),
|
|
130
|
+
} as any;
|
|
131
|
+
|
|
132
|
+
// Add user info for user messages in groups
|
|
133
|
+
if (!isBotResponse && memory.chatInfo.chatType !== 'private' && message.from) {
|
|
134
|
+
newMessage.userInfo = {
|
|
135
|
+
displayName: this.evaluateExpression(inputData, this.config.userFieldMapping.displayName),
|
|
136
|
+
username: this.config.userFieldMapping.username ?
|
|
137
|
+
this.evaluateExpression(inputData, this.config.userFieldMapping.username) :
|
|
138
|
+
undefined,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Add reply information if enabled and present
|
|
143
|
+
if (this.config.includeReplyContext && message.reply_to_message) {
|
|
144
|
+
const replyUser = message.reply_to_message.from;
|
|
145
|
+
if (replyUser) {
|
|
146
|
+
newMessage.replyInfo = {
|
|
147
|
+
replyToDisplayName: this.evaluateExpression(
|
|
148
|
+
{ message: { from: replyUser } },
|
|
149
|
+
this.config.userFieldMapping.displayName
|
|
150
|
+
),
|
|
151
|
+
replyToUsername: this.config.userFieldMapping.username ?
|
|
152
|
+
this.evaluateExpression(
|
|
153
|
+
{ message: { from: replyUser } },
|
|
154
|
+
this.config.userFieldMapping.username
|
|
155
|
+
) :
|
|
156
|
+
undefined,
|
|
157
|
+
replyToMessage: message.reply_to_message.text ||
|
|
158
|
+
message.reply_to_message.caption ||
|
|
159
|
+
'[Media/Other content]',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
...memory,
|
|
166
|
+
messages: [...memory.messages, newMessage],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private evaluateExpression(data: any, expression: string): string {
|
|
171
|
+
// Simple expression evaluator - in real implementation, use n8n's expression system
|
|
172
|
+
// Examples: "={{ $json.message.from.first_name + ' ' + $json.message.from.last_name }}"
|
|
173
|
+
// "={{ $json.message.from.username }}"
|
|
174
|
+
try {
|
|
175
|
+
// This would integrate with n8n's expression system
|
|
176
|
+
if (expression.startsWith('={{ ') && expression.endsWith(' }}')) {
|
|
177
|
+
// Extract the expression part
|
|
178
|
+
const expr = expression.slice(3, -2);
|
|
179
|
+
// Simple evaluation for common cases
|
|
180
|
+
if (expr.includes('$json.message.from.first_name') && expr.includes('$json.message.from.last_name')) {
|
|
181
|
+
const firstName = data.message?.from?.first_name || '';
|
|
182
|
+
const lastName = data.message?.from?.last_name || '';
|
|
183
|
+
return `${firstName} ${lastName}`.trim();
|
|
184
|
+
}
|
|
185
|
+
if (expr.includes('$json.message.from.username')) {
|
|
186
|
+
return data.message?.from?.username || '';
|
|
187
|
+
}
|
|
188
|
+
return expr;
|
|
189
|
+
}
|
|
190
|
+
return expression;
|
|
191
|
+
} catch {
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { PureMemoryManager } from '../SmartMemory';
|
|
2
|
+
|
|
3
|
+
describe('PureMemoryManager', () => {
|
|
4
|
+
let memoryManager: PureMemoryManager;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
memoryManager = new PureMemoryManager({
|
|
8
|
+
memoryWindowSize: 10,
|
|
9
|
+
storageBackend: 'memory',
|
|
10
|
+
userFieldMapping: {
|
|
11
|
+
displayName: '={{ $json.message.from.first_name + " " + $json.message.from.last_name }}',
|
|
12
|
+
username: '={{ $json.message.from.username }}',
|
|
13
|
+
},
|
|
14
|
+
assistantRole: 'Assistant',
|
|
15
|
+
includeReplyContext: true,
|
|
16
|
+
enableSemanticMemory: false,
|
|
17
|
+
enableEpisodicMemory: false,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('processMessage', () => {
|
|
22
|
+
it('should create new memory for first message', async () => {
|
|
23
|
+
const inputData = {
|
|
24
|
+
message: {
|
|
25
|
+
chat: {
|
|
26
|
+
id: '123',
|
|
27
|
+
type: 'private',
|
|
28
|
+
},
|
|
29
|
+
from: {
|
|
30
|
+
id: '456',
|
|
31
|
+
first_name: 'John',
|
|
32
|
+
last_name: 'Doe',
|
|
33
|
+
username: 'johndoe',
|
|
34
|
+
},
|
|
35
|
+
text: 'Hello',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const result = await memoryManager.processMessage(inputData);
|
|
40
|
+
|
|
41
|
+
expect(result.chatInfo.chatId).toBe('123');
|
|
42
|
+
expect(result.chatInfo.chatType).toBe('private');
|
|
43
|
+
expect(result.privateUserInfo).toBeDefined();
|
|
44
|
+
expect(result.privateUserInfo?.displayName).toBe('John Doe');
|
|
45
|
+
expect(result.messages).toHaveLength(1);
|
|
46
|
+
if (result.messages[0]) {
|
|
47
|
+
expect(result.messages[0].content).toBe('Hello');
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should update existing memory for subsequent messages', async () => {
|
|
52
|
+
const inputData = {
|
|
53
|
+
message: {
|
|
54
|
+
chat: {
|
|
55
|
+
id: '123',
|
|
56
|
+
type: 'private',
|
|
57
|
+
},
|
|
58
|
+
from: {
|
|
59
|
+
id: '456',
|
|
60
|
+
first_name: 'John',
|
|
61
|
+
last_name: 'Doe',
|
|
62
|
+
username: 'johndoe',
|
|
63
|
+
},
|
|
64
|
+
text: 'Hello',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// First message
|
|
69
|
+
await memoryManager.processMessage(inputData);
|
|
70
|
+
|
|
71
|
+
// Second message
|
|
72
|
+
const secondInput = {
|
|
73
|
+
...inputData,
|
|
74
|
+
message: {
|
|
75
|
+
...inputData.message,
|
|
76
|
+
text: 'How are you?',
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const result = await memoryManager.processMessage(secondInput);
|
|
81
|
+
|
|
82
|
+
expect(result.messages).toHaveLength(2);
|
|
83
|
+
if (result.messages[0]) {
|
|
84
|
+
expect(result.messages[0].content).toBe('Hello');
|
|
85
|
+
}
|
|
86
|
+
if (result.messages[1]) {
|
|
87
|
+
expect(result.messages[1].content).toBe('How are you?');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should handle group messages with user info', async () => {
|
|
92
|
+
const inputData = {
|
|
93
|
+
message: {
|
|
94
|
+
chat: {
|
|
95
|
+
id: '789',
|
|
96
|
+
type: 'group',
|
|
97
|
+
title: 'Test Group',
|
|
98
|
+
},
|
|
99
|
+
from: {
|
|
100
|
+
id: '456',
|
|
101
|
+
first_name: 'John',
|
|
102
|
+
last_name: 'Doe',
|
|
103
|
+
username: 'johndoe',
|
|
104
|
+
},
|
|
105
|
+
text: 'Hello group',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const result = await memoryManager.processMessage(inputData);
|
|
110
|
+
|
|
111
|
+
expect(result.chatInfo.chatType).toBe('group');
|
|
112
|
+
expect(result.privateUserInfo).toBeUndefined();
|
|
113
|
+
expect(result.messages).toHaveLength(1);
|
|
114
|
+
if (result.messages[0]) {
|
|
115
|
+
expect(result.messages[0].userInfo).toBeDefined();
|
|
116
|
+
expect(result.messages[0].userInfo?.displayName).toBe('John Doe');
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should handle reply context', async () => {
|
|
121
|
+
const inputData = {
|
|
122
|
+
message: {
|
|
123
|
+
chat: {
|
|
124
|
+
id: '789',
|
|
125
|
+
type: 'group',
|
|
126
|
+
title: 'Test Group',
|
|
127
|
+
},
|
|
128
|
+
from: {
|
|
129
|
+
id: '456',
|
|
130
|
+
first_name: 'John',
|
|
131
|
+
last_name: 'Doe',
|
|
132
|
+
username: 'johndoe',
|
|
133
|
+
},
|
|
134
|
+
reply_to_message: {
|
|
135
|
+
from: {
|
|
136
|
+
id: '789',
|
|
137
|
+
first_name: 'Jane',
|
|
138
|
+
last_name: 'Smith',
|
|
139
|
+
username: 'janesmith',
|
|
140
|
+
},
|
|
141
|
+
text: 'Original message',
|
|
142
|
+
},
|
|
143
|
+
text: 'Replying to your message',
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const result = await memoryManager.processMessage(inputData);
|
|
148
|
+
|
|
149
|
+
expect(result.messages).toHaveLength(1);
|
|
150
|
+
if (result.messages[0]) {
|
|
151
|
+
expect(result.messages[0].replyInfo).toBeDefined();
|
|
152
|
+
expect(result.messages[0].replyInfo?.replyToDisplayName).toBe('Jane Smith');
|
|
153
|
+
expect(result.messages[0].replyInfo?.replyToMessage).toBe('Original message');
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Global test setup
|
|
2
|
+
import { jest } from '@jest/globals';
|
|
3
|
+
|
|
4
|
+
// Mock n8n-core and n8n-workflow
|
|
5
|
+
jest.mock('n8n-core', () => ({
|
|
6
|
+
// Add mock implementations as needed
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
jest.mock('n8n-workflow', () => ({
|
|
10
|
+
// Add mock implementations as needed
|
|
11
|
+
IExecuteFunctions: class {},
|
|
12
|
+
INodeExecutionData: class {},
|
|
13
|
+
INodeType: class {},
|
|
14
|
+
INodeTypeDescription: class {},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Set test timeout
|
|
18
|
+
jest.setTimeout(30000);
|