@juspay/neurolink 7.33.4 → 7.35.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/CHANGELOG.md +15 -0
- package/README.md +101 -7
- package/dist/cli/commands/setup-anthropic.d.ts +16 -0
- package/dist/cli/commands/setup-anthropic.js +414 -0
- package/dist/cli/commands/setup-azure.d.ts +17 -0
- package/dist/cli/commands/setup-azure.js +415 -0
- package/dist/cli/commands/setup-bedrock.d.ts +13 -0
- package/dist/cli/commands/setup-bedrock.js +487 -0
- package/dist/cli/commands/setup-gcp.d.ts +18 -0
- package/dist/cli/commands/setup-gcp.js +569 -0
- package/dist/cli/commands/setup-google-ai.d.ts +16 -0
- package/dist/cli/commands/setup-google-ai.js +369 -0
- package/dist/cli/commands/setup-huggingface.d.ts +8 -0
- package/dist/cli/commands/setup-huggingface.js +200 -0
- package/dist/cli/commands/setup-mistral.d.ts +8 -0
- package/dist/cli/commands/setup-mistral.js +233 -0
- package/dist/cli/commands/setup-openai.d.ts +16 -0
- package/dist/cli/commands/setup-openai.js +402 -0
- package/dist/cli/commands/setup.d.ts +19 -0
- package/dist/cli/commands/setup.js +539 -0
- package/dist/cli/errorHandler.d.ts +1 -0
- package/dist/cli/errorHandler.js +28 -0
- package/dist/cli/factories/commandFactory.d.ts +27 -0
- package/dist/cli/factories/commandFactory.js +416 -60
- package/dist/cli/factories/ollamaCommandFactory.js +7 -1
- package/dist/cli/factories/setupCommandFactory.d.ts +18 -0
- package/dist/cli/factories/setupCommandFactory.js +137 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +9 -164
- package/dist/cli/loop/optionsSchema.d.ts +15 -0
- package/dist/cli/loop/optionsSchema.js +59 -0
- package/dist/cli/loop/session.d.ts +15 -0
- package/dist/cli/loop/session.js +252 -0
- package/dist/cli/parser.d.ts +1 -0
- package/dist/cli/parser.js +161 -0
- package/dist/cli/utils/envManager.d.ts +3 -2
- package/dist/cli/utils/envManager.js +18 -4
- package/dist/cli/utils/ollamaUtils.js +6 -0
- package/dist/config/{conversationMemoryConfig.d.ts → conversationMemory.d.ts} +1 -1
- package/dist/core/baseProvider.js +17 -3
- package/dist/core/conversationMemoryFactory.d.ts +23 -0
- package/dist/core/conversationMemoryFactory.js +144 -0
- package/dist/core/conversationMemoryInitializer.d.ts +14 -0
- package/dist/core/conversationMemoryInitializer.js +127 -0
- package/dist/core/conversationMemoryManager.d.ts +3 -2
- package/dist/core/conversationMemoryManager.js +4 -3
- package/dist/core/redisConversationMemoryManager.d.ts +73 -0
- package/dist/core/redisConversationMemoryManager.js +483 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/lib/config/{conversationMemoryConfig.d.ts → conversationMemory.d.ts} +1 -1
- package/dist/lib/core/baseProvider.js +17 -3
- package/dist/lib/core/conversationMemoryFactory.d.ts +23 -0
- package/dist/lib/core/conversationMemoryFactory.js +144 -0
- package/dist/lib/core/conversationMemoryInitializer.d.ts +14 -0
- package/dist/lib/core/conversationMemoryInitializer.js +127 -0
- package/dist/lib/core/conversationMemoryManager.d.ts +3 -2
- package/dist/lib/core/conversationMemoryManager.js +4 -3
- package/dist/lib/core/redisConversationMemoryManager.d.ts +73 -0
- package/dist/lib/core/redisConversationMemoryManager.js +483 -0
- package/dist/lib/core/types.d.ts +1 -1
- package/dist/lib/neurolink.d.ts +15 -9
- package/dist/lib/neurolink.js +218 -67
- package/dist/lib/providers/amazonBedrock.d.ts +4 -4
- package/dist/lib/providers/anthropic.d.ts +4 -4
- package/dist/lib/providers/azureOpenai.d.ts +4 -4
- package/dist/lib/providers/googleAiStudio.d.ts +4 -4
- package/dist/lib/providers/googleAiStudio.js +1 -1
- package/dist/lib/providers/huggingFace.d.ts +4 -4
- package/dist/lib/providers/litellm.d.ts +1 -1
- package/dist/lib/providers/mistral.d.ts +4 -4
- package/dist/lib/providers/mistral.js +2 -2
- package/dist/lib/providers/openAI.d.ts +4 -4
- package/dist/lib/session/globalSessionState.d.ts +27 -0
- package/dist/lib/session/globalSessionState.js +77 -0
- package/dist/lib/types/{conversationTypes.d.ts → conversation.d.ts} +32 -0
- package/dist/lib/types/generateTypes.d.ts +1 -1
- package/dist/lib/types/streamTypes.d.ts +1 -1
- package/dist/lib/utils/conversationMemory.d.ts +22 -0
- package/dist/lib/utils/conversationMemory.js +121 -0
- package/dist/lib/utils/conversationMemoryUtils.d.ts +1 -1
- package/dist/lib/utils/conversationMemoryUtils.js +2 -2
- package/dist/lib/utils/messageBuilder.d.ts +1 -1
- package/dist/lib/utils/messageBuilder.js +1 -1
- package/dist/lib/utils/redis.d.ts +42 -0
- package/dist/lib/utils/redis.js +263 -0
- package/dist/neurolink.d.ts +15 -9
- package/dist/neurolink.js +218 -67
- package/dist/providers/amazonBedrock.d.ts +4 -4
- package/dist/providers/anthropic.d.ts +4 -4
- package/dist/providers/azureOpenai.d.ts +4 -4
- package/dist/providers/googleAiStudio.d.ts +4 -4
- package/dist/providers/googleAiStudio.js +1 -1
- package/dist/providers/huggingFace.d.ts +4 -4
- package/dist/providers/litellm.d.ts +1 -1
- package/dist/providers/mistral.d.ts +4 -4
- package/dist/providers/mistral.js +2 -2
- package/dist/providers/openAI.d.ts +4 -4
- package/dist/session/globalSessionState.d.ts +27 -0
- package/dist/session/globalSessionState.js +77 -0
- package/dist/types/{conversationTypes.d.ts → conversation.d.ts} +32 -0
- package/dist/types/generateTypes.d.ts +1 -1
- package/dist/types/streamTypes.d.ts +1 -1
- package/dist/utils/conversationMemory.d.ts +22 -0
- package/dist/utils/conversationMemory.js +121 -0
- package/dist/utils/conversationMemoryUtils.d.ts +1 -1
- package/dist/utils/conversationMemoryUtils.js +2 -2
- package/dist/utils/messageBuilder.d.ts +1 -1
- package/dist/utils/messageBuilder.js +1 -1
- package/dist/utils/redis.d.ts +42 -0
- package/dist/utils/redis.js +263 -0
- package/package.json +3 -1
- /package/dist/config/{conversationMemoryConfig.js → conversationMemory.js} +0 -0
- /package/dist/lib/config/{conversationMemoryConfig.js → conversationMemory.js} +0 -0
- /package/dist/lib/types/{conversationTypes.js → conversation.js} +0 -0
- /package/dist/types/{conversationTypes.js → conversation.js} +0 -0
@@ -0,0 +1,483 @@
|
|
1
|
+
/**
|
2
|
+
* Redis Conversation Memory Manager for NeuroLink
|
3
|
+
* Redis-based implementation of conversation storage with same interface as ConversationMemoryManager
|
4
|
+
*/
|
5
|
+
import { ConversationMemoryError } from "../types/conversation.js";
|
6
|
+
import { DEFAULT_MAX_TURNS_PER_SESSION, DEFAULT_MAX_SESSIONS, MESSAGES_PER_TURN, } from "../config/conversationMemory.js";
|
7
|
+
import { logger } from "../utils/logger.js";
|
8
|
+
import { NeuroLink } from "../neurolink.js";
|
9
|
+
import { createRedisClient, getSessionKey, getNormalizedConfig, serializeMessages, deserializeMessages, scanKeys, } from "../utils/redis.js";
|
10
|
+
/**
|
11
|
+
* Redis-based implementation of the ConversationMemoryManager
|
12
|
+
* Uses the same interface but stores data in Redis
|
13
|
+
*/
|
14
|
+
export class RedisConversationMemoryManager {
|
15
|
+
config;
|
16
|
+
isInitialized = false;
|
17
|
+
redisConfig;
|
18
|
+
redisClient = null;
|
19
|
+
constructor(config, redisConfig = {}) {
|
20
|
+
this.config = config;
|
21
|
+
this.redisConfig = getNormalizedConfig(redisConfig);
|
22
|
+
}
|
23
|
+
/**
|
24
|
+
* Initialize the memory manager with Redis connection
|
25
|
+
*/
|
26
|
+
async initialize() {
|
27
|
+
if (this.isInitialized) {
|
28
|
+
logger.debug("[RedisConversationMemoryManager] Already initialized, skipping");
|
29
|
+
return;
|
30
|
+
}
|
31
|
+
try {
|
32
|
+
logger.debug("[RedisConversationMemoryManager] Initializing with config", {
|
33
|
+
host: this.redisConfig.host,
|
34
|
+
port: this.redisConfig.port,
|
35
|
+
keyPrefix: this.redisConfig.keyPrefix,
|
36
|
+
ttl: this.redisConfig.ttl,
|
37
|
+
});
|
38
|
+
this.redisClient = await createRedisClient(this.redisConfig);
|
39
|
+
this.isInitialized = true;
|
40
|
+
logger.info("RedisConversationMemoryManager initialized", {
|
41
|
+
storage: "redis",
|
42
|
+
host: this.redisConfig.host,
|
43
|
+
port: this.redisConfig.port,
|
44
|
+
maxSessions: this.config.maxSessions,
|
45
|
+
maxTurnsPerSession: this.config.maxTurnsPerSession,
|
46
|
+
});
|
47
|
+
logger.debug("[RedisConversationMemoryManager] Redis client created successfully", {
|
48
|
+
clientType: this.redisClient?.constructor?.name || "unknown",
|
49
|
+
isConnected: !!this.redisClient,
|
50
|
+
});
|
51
|
+
}
|
52
|
+
catch (error) {
|
53
|
+
logger.error("[RedisConversationMemoryManager] Failed to initialize", {
|
54
|
+
error: error instanceof Error ? error.message : String(error),
|
55
|
+
stack: error instanceof Error ? error.stack : undefined,
|
56
|
+
config: {
|
57
|
+
host: this.redisConfig.host,
|
58
|
+
port: this.redisConfig.port,
|
59
|
+
},
|
60
|
+
});
|
61
|
+
throw new ConversationMemoryError("Failed to initialize Redis conversation memory", "CONFIG_ERROR", { error: error instanceof Error ? error.message : String(error) });
|
62
|
+
}
|
63
|
+
}
|
64
|
+
/**
|
65
|
+
* Store a conversation turn for a session
|
66
|
+
*/
|
67
|
+
async storeConversationTurn(sessionId, userId, userMessage, aiResponse) {
|
68
|
+
logger.debug("[RedisConversationMemoryManager] Storing conversation turn", {
|
69
|
+
sessionId,
|
70
|
+
userId,
|
71
|
+
userMessageLength: userMessage.length,
|
72
|
+
aiResponseLength: aiResponse.length,
|
73
|
+
});
|
74
|
+
await this.ensureInitialized();
|
75
|
+
try {
|
76
|
+
if (!this.redisClient) {
|
77
|
+
throw new Error("Redis client not initialized");
|
78
|
+
}
|
79
|
+
// Generate Redis key
|
80
|
+
const redisKey = getSessionKey(this.redisConfig, sessionId);
|
81
|
+
// Get existing messages
|
82
|
+
const messagesData = await this.redisClient.get(redisKey);
|
83
|
+
const messages = deserializeMessages(messagesData);
|
84
|
+
logger.info("[RedisConversationMemoryManager] Deserialized messages", {
|
85
|
+
messageCount: messages.length,
|
86
|
+
roles: messages.map((m) => m.role),
|
87
|
+
});
|
88
|
+
// Add new messages
|
89
|
+
messages.push({ role: "user", content: userMessage }, { role: "assistant", content: aiResponse });
|
90
|
+
logger.info("[RedisConversationMemoryManager] Added new messages", {
|
91
|
+
newMessageCount: messages.length,
|
92
|
+
latestMessages: [
|
93
|
+
{
|
94
|
+
role: messages[messages.length - 2]?.role,
|
95
|
+
contentLength: messages[messages.length - 2]?.content.length,
|
96
|
+
},
|
97
|
+
{
|
98
|
+
role: messages[messages.length - 1]?.role,
|
99
|
+
contentLength: messages[messages.length - 1]?.content.length,
|
100
|
+
},
|
101
|
+
],
|
102
|
+
});
|
103
|
+
// Handle summarization or message limit
|
104
|
+
if (this.config.enableSummarization) {
|
105
|
+
const userAssistantCount = messages.filter((msg) => msg.role === "user" || msg.role === "assistant").length;
|
106
|
+
const currentTurnCount = Math.floor(userAssistantCount / MESSAGES_PER_TURN);
|
107
|
+
logger.debug("[RedisConversationMemoryManager] Checking summarization threshold", {
|
108
|
+
userAssistantCount,
|
109
|
+
currentTurnCount,
|
110
|
+
summarizationThreshold: this.config.summarizationThresholdTurns || 20,
|
111
|
+
shouldSummarize: currentTurnCount >=
|
112
|
+
(this.config.summarizationThresholdTurns || 20),
|
113
|
+
});
|
114
|
+
if (currentTurnCount >= (this.config.summarizationThresholdTurns || 20)) {
|
115
|
+
await this._summarizeMessages(sessionId, userId, messages);
|
116
|
+
return;
|
117
|
+
}
|
118
|
+
}
|
119
|
+
else {
|
120
|
+
const maxMessages = (this.config.maxTurnsPerSession || DEFAULT_MAX_TURNS_PER_SESSION) *
|
121
|
+
MESSAGES_PER_TURN;
|
122
|
+
logger.debug("[RedisConversationMemoryManager] Checking message limit", {
|
123
|
+
currentMessageCount: messages.length,
|
124
|
+
maxMessages,
|
125
|
+
shouldTrimMessages: messages.length > maxMessages,
|
126
|
+
});
|
127
|
+
if (messages.length > maxMessages) {
|
128
|
+
const trimCount = messages.length - maxMessages;
|
129
|
+
logger.debug("[RedisConversationMemoryManager] Trimming messages", {
|
130
|
+
beforeCount: messages.length,
|
131
|
+
trimCount,
|
132
|
+
afterCount: maxMessages,
|
133
|
+
});
|
134
|
+
messages.splice(0, messages.length - maxMessages);
|
135
|
+
}
|
136
|
+
}
|
137
|
+
// Save updated messages
|
138
|
+
const serializedData = serializeMessages(messages);
|
139
|
+
logger.debug("[RedisConversationMemoryManager] Saving messages to Redis", {
|
140
|
+
redisKey,
|
141
|
+
messageCount: messages.length,
|
142
|
+
serializedDataLength: serializedData.length,
|
143
|
+
});
|
144
|
+
await this.redisClient.set(redisKey, serializedData);
|
145
|
+
// Set TTL if configured
|
146
|
+
if (this.redisConfig.ttl > 0) {
|
147
|
+
logger.debug("[RedisConversationMemoryManager] Setting Redis TTL", {
|
148
|
+
redisKey,
|
149
|
+
ttl: this.redisConfig.ttl,
|
150
|
+
});
|
151
|
+
await this.redisClient.expire(redisKey, this.redisConfig.ttl);
|
152
|
+
}
|
153
|
+
// Enforce session limit
|
154
|
+
await this.enforceSessionLimit();
|
155
|
+
logger.debug("[RedisConversationMemoryManager] Successfully stored conversation turn", {
|
156
|
+
sessionId,
|
157
|
+
totalMessages: messages.length,
|
158
|
+
});
|
159
|
+
}
|
160
|
+
catch (error) {
|
161
|
+
throw new ConversationMemoryError(`Failed to store conversation turn in Redis for session ${sessionId}`, "STORAGE_ERROR", {
|
162
|
+
sessionId,
|
163
|
+
error: error instanceof Error ? error.message : String(error),
|
164
|
+
});
|
165
|
+
}
|
166
|
+
}
|
167
|
+
/**
|
168
|
+
* Build context messages for AI prompt injection
|
169
|
+
*/
|
170
|
+
async buildContextMessages(sessionId) {
|
171
|
+
logger.info("[RedisConversationMemoryManager] Building context messages", {
|
172
|
+
sessionId,
|
173
|
+
method: "buildContextMessages",
|
174
|
+
});
|
175
|
+
await this.ensureInitialized();
|
176
|
+
if (!this.redisClient) {
|
177
|
+
logger.warn("[RedisConversationMemoryManager] Redis client not available, returning empty context", {
|
178
|
+
sessionId,
|
179
|
+
});
|
180
|
+
return [];
|
181
|
+
}
|
182
|
+
const redisKey = getSessionKey(this.redisConfig, sessionId);
|
183
|
+
logger.info("[RedisConversationMemoryManager] Getting messages from Redis", {
|
184
|
+
sessionId,
|
185
|
+
redisKey,
|
186
|
+
});
|
187
|
+
const messagesData = await this.redisClient.get(redisKey);
|
188
|
+
logger.info("[RedisConversationMemoryManager] Retrieved message data from Redis", {
|
189
|
+
sessionId,
|
190
|
+
redisKey,
|
191
|
+
hasData: !!messagesData,
|
192
|
+
dataLength: messagesData?.length || 0,
|
193
|
+
});
|
194
|
+
const messages = deserializeMessages(messagesData);
|
195
|
+
logger.info("[RedisConversationMemoryManager] Deserialized messages for context", {
|
196
|
+
sessionId,
|
197
|
+
messageCount: messages.length,
|
198
|
+
messageRoles: messages.map((m) => m.role),
|
199
|
+
firstMessagePreview: messages[0]?.content?.substring(0, 50),
|
200
|
+
lastMessagePreview: messages[messages.length - 1]?.content?.substring(0, 50),
|
201
|
+
});
|
202
|
+
return messages;
|
203
|
+
}
|
204
|
+
/**
|
205
|
+
* Get session data
|
206
|
+
*/
|
207
|
+
async getSession(sessionId) {
|
208
|
+
logger.debug("[RedisConversationMemoryManager] Getting session", {
|
209
|
+
sessionId,
|
210
|
+
method: "getSession",
|
211
|
+
});
|
212
|
+
await this.ensureInitialized();
|
213
|
+
if (!this.redisClient) {
|
214
|
+
logger.warn("[RedisConversationMemoryManager] Redis client not available", {
|
215
|
+
sessionId,
|
216
|
+
});
|
217
|
+
return undefined;
|
218
|
+
}
|
219
|
+
const redisKey = getSessionKey(this.redisConfig, sessionId);
|
220
|
+
logger.debug("[RedisConversationMemoryManager] Getting session data from Redis", {
|
221
|
+
sessionId,
|
222
|
+
redisKey,
|
223
|
+
});
|
224
|
+
const messagesData = await this.redisClient.get(redisKey);
|
225
|
+
logger.debug("[RedisConversationMemoryManager] Retrieved session data", {
|
226
|
+
sessionId,
|
227
|
+
hasData: !!messagesData,
|
228
|
+
dataLength: messagesData?.length || 0,
|
229
|
+
});
|
230
|
+
if (!messagesData) {
|
231
|
+
logger.debug("[RedisConversationMemoryManager] No session data found", {
|
232
|
+
sessionId,
|
233
|
+
redisKey,
|
234
|
+
});
|
235
|
+
return undefined;
|
236
|
+
}
|
237
|
+
const messages = deserializeMessages(messagesData);
|
238
|
+
logger.debug("[RedisConversationMemoryManager] Deserialized session messages", {
|
239
|
+
sessionId,
|
240
|
+
messageCount: messages.length,
|
241
|
+
messageRoles: messages.map((m) => m.role),
|
242
|
+
});
|
243
|
+
// We don't store the full SessionMemory object in Redis,
|
244
|
+
// just the messages, so we recreate the SessionMemory object here
|
245
|
+
const session = {
|
246
|
+
sessionId,
|
247
|
+
messages,
|
248
|
+
createdAt: Date.now(), // We don't have this information
|
249
|
+
lastActivity: Date.now(), // We don't have this information
|
250
|
+
};
|
251
|
+
logger.debug("[RedisConversationMemoryManager] Created session memory object", {
|
252
|
+
sessionId,
|
253
|
+
messageCount: session.messages.length,
|
254
|
+
});
|
255
|
+
return session;
|
256
|
+
}
|
257
|
+
/**
|
258
|
+
* Create summary system message
|
259
|
+
*/
|
260
|
+
createSummarySystemMessage(content) {
|
261
|
+
return {
|
262
|
+
role: "system",
|
263
|
+
content: `Summary of previous conversation turns:\n\n${content}`,
|
264
|
+
};
|
265
|
+
}
|
266
|
+
/**
|
267
|
+
* Close Redis connection
|
268
|
+
*/
|
269
|
+
async close() {
|
270
|
+
if (this.redisClient) {
|
271
|
+
await this.redisClient.quit();
|
272
|
+
this.redisClient = null;
|
273
|
+
this.isInitialized = false;
|
274
|
+
logger.info("Redis connection closed");
|
275
|
+
}
|
276
|
+
}
|
277
|
+
/**
|
278
|
+
* Get statistics about conversation storage
|
279
|
+
*/
|
280
|
+
async getStats() {
|
281
|
+
await this.ensureInitialized();
|
282
|
+
if (!this.redisClient) {
|
283
|
+
return { totalSessions: 0, totalTurns: 0 };
|
284
|
+
}
|
285
|
+
// Get all session keys using SCAN instead of KEYS to avoid blocking
|
286
|
+
const pattern = `${this.redisConfig.keyPrefix}*`;
|
287
|
+
const keys = await scanKeys(this.redisClient, pattern);
|
288
|
+
logger.debug("[RedisConversationMemoryManager] Got session keys with SCAN", {
|
289
|
+
pattern,
|
290
|
+
keyCount: keys.length,
|
291
|
+
});
|
292
|
+
// Count messages in each session
|
293
|
+
let totalTurns = 0;
|
294
|
+
for (const key of keys) {
|
295
|
+
const messagesData = await this.redisClient.get(key);
|
296
|
+
const messages = deserializeMessages(messagesData);
|
297
|
+
totalTurns += messages.length / MESSAGES_PER_TURN;
|
298
|
+
}
|
299
|
+
return {
|
300
|
+
totalSessions: keys.length,
|
301
|
+
totalTurns,
|
302
|
+
};
|
303
|
+
}
|
304
|
+
/**
|
305
|
+
* Clear a specific session
|
306
|
+
*/
|
307
|
+
async clearSession(sessionId) {
|
308
|
+
await this.ensureInitialized();
|
309
|
+
if (!this.redisClient) {
|
310
|
+
return false;
|
311
|
+
}
|
312
|
+
const redisKey = getSessionKey(this.redisConfig, sessionId);
|
313
|
+
const result = await this.redisClient.del(redisKey);
|
314
|
+
if (result > 0) {
|
315
|
+
logger.info("Redis session cleared", { sessionId });
|
316
|
+
return true;
|
317
|
+
}
|
318
|
+
return false;
|
319
|
+
}
|
320
|
+
/**
|
321
|
+
* Clear all sessions
|
322
|
+
*/
|
323
|
+
async clearAllSessions() {
|
324
|
+
await this.ensureInitialized();
|
325
|
+
if (!this.redisClient) {
|
326
|
+
return;
|
327
|
+
}
|
328
|
+
const pattern = `${this.redisConfig.keyPrefix}*`;
|
329
|
+
// Use SCAN instead of KEYS to avoid blocking the server
|
330
|
+
const keys = await scanKeys(this.redisClient, pattern);
|
331
|
+
logger.debug("[RedisConversationMemoryManager] Got session keys with SCAN for clearing", {
|
332
|
+
pattern,
|
333
|
+
keyCount: keys.length,
|
334
|
+
});
|
335
|
+
if (keys.length > 0) {
|
336
|
+
// Process keys in batches to avoid blocking Redis for too long
|
337
|
+
const batchSize = 100;
|
338
|
+
for (let i = 0; i < keys.length; i += batchSize) {
|
339
|
+
const batch = keys.slice(i, i + batchSize);
|
340
|
+
await this.redisClient.del(batch);
|
341
|
+
logger.debug("[RedisConversationMemoryManager] Cleared batch of sessions", {
|
342
|
+
batchIndex: Math.floor(i / batchSize) + 1,
|
343
|
+
batchSize: batch.length,
|
344
|
+
totalProcessed: i + batch.length,
|
345
|
+
totalKeys: keys.length,
|
346
|
+
});
|
347
|
+
}
|
348
|
+
logger.info("All Redis sessions cleared", { clearedCount: keys.length });
|
349
|
+
}
|
350
|
+
}
|
351
|
+
/**
|
352
|
+
* Summarize messages for a session
|
353
|
+
*/
|
354
|
+
async _summarizeMessages(sessionId, userId, messages) {
|
355
|
+
logger.info(`[RedisConversationMemory] Summarizing session ${sessionId}...`);
|
356
|
+
logger.debug("[RedisConversationMemoryManager] Starting message summarization", {
|
357
|
+
sessionId,
|
358
|
+
userId,
|
359
|
+
messageCount: messages.length,
|
360
|
+
messageTypes: messages.map((m) => m.role),
|
361
|
+
});
|
362
|
+
const targetTurns = this.config.summarizationTargetTurns || 10;
|
363
|
+
const splitIndex = Math.max(0, messages.length - targetTurns * MESSAGES_PER_TURN);
|
364
|
+
const messagesToSummarize = messages.slice(0, splitIndex);
|
365
|
+
const recentMessages = messages.slice(splitIndex);
|
366
|
+
if (messagesToSummarize.length === 0) {
|
367
|
+
return;
|
368
|
+
}
|
369
|
+
const summarizationPrompt = this._createSummarizationPrompt(messagesToSummarize);
|
370
|
+
const summarizer = new NeuroLink({
|
371
|
+
conversationMemory: { enabled: false },
|
372
|
+
});
|
373
|
+
try {
|
374
|
+
const providerName = this.config.summarizationProvider;
|
375
|
+
// Map provider names to correct format
|
376
|
+
let mappedProvider = providerName;
|
377
|
+
if (providerName === "vertex") {
|
378
|
+
mappedProvider = "googlevertex";
|
379
|
+
}
|
380
|
+
if (!mappedProvider) {
|
381
|
+
logger.error(`[RedisConversationMemory] Missing summarization provider`);
|
382
|
+
return;
|
383
|
+
}
|
384
|
+
logger.debug(`[RedisConversationMemory] Using provider: ${mappedProvider} for summarization`);
|
385
|
+
const summaryResult = await summarizer.generate({
|
386
|
+
input: { text: summarizationPrompt },
|
387
|
+
provider: mappedProvider,
|
388
|
+
model: this.config.summarizationModel,
|
389
|
+
disableTools: true,
|
390
|
+
});
|
391
|
+
if (!this.redisClient) {
|
392
|
+
throw new Error("Redis client not initialized");
|
393
|
+
}
|
394
|
+
if (summaryResult.content) {
|
395
|
+
const updatedMessages = [
|
396
|
+
this.createSummarySystemMessage(summaryResult.content),
|
397
|
+
...recentMessages,
|
398
|
+
];
|
399
|
+
const redisKey = getSessionKey(this.redisConfig, sessionId);
|
400
|
+
await this.redisClient.set(redisKey, serializeMessages(updatedMessages));
|
401
|
+
// Set TTL if configured
|
402
|
+
if (this.redisConfig.ttl > 0) {
|
403
|
+
await this.redisClient.expire(redisKey, this.redisConfig.ttl);
|
404
|
+
}
|
405
|
+
logger.info(`[RedisConversationMemory] Summarization complete for session ${sessionId}.`);
|
406
|
+
}
|
407
|
+
else {
|
408
|
+
logger.warn(`[RedisConversationMemory] Summarization failed for session ${sessionId}. History not modified.`);
|
409
|
+
}
|
410
|
+
}
|
411
|
+
catch (error) {
|
412
|
+
logger.error(`[RedisConversationMemory] Error during summarization for session ${sessionId}`, { error });
|
413
|
+
}
|
414
|
+
}
|
415
|
+
/**
|
416
|
+
* Create summarization prompt
|
417
|
+
*/
|
418
|
+
_createSummarizationPrompt(history) {
|
419
|
+
const formattedHistory = history
|
420
|
+
.map((msg) => `${msg.role}: ${msg.content}`)
|
421
|
+
.join("\n\n");
|
422
|
+
return `
|
423
|
+
You are a context summarization AI. Your task is to condense the following conversation history for another AI assistant.
|
424
|
+
The summary must be a concise, third-person narrative that retains all critical information, including key entities, technical details, decisions made, and any specific dates or times mentioned.
|
425
|
+
Ensure the summary flows logically and is ready to be used as context for the next turn in the conversation.
|
426
|
+
|
427
|
+
Conversation History to Summarize:
|
428
|
+
---
|
429
|
+
${formattedHistory}
|
430
|
+
---
|
431
|
+
`.trim();
|
432
|
+
}
|
433
|
+
/**
|
434
|
+
* Ensure Redis client is initialized
|
435
|
+
*/
|
436
|
+
async ensureInitialized() {
|
437
|
+
logger.debug("[RedisConversationMemoryManager] Ensuring initialization");
|
438
|
+
if (!this.isInitialized) {
|
439
|
+
logger.debug("[RedisConversationMemoryManager] Not initialized, initializing now");
|
440
|
+
await this.initialize();
|
441
|
+
}
|
442
|
+
else {
|
443
|
+
logger.debug("[RedisConversationMemoryManager] Already initialized");
|
444
|
+
}
|
445
|
+
}
|
446
|
+
/**
|
447
|
+
* Enforce session limit
|
448
|
+
*
|
449
|
+
* NOTE: This function is maintained only for backward compatibility with the
|
450
|
+
* in-memory implementation. In Redis, we do not actually delete sessions based on
|
451
|
+
* a global limit, as this could cause race conditions in a distributed environment.
|
452
|
+
* Each session is managed independently with its own TTL.
|
453
|
+
*/
|
454
|
+
async enforceSessionLimit() {
|
455
|
+
logger.debug("[RedisConversationMemoryManager] Enforcing session limit (compatibility function)");
|
456
|
+
if (!this.redisClient) {
|
457
|
+
logger.debug("[RedisConversationMemoryManager] No Redis client, skipping session limit check");
|
458
|
+
return;
|
459
|
+
}
|
460
|
+
const maxSessions = this.config.maxSessions || DEFAULT_MAX_SESSIONS;
|
461
|
+
const pattern = `${this.redisConfig.keyPrefix}*`;
|
462
|
+
logger.debug("[RedisConversationMemoryManager] Listing all session keys", {
|
463
|
+
pattern,
|
464
|
+
maxSessions,
|
465
|
+
});
|
466
|
+
// Use SCAN instead of KEYS to avoid blocking the server
|
467
|
+
const keys = await scanKeys(this.redisClient, pattern);
|
468
|
+
logger.debug("[RedisConversationMemoryManager] Found existing sessions using SCAN", {
|
469
|
+
sessionCount: keys.length,
|
470
|
+
maxSessions,
|
471
|
+
needsTrimming: keys.length > maxSessions,
|
472
|
+
});
|
473
|
+
// In the Redis implementation, we intentionally do not delete sessions based on a global limit.
|
474
|
+
// Each session is managed independently with its own TTL.
|
475
|
+
if (keys.length > maxSessions) {
|
476
|
+
logger.info("Redis session count exceeds limit, but not enforcing deletion", {
|
477
|
+
currentCount: keys.length,
|
478
|
+
maxSessions,
|
479
|
+
reason: "Redis sessions are managed independently with TTL",
|
480
|
+
});
|
481
|
+
}
|
482
|
+
}
|
483
|
+
}
|
package/dist/core/types.d.ts
CHANGED
@@ -3,7 +3,7 @@ import type { ZodUnknownSchema, ValidationSchema } from "../types/typeAliases.js
|
|
3
3
|
import type { GenerateResult } from "../types/generateTypes.js";
|
4
4
|
import type { StreamOptions, StreamResult } from "../types/streamTypes.js";
|
5
5
|
import type { JsonValue } from "../types/common.js";
|
6
|
-
import type { ChatMessage, ConversationMemoryConfig } from "../types/
|
6
|
+
import type { ChatMessage, ConversationMemoryConfig } from "../types/conversation.js";
|
7
7
|
import type { TokenUsage, AnalyticsData } from "../types/analytics.js";
|
8
8
|
import type { EvaluationData } from "../index.js";
|
9
9
|
export type { EvaluationData };
|
@@ -2,7 +2,7 @@
|
|
2
2
|
* Conversation Memory Configuration
|
3
3
|
* Provides default values for conversation memory feature with environment variable support
|
4
4
|
*/
|
5
|
-
import type { ConversationMemoryConfig } from "../types/
|
5
|
+
import type { ConversationMemoryConfig } from "../types/conversation.js";
|
6
6
|
/**
|
7
7
|
* Default maximum number of turns per session
|
8
8
|
*/
|
@@ -100,7 +100,15 @@ export class BaseProvider {
|
|
100
100
|
yield { content: buffer };
|
101
101
|
buffer = "";
|
102
102
|
// Small delay to simulate streaming (1-10ms)
|
103
|
-
await new Promise((resolve
|
103
|
+
await new Promise((resolve, reject) => {
|
104
|
+
const timeoutId = setTimeout(resolve, Math.random() * 9 + 1);
|
105
|
+
// Handle potential timeout issues
|
106
|
+
if (!timeoutId) {
|
107
|
+
reject(new Error("Failed to create timeout"));
|
108
|
+
}
|
109
|
+
}).catch((err) => {
|
110
|
+
logger.error("Error in streaming delay:", err);
|
111
|
+
});
|
104
112
|
}
|
105
113
|
}
|
106
114
|
// Yield all remaining content
|
@@ -217,8 +225,14 @@ export class BaseProvider {
|
|
217
225
|
// Accumulate the streamed content
|
218
226
|
let accumulatedContent = "";
|
219
227
|
// Wait for the stream to complete and accumulate content
|
220
|
-
|
221
|
-
|
228
|
+
try {
|
229
|
+
for await (const chunk of streamResult.textStream) {
|
230
|
+
accumulatedContent += chunk;
|
231
|
+
}
|
232
|
+
}
|
233
|
+
catch (streamError) {
|
234
|
+
logger.error(`Error reading text stream for ${this.providerName}:`, streamError);
|
235
|
+
throw streamError;
|
222
236
|
}
|
223
237
|
// Get the final result - this should include usage, toolCalls, etc.
|
224
238
|
const usage = await streamResult.usage;
|
@@ -0,0 +1,23 @@
|
|
1
|
+
/**
|
2
|
+
* Conversation Memory Factory for NeuroLink
|
3
|
+
* Creates appropriate conversation memory manager based on configuration
|
4
|
+
*/
|
5
|
+
import type { ConversationMemoryConfig, RedisStorageConfig } from "../types/conversation.js";
|
6
|
+
import { ConversationMemoryManager } from "./conversationMemoryManager.js";
|
7
|
+
import { RedisConversationMemoryManager } from "./redisConversationMemoryManager.js";
|
8
|
+
/**
|
9
|
+
* Configuration for memory storage type
|
10
|
+
*/
|
11
|
+
export type StorageType = "memory" | "redis";
|
12
|
+
/**
|
13
|
+
* Creates a conversation memory manager based on configuration
|
14
|
+
*/
|
15
|
+
export declare function createConversationMemoryManager(config: ConversationMemoryConfig, storageType?: StorageType, redisConfig?: RedisStorageConfig): ConversationMemoryManager | RedisConversationMemoryManager;
|
16
|
+
/**
|
17
|
+
* Get storage type from environment variable or configuration
|
18
|
+
*/
|
19
|
+
export declare function getStorageType(): StorageType;
|
20
|
+
/**
|
21
|
+
* Get Redis configuration from environment variables
|
22
|
+
*/
|
23
|
+
export declare function getRedisConfigFromEnv(): RedisStorageConfig;
|