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.
Files changed (54) hide show
  1. package/.eslintrc.js +29 -0
  2. package/.prettierrc +10 -0
  3. package/README.md +45 -0
  4. package/dist/SmartMemory.d.ts +13 -0
  5. package/dist/SmartMemory.d.ts.map +1 -0
  6. package/dist/SmartMemory.js +163 -0
  7. package/dist/SmartMemory.js.map +1 -0
  8. package/dist/SmartMemory.node.d.ts +6 -0
  9. package/dist/SmartMemory.node.d.ts.map +1 -0
  10. package/dist/SmartMemory.node.js +136 -0
  11. package/dist/SmartMemory.node.js.map +1 -0
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +6 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/interfaces/MemoryInterfaces.d.ts +40 -0
  17. package/dist/interfaces/MemoryInterfaces.d.ts.map +1 -0
  18. package/dist/interfaces/MemoryInterfaces.js +3 -0
  19. package/dist/interfaces/MemoryInterfaces.js.map +1 -0
  20. package/dist/storage/GoogleSheetsStorage.d.ts +8 -0
  21. package/dist/storage/GoogleSheetsStorage.d.ts.map +1 -0
  22. package/dist/storage/GoogleSheetsStorage.js +20 -0
  23. package/dist/storage/GoogleSheetsStorage.js.map +1 -0
  24. package/dist/storage/InMemoryStorage.d.ts +9 -0
  25. package/dist/storage/InMemoryStorage.d.ts.map +1 -0
  26. package/dist/storage/InMemoryStorage.js +19 -0
  27. package/dist/storage/InMemoryStorage.js.map +1 -0
  28. package/dist/storage/MemoryStorage.d.ts +7 -0
  29. package/dist/storage/MemoryStorage.d.ts.map +1 -0
  30. package/dist/storage/MemoryStorage.js +3 -0
  31. package/dist/storage/MemoryStorage.js.map +1 -0
  32. package/dist/storage/RedisStorage.d.ts +8 -0
  33. package/dist/storage/RedisStorage.d.ts.map +1 -0
  34. package/dist/storage/RedisStorage.js +20 -0
  35. package/dist/storage/RedisStorage.js.map +1 -0
  36. package/dist/utils/ExpressionEvaluator.d.ts +4 -0
  37. package/dist/utils/ExpressionEvaluator.d.ts.map +1 -0
  38. package/dist/utils/ExpressionEvaluator.js +33 -0
  39. package/dist/utils/ExpressionEvaluator.js.map +1 -0
  40. package/jest.config.js +18 -0
  41. package/package.json +51 -0
  42. package/src/SmartMemory.node.ts +153 -0
  43. package/src/SmartMemory.ts +195 -0
  44. package/src/__tests__/SmartMemory.test.ts +157 -0
  45. package/src/__tests__/setup.ts +18 -0
  46. package/src/__tests__/storage/InMemoryStorage.test.ts +70 -0
  47. package/src/index.ts +3 -0
  48. package/src/interfaces/MemoryInterfaces.ts +42 -0
  49. package/src/storage/GoogleSheetsStorage.ts +20 -0
  50. package/src/storage/InMemoryStorage.ts +18 -0
  51. package/src/storage/MemoryStorage.ts +7 -0
  52. package/src/storage/RedisStorage.ts +20 -0
  53. package/src/utils/ExpressionEvaluator.ts +27 -0
  54. 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,4 @@
1
+ export declare class ExpressionEvaluator {
2
+ static evaluate(_data: any, expression: string): string;
3
+ }
4
+ //# sourceMappingURL=ExpressionEvaluator.d.ts.map
@@ -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);