@juspay/neurolink 8.19.0 → 8.20.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 +12 -0
- package/dist/adapters/providerImageAdapter.d.ts +12 -0
- package/dist/adapters/providerImageAdapter.js +30 -3
- package/dist/cli/loop/optionsSchema.js +4 -0
- package/dist/config/conversationMemory.d.ts +17 -1
- package/dist/config/conversationMemory.js +37 -10
- package/dist/core/baseProvider.js +23 -13
- package/dist/core/conversationMemoryFactory.js +0 -3
- package/dist/core/conversationMemoryInitializer.js +1 -9
- package/dist/core/conversationMemoryManager.d.ts +31 -8
- package/dist/core/conversationMemoryManager.js +174 -80
- package/dist/core/modules/GenerationHandler.d.ts +5 -0
- package/dist/core/modules/GenerationHandler.js +56 -9
- package/dist/core/redisConversationMemoryManager.d.ts +28 -13
- package/dist/core/redisConversationMemoryManager.js +211 -121
- package/dist/lib/adapters/providerImageAdapter.d.ts +12 -0
- package/dist/lib/adapters/providerImageAdapter.js +30 -3
- package/dist/lib/config/conversationMemory.d.ts +17 -1
- package/dist/lib/config/conversationMemory.js +37 -10
- package/dist/lib/core/baseProvider.js +23 -13
- package/dist/lib/core/conversationMemoryFactory.js +0 -3
- package/dist/lib/core/conversationMemoryInitializer.js +1 -9
- package/dist/lib/core/conversationMemoryManager.d.ts +31 -8
- package/dist/lib/core/conversationMemoryManager.js +174 -80
- package/dist/lib/core/modules/GenerationHandler.d.ts +5 -0
- package/dist/lib/core/modules/GenerationHandler.js +56 -9
- package/dist/lib/core/redisConversationMemoryManager.d.ts +28 -13
- package/dist/lib/core/redisConversationMemoryManager.js +211 -121
- package/dist/lib/mcp/servers/agent/directToolsServer.js +5 -0
- package/dist/lib/mcp/toolRegistry.js +5 -0
- package/dist/lib/neurolink.js +29 -22
- package/dist/lib/types/conversation.d.ts +58 -9
- package/dist/lib/types/generateTypes.d.ts +1 -0
- package/dist/lib/types/sdkTypes.d.ts +1 -1
- package/dist/lib/types/streamTypes.d.ts +1 -0
- package/dist/lib/utils/conversationMemory.d.ts +43 -1
- package/dist/lib/utils/conversationMemory.js +181 -5
- package/dist/lib/utils/conversationMemoryUtils.js +16 -1
- package/dist/lib/utils/fileDetector.d.ts +25 -0
- package/dist/lib/utils/fileDetector.js +433 -10
- package/dist/lib/utils/messageBuilder.js +6 -2
- package/dist/lib/utils/redis.js +0 -5
- package/dist/mcp/servers/agent/directToolsServer.js +5 -0
- package/dist/mcp/toolRegistry.js +5 -0
- package/dist/neurolink.js +29 -22
- package/dist/types/conversation.d.ts +58 -9
- package/dist/types/generateTypes.d.ts +1 -0
- package/dist/types/sdkTypes.d.ts +1 -1
- package/dist/types/streamTypes.d.ts +1 -0
- package/dist/utils/conversationMemory.d.ts +43 -1
- package/dist/utils/conversationMemory.js +181 -5
- package/dist/utils/conversationMemoryUtils.js +16 -1
- package/dist/utils/fileDetector.d.ts +25 -0
- package/dist/utils/fileDetector.js +433 -10
- package/dist/utils/messageBuilder.js +6 -2
- package/dist/utils/redis.js +0 -5
- package/package.json +1 -1
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
6
|
import { ConversationMemoryError } from "../types/conversation.js";
|
|
7
|
-
import { MESSAGES_PER_TURN } from "../config/conversationMemory.js";
|
|
7
|
+
import { MESSAGES_PER_TURN, RECENT_MESSAGES_RATIO, } from "../config/conversationMemory.js";
|
|
8
8
|
import { logger } from "../utils/logger.js";
|
|
9
9
|
import { NeuroLink } from "../neurolink.js";
|
|
10
10
|
import { createRedisClient, getSessionKey, getUserSessionsKey, getNormalizedConfig, serializeConversation, deserializeConversation, scanKeys, } from "../utils/redis.js";
|
|
11
|
+
import { TokenUtils } from "../constants/tokens.js";
|
|
12
|
+
import { buildContextFromPointer, getEffectiveTokenThreshold, generateSummary, } from "../utils/conversationMemory.js";
|
|
11
13
|
/**
|
|
12
14
|
* Redis-based implementation of the ConversationMemoryManager
|
|
13
15
|
* Uses the same interface but stores data in Redis
|
|
@@ -27,6 +29,11 @@ export class RedisConversationMemoryManager {
|
|
|
27
29
|
* Key format: "${sessionId}:${userId}"
|
|
28
30
|
*/
|
|
29
31
|
titleGenerationInProgress = new Set();
|
|
32
|
+
/**
|
|
33
|
+
* Track sessions currently being summarized to prevent race conditions
|
|
34
|
+
* Key format: "${sessionId}:${userId}"
|
|
35
|
+
*/
|
|
36
|
+
summarizationInProgress = new Set();
|
|
30
37
|
constructor(config, redisConfig = {}) {
|
|
31
38
|
this.config = config;
|
|
32
39
|
this.redisConfig = getNormalizedConfig(redisConfig);
|
|
@@ -138,25 +145,12 @@ export class RedisConversationMemoryManager {
|
|
|
138
145
|
return false;
|
|
139
146
|
}
|
|
140
147
|
}
|
|
141
|
-
/**
|
|
142
|
-
* Generate next message ID for a conversation
|
|
143
|
-
*/
|
|
144
|
-
generateMessageId(conversation) {
|
|
145
|
-
const currentCount = conversation?.messages?.length || 0;
|
|
146
|
-
return `msg_${currentCount + 1}`;
|
|
147
|
-
}
|
|
148
148
|
/**
|
|
149
149
|
* Generate current timestamp in ISO format
|
|
150
150
|
*/
|
|
151
151
|
generateTimestamp() {
|
|
152
152
|
return new Date().toISOString();
|
|
153
153
|
}
|
|
154
|
-
/**
|
|
155
|
-
* Generate a unique conversation ID using UUID v4
|
|
156
|
-
*/
|
|
157
|
-
generateUniqueId() {
|
|
158
|
-
return randomUUID();
|
|
159
|
-
}
|
|
160
154
|
/**
|
|
161
155
|
* Store tool execution data for a session (temporarily to avoid race conditions)
|
|
162
156
|
*/
|
|
@@ -224,49 +218,31 @@ export class RedisConversationMemoryManager {
|
|
|
224
218
|
/**
|
|
225
219
|
* Store a conversation turn for a session
|
|
226
220
|
*/
|
|
227
|
-
async storeConversationTurn(
|
|
221
|
+
async storeConversationTurn(options) {
|
|
228
222
|
logger.debug("[RedisConversationMemoryManager] Storing conversation turn", {
|
|
229
|
-
sessionId,
|
|
230
|
-
userId,
|
|
231
|
-
userMessageLength: userMessage.length,
|
|
232
|
-
aiResponseLength: aiResponse.length,
|
|
223
|
+
sessionId: options.sessionId,
|
|
224
|
+
userId: options.userId,
|
|
233
225
|
});
|
|
234
226
|
await this.ensureInitialized();
|
|
235
227
|
try {
|
|
236
228
|
if (!this.redisClient) {
|
|
237
229
|
throw new Error("Redis client not initialized");
|
|
238
230
|
}
|
|
239
|
-
|
|
240
|
-
const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
|
|
241
|
-
// Get existing conversation object
|
|
231
|
+
const redisKey = getSessionKey(this.redisConfig, options.sessionId, options.userId);
|
|
242
232
|
const conversationData = await this.redisClient.get(redisKey);
|
|
243
233
|
let conversation = deserializeConversation(conversationData);
|
|
244
234
|
const currentTime = new Date().toISOString();
|
|
245
|
-
const normalizedUserId = userId || "randomUser";
|
|
246
|
-
// If no existing conversation, create a new one
|
|
235
|
+
const normalizedUserId = options.userId || "randomUser";
|
|
247
236
|
if (!conversation) {
|
|
248
|
-
|
|
249
|
-
const titleGenerationKey = `${sessionId}:${normalizedUserId}`;
|
|
237
|
+
const titleGenerationKey = `${options.sessionId}:${normalizedUserId}`;
|
|
250
238
|
setImmediate(async () => {
|
|
251
|
-
// Check if title generation is already in progress for this session
|
|
252
239
|
if (this.titleGenerationInProgress.has(titleGenerationKey)) {
|
|
253
|
-
logger.debug("[RedisConversationMemoryManager] Title generation already in progress, skipping", {
|
|
254
|
-
sessionId,
|
|
255
|
-
userId: normalizedUserId,
|
|
256
|
-
titleGenerationKey,
|
|
257
|
-
});
|
|
258
240
|
return;
|
|
259
241
|
}
|
|
260
|
-
// Mark title generation as in progress
|
|
261
242
|
this.titleGenerationInProgress.add(titleGenerationKey);
|
|
262
243
|
try {
|
|
263
|
-
const title = await this.generateConversationTitle(userMessage);
|
|
264
|
-
|
|
265
|
-
sessionId,
|
|
266
|
-
userId: normalizedUserId,
|
|
267
|
-
title,
|
|
268
|
-
});
|
|
269
|
-
const updatedRedisKey = getSessionKey(this.redisConfig, sessionId, userId || undefined);
|
|
244
|
+
const title = await this.generateConversationTitle(options.userMessage);
|
|
245
|
+
const updatedRedisKey = getSessionKey(this.redisConfig, options.sessionId, options.userId || undefined);
|
|
270
246
|
const updatedConversationData = await this.redisClient?.get(updatedRedisKey);
|
|
271
247
|
const updatedConversation = deserializeConversation(updatedConversationData || null);
|
|
272
248
|
if (updatedConversation) {
|
|
@@ -281,7 +257,7 @@ export class RedisConversationMemoryManager {
|
|
|
281
257
|
}
|
|
282
258
|
catch (titleError) {
|
|
283
259
|
logger.warn("[RedisConversationMemoryManager] Failed to generate conversation title in background", {
|
|
284
|
-
sessionId,
|
|
260
|
+
sessionId: options.sessionId,
|
|
285
261
|
userId: normalizedUserId,
|
|
286
262
|
error: titleError instanceof Error
|
|
287
263
|
? titleError.message
|
|
@@ -289,136 +265,243 @@ export class RedisConversationMemoryManager {
|
|
|
289
265
|
});
|
|
290
266
|
}
|
|
291
267
|
finally {
|
|
292
|
-
// Always remove from tracking set when done (success or failure)
|
|
293
268
|
this.titleGenerationInProgress.delete(titleGenerationKey);
|
|
294
|
-
logger.debug("[RedisConversationMemoryManager] Title generation completed, removed from tracking", {
|
|
295
|
-
sessionId,
|
|
296
|
-
userId: normalizedUserId,
|
|
297
|
-
titleGenerationKey,
|
|
298
|
-
remainingInProgress: this.titleGenerationInProgress.size,
|
|
299
|
-
});
|
|
300
269
|
}
|
|
301
270
|
});
|
|
302
271
|
conversation = {
|
|
303
|
-
id:
|
|
272
|
+
id: randomUUID(),
|
|
304
273
|
title: "New Conversation", // Temporary title until generated
|
|
305
|
-
sessionId,
|
|
274
|
+
sessionId: options.sessionId,
|
|
306
275
|
userId: normalizedUserId,
|
|
307
|
-
createdAt: startTimeStamp?.toISOString() || currentTime,
|
|
308
|
-
updatedAt: startTimeStamp?.toISOString() || currentTime,
|
|
276
|
+
createdAt: options.startTimeStamp?.toISOString() || currentTime,
|
|
277
|
+
updatedAt: options.startTimeStamp?.toISOString() || currentTime,
|
|
309
278
|
messages: [],
|
|
310
279
|
};
|
|
311
280
|
}
|
|
312
281
|
else {
|
|
313
|
-
// Update existing conversation timestamp
|
|
314
282
|
conversation.updatedAt = currentTime;
|
|
315
283
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
sessionId: conversation.sessionId,
|
|
320
|
-
userId: conversation.userId,
|
|
321
|
-
});
|
|
322
|
-
// Add new messages to conversation history with new format
|
|
284
|
+
const tokenThreshold = options.providerDetails
|
|
285
|
+
? getEffectiveTokenThreshold(options.providerDetails.provider, options.providerDetails.model, this.config.tokenThreshold, conversation.tokenThreshold)
|
|
286
|
+
: this.config.tokenThreshold || 50000;
|
|
323
287
|
const userMsg = {
|
|
324
|
-
id:
|
|
325
|
-
timestamp: startTimeStamp?.toISOString() || this.generateTimestamp(),
|
|
288
|
+
id: randomUUID(),
|
|
289
|
+
timestamp: options.startTimeStamp?.toISOString() || this.generateTimestamp(),
|
|
326
290
|
role: "user",
|
|
327
|
-
content: userMessage,
|
|
291
|
+
content: options.userMessage,
|
|
328
292
|
};
|
|
329
293
|
conversation.messages.push(userMsg);
|
|
330
|
-
await this.flushPendingToolData(conversation, sessionId, normalizedUserId);
|
|
294
|
+
await this.flushPendingToolData(conversation, options.sessionId, normalizedUserId);
|
|
331
295
|
const assistantMsg = {
|
|
332
|
-
id:
|
|
296
|
+
id: randomUUID(),
|
|
333
297
|
timestamp: this.generateTimestamp(),
|
|
334
298
|
role: "assistant",
|
|
335
|
-
content: aiResponse,
|
|
299
|
+
content: options.aiResponse,
|
|
336
300
|
};
|
|
337
301
|
conversation.messages.push(assistantMsg);
|
|
338
302
|
logger.info("[RedisConversationMemoryManager] Added new messages", {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
{
|
|
342
|
-
role: conversation.messages[conversation.messages.length - 2]?.role,
|
|
343
|
-
contentLength: conversation.messages[conversation.messages.length - 2]?.content
|
|
344
|
-
.length,
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
role: conversation.messages[conversation.messages.length - 1]?.role,
|
|
348
|
-
contentLength: conversation.messages[conversation.messages.length - 1]?.content
|
|
349
|
-
.length,
|
|
350
|
-
},
|
|
351
|
-
],
|
|
303
|
+
sessionId: conversation.sessionId,
|
|
304
|
+
userId: conversation.userId,
|
|
352
305
|
});
|
|
353
|
-
//
|
|
306
|
+
// Use per-request enableSummarization with higher priority than instance config
|
|
307
|
+
const shouldSummarize = options.enableSummarization !== undefined
|
|
308
|
+
? options.enableSummarization
|
|
309
|
+
: this.config.enableSummarization;
|
|
310
|
+
if (shouldSummarize) {
|
|
311
|
+
const normalizedUserId = options.userId || "randomUser";
|
|
312
|
+
const summarizationKey = `${options.sessionId}:${normalizedUserId}`;
|
|
313
|
+
// Only trigger summarization if not already in progress for this session
|
|
314
|
+
if (!this.summarizationInProgress.has(summarizationKey)) {
|
|
315
|
+
setImmediate(async () => {
|
|
316
|
+
try {
|
|
317
|
+
await this.checkAndSummarize(conversation, tokenThreshold, options.sessionId, options.userId);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
logger.error("Background summarization failed", {
|
|
321
|
+
sessionId: conversation.sessionId,
|
|
322
|
+
error: error instanceof Error ? error.message : String(error),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
logger.debug("[RedisConversationMemoryManager] Summarization already in progress, skipping", {
|
|
329
|
+
sessionId: options.sessionId,
|
|
330
|
+
userId: normalizedUserId,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
354
334
|
const serializedData = serializeConversation(conversation);
|
|
355
|
-
logger.debug("[RedisConversationMemoryManager] Saving conversation to Redis", {
|
|
356
|
-
redisKey,
|
|
357
|
-
messageCount: conversation.messages.length,
|
|
358
|
-
serializedDataLength: serializedData.length,
|
|
359
|
-
title: conversation.title,
|
|
360
|
-
});
|
|
361
|
-
logger.info("Storing conversation data to Redis", {
|
|
362
|
-
sessionId,
|
|
363
|
-
dataLength: serializedData.length,
|
|
364
|
-
messageCount: conversation.messages.length,
|
|
365
|
-
});
|
|
366
335
|
await this.redisClient.set(redisKey, serializedData);
|
|
367
|
-
// Set TTL if configured
|
|
368
336
|
if (this.redisConfig.ttl > 0) {
|
|
369
|
-
logger.debug("[RedisConversationMemoryManager] Setting Redis TTL", {
|
|
370
|
-
redisKey,
|
|
371
|
-
ttl: this.redisConfig.ttl,
|
|
372
|
-
});
|
|
373
337
|
await this.redisClient.expire(redisKey, this.redisConfig.ttl);
|
|
374
338
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
await this.addUserSession(userId, sessionId);
|
|
339
|
+
if (options.userId) {
|
|
340
|
+
await this.addUserSession(options.userId, options.sessionId);
|
|
378
341
|
}
|
|
379
342
|
logger.debug("[RedisConversationMemoryManager] Successfully stored conversation turn", {
|
|
380
|
-
sessionId,
|
|
343
|
+
sessionId: options.sessionId,
|
|
381
344
|
totalMessages: conversation.messages.length,
|
|
382
345
|
title: conversation.title,
|
|
383
346
|
});
|
|
384
347
|
}
|
|
385
348
|
catch (error) {
|
|
386
|
-
throw new ConversationMemoryError(`Failed to store conversation turn in Redis for session ${sessionId}`, "STORAGE_ERROR", {
|
|
349
|
+
throw new ConversationMemoryError(`Failed to store conversation turn in Redis for session ${options.sessionId}`, "STORAGE_ERROR", {
|
|
350
|
+
sessionId: options.sessionId,
|
|
351
|
+
error: error instanceof Error ? error.message : String(error),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Check if summarization is needed based on token count
|
|
357
|
+
*/
|
|
358
|
+
async checkAndSummarize(conversation, threshold, sessionId, userId) {
|
|
359
|
+
const normalizedUserId = userId || "randomUser";
|
|
360
|
+
const summarizationKey = `${sessionId}:${normalizedUserId}`;
|
|
361
|
+
// Acquire lock - if already in progress, skip
|
|
362
|
+
if (this.summarizationInProgress.has(summarizationKey)) {
|
|
363
|
+
logger.debug("[RedisConversationMemoryManager] Summarization already in progress, skipping", {
|
|
387
364
|
sessionId,
|
|
365
|
+
userId: normalizedUserId,
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
this.summarizationInProgress.add(summarizationKey);
|
|
370
|
+
try {
|
|
371
|
+
const session = {
|
|
372
|
+
sessionId: conversation.sessionId,
|
|
373
|
+
userId: conversation.userId,
|
|
374
|
+
messages: conversation.messages,
|
|
375
|
+
summarizedUpToMessageId: conversation.summarizedUpToMessageId,
|
|
376
|
+
summarizedMessage: conversation.summarizedMessage,
|
|
377
|
+
tokenThreshold: conversation.tokenThreshold,
|
|
378
|
+
lastTokenCount: conversation.lastTokenCount,
|
|
379
|
+
lastCountedAt: conversation.lastCountedAt,
|
|
380
|
+
createdAt: new Date(conversation.createdAt).getTime(),
|
|
381
|
+
lastActivity: new Date(conversation.updatedAt).getTime(),
|
|
382
|
+
};
|
|
383
|
+
const contextMessages = buildContextFromPointer(session);
|
|
384
|
+
const tokenCount = this.estimateTokens(contextMessages);
|
|
385
|
+
conversation.lastTokenCount = tokenCount;
|
|
386
|
+
conversation.lastCountedAt = Date.now();
|
|
387
|
+
if (tokenCount >= threshold) {
|
|
388
|
+
await this.summarizeSessionTokenBased(conversation, threshold, sessionId, userId);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
logger.error("Token counting or summarization failed", {
|
|
393
|
+
sessionId: conversation.sessionId,
|
|
388
394
|
error: error instanceof Error ? error.message : String(error),
|
|
389
395
|
});
|
|
390
396
|
}
|
|
397
|
+
finally {
|
|
398
|
+
// Release lock when done
|
|
399
|
+
this.summarizationInProgress.delete(summarizationKey);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Estimate total tokens for a list of messages
|
|
404
|
+
*/
|
|
405
|
+
estimateTokens(messages) {
|
|
406
|
+
return messages.reduce((total, msg) => {
|
|
407
|
+
return total + TokenUtils.estimateTokenCount(msg.content);
|
|
408
|
+
}, 0);
|
|
391
409
|
}
|
|
392
410
|
/**
|
|
393
|
-
*
|
|
411
|
+
* Token-based summarization (pointer-based, non-destructive)
|
|
394
412
|
*/
|
|
395
|
-
async
|
|
413
|
+
async summarizeSessionTokenBased(conversation, threshold, sessionId, userId) {
|
|
414
|
+
const startIndex = conversation.summarizedUpToMessageId
|
|
415
|
+
? conversation.messages.findIndex((m) => m.id === conversation.summarizedUpToMessageId) + 1
|
|
416
|
+
: 0;
|
|
417
|
+
const recentMessages = conversation.messages.slice(startIndex);
|
|
418
|
+
if (recentMessages.length === 0) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
// We only want to include user, assistant, and system messages in summarization
|
|
422
|
+
const filteredRecentMessages = recentMessages.filter((msg) => msg.role !== "tool_call" && msg.role !== "tool_result");
|
|
423
|
+
const targetRecentTokens = threshold * RECENT_MESSAGES_RATIO;
|
|
424
|
+
const splitIndex = await this.findSplitIndexByTokens(filteredRecentMessages, targetRecentTokens);
|
|
425
|
+
const messagesToSummarize = filteredRecentMessages.slice(0, splitIndex);
|
|
426
|
+
if (messagesToSummarize.length === 0) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const summary = await generateSummary(messagesToSummarize, this.config, "[RedisConversationMemoryManager]", conversation.summarizedMessage);
|
|
430
|
+
if (!summary) {
|
|
431
|
+
logger.warn(`[RedisConversationMemoryManager] Summary generation failed for session ${conversation.sessionId}`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const lastSummarized = messagesToSummarize[messagesToSummarize.length - 1];
|
|
435
|
+
conversation.summarizedUpToMessageId = lastSummarized.id;
|
|
436
|
+
conversation.summarizedMessage = summary;
|
|
437
|
+
if (this.redisClient) {
|
|
438
|
+
const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
|
|
439
|
+
const serializedData = serializeConversation(conversation);
|
|
440
|
+
await this.redisClient.set(redisKey, serializedData);
|
|
441
|
+
if (this.redisConfig.ttl > 0) {
|
|
442
|
+
await this.redisClient.expire(redisKey, this.redisConfig.ttl);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Find split index to keep recent messages within target token count
|
|
448
|
+
*/
|
|
449
|
+
async findSplitIndexByTokens(messages, targetRecentTokens) {
|
|
450
|
+
let recentTokens = 0;
|
|
451
|
+
let splitIndex = messages.length;
|
|
452
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
453
|
+
const msgTokens = TokenUtils.estimateTokenCount(messages[i].content);
|
|
454
|
+
if (recentTokens + msgTokens > targetRecentTokens) {
|
|
455
|
+
splitIndex = i + 1;
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
recentTokens += msgTokens;
|
|
459
|
+
}
|
|
460
|
+
// Ensure we're summarizing at least something
|
|
461
|
+
return Math.max(1, splitIndex);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Build context messages for AI prompt injection (TOKEN-BASED)
|
|
465
|
+
* Returns messages from pointer onwards (or all if no pointer)
|
|
466
|
+
* Filters out tool_call and tool_result messages when summarization is enabled
|
|
467
|
+
*/
|
|
468
|
+
async buildContextMessages(sessionId, userId, enableSummarization) {
|
|
396
469
|
logger.info("[RedisConversationMemoryManager] Building context messages", {
|
|
397
470
|
sessionId,
|
|
398
471
|
userId,
|
|
399
472
|
method: "buildContextMessages",
|
|
400
473
|
});
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
userId,
|
|
406
|
-
});
|
|
474
|
+
const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
|
|
475
|
+
const conversationData = await this.redisClient?.get(redisKey);
|
|
476
|
+
const conversation = deserializeConversation(conversationData || null);
|
|
477
|
+
if (!conversation) {
|
|
407
478
|
return [];
|
|
408
479
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
480
|
+
const session = {
|
|
481
|
+
sessionId: conversation.sessionId,
|
|
482
|
+
userId: conversation.userId,
|
|
483
|
+
messages: conversation.messages,
|
|
484
|
+
summarizedUpToMessageId: conversation.summarizedUpToMessageId,
|
|
485
|
+
summarizedMessage: conversation.summarizedMessage,
|
|
486
|
+
tokenThreshold: conversation.tokenThreshold,
|
|
487
|
+
lastTokenCount: conversation.lastTokenCount,
|
|
488
|
+
lastCountedAt: conversation.lastCountedAt,
|
|
489
|
+
createdAt: new Date(conversation.createdAt).getTime(),
|
|
490
|
+
lastActivity: new Date(conversation.updatedAt).getTime(),
|
|
491
|
+
};
|
|
492
|
+
const contextMessages = buildContextFromPointer(session);
|
|
493
|
+
const isSummarizationEnabled = enableSummarization !== undefined
|
|
494
|
+
? enableSummarization
|
|
495
|
+
: this.config.enableSummarization === true;
|
|
496
|
+
let finalMessages = contextMessages;
|
|
497
|
+
if (isSummarizationEnabled) {
|
|
498
|
+
finalMessages = contextMessages.filter((msg) => msg.role !== "tool_call" && msg.role !== "tool_result");
|
|
499
|
+
}
|
|
413
500
|
logger.info("[RedisConversationMemoryManager] Retrieved context messages", {
|
|
414
501
|
sessionId,
|
|
415
502
|
userId,
|
|
416
|
-
messageCount: messages.length,
|
|
417
|
-
messageRoles: messages.map((m) => m.role),
|
|
418
|
-
firstMessagePreview: messages[0]?.content?.substring(0, 50),
|
|
419
|
-
lastMessagePreview: messages[messages.length - 1]?.content?.substring(0, 50),
|
|
420
503
|
});
|
|
421
|
-
return
|
|
504
|
+
return finalMessages;
|
|
422
505
|
}
|
|
423
506
|
/**
|
|
424
507
|
* Get session metadata for a specific user session (optimized for listing)
|
|
@@ -649,10 +732,17 @@ User message: "${userMessage}`;
|
|
|
649
732
|
/**
|
|
650
733
|
* Create summary system message
|
|
651
734
|
*/
|
|
652
|
-
createSummarySystemMessage(content) {
|
|
735
|
+
createSummarySystemMessage(content, summarizesFrom, summarizesTo) {
|
|
653
736
|
return {
|
|
737
|
+
id: `summary-${randomUUID()}`,
|
|
654
738
|
role: "system",
|
|
655
739
|
content: `Summary of previous conversation turns:\n\n${content}`,
|
|
740
|
+
timestamp: new Date().toISOString(),
|
|
741
|
+
metadata: {
|
|
742
|
+
isSummary: true,
|
|
743
|
+
summarizesFrom,
|
|
744
|
+
summarizesTo,
|
|
745
|
+
},
|
|
656
746
|
};
|
|
657
747
|
}
|
|
658
748
|
/**
|
|
@@ -876,7 +966,7 @@ User message: "${userMessage}`;
|
|
|
876
966
|
// Store in mapping for tool results
|
|
877
967
|
toolCallMap.set(toolCallId, toolName);
|
|
878
968
|
const toolCallMessage = {
|
|
879
|
-
id:
|
|
969
|
+
id: randomUUID(),
|
|
880
970
|
timestamp: toolCall.timestamp?.toISOString() || this.generateTimestamp(),
|
|
881
971
|
role: "tool_call",
|
|
882
972
|
content: "", // Can be empty for tool calls
|
|
@@ -893,7 +983,7 @@ User message: "${userMessage}`;
|
|
|
893
983
|
const toolCallId = String(toolResult.toolCallId || toolResult.id || "unknown");
|
|
894
984
|
const toolName = toolCallMap.get(toolCallId) || "unknown";
|
|
895
985
|
const toolResultMessage = {
|
|
896
|
-
id:
|
|
986
|
+
id: randomUUID(),
|
|
897
987
|
timestamp: toolResult.timestamp?.toISOString() || this.generateTimestamp(),
|
|
898
988
|
role: "tool_result",
|
|
899
989
|
content: "", // Can be empty for tool results
|
|
@@ -60,4 +60,16 @@ export declare class ProviderImageAdapter {
|
|
|
60
60
|
* Get all vision-capable providers
|
|
61
61
|
*/
|
|
62
62
|
static getVisionProviders(): string[];
|
|
63
|
+
/**
|
|
64
|
+
* Count total "images" in a message (actual images + PDF pages)
|
|
65
|
+
* PDF pages count toward image limits for providers
|
|
66
|
+
*/
|
|
67
|
+
static countImagesInMessage(images: Array<Buffer | string>, pdfPages?: number | null): number;
|
|
68
|
+
/**
|
|
69
|
+
* Extract page count from PDF metadata array
|
|
70
|
+
* Returns total pages across all PDFs
|
|
71
|
+
*/
|
|
72
|
+
static countImagesInPages(pdfMetadataArray: Array<{
|
|
73
|
+
pageCount?: number | null;
|
|
74
|
+
}> | undefined): number;
|
|
63
75
|
}
|
|
@@ -416,13 +416,19 @@ export class ProviderImageAdapter {
|
|
|
416
416
|
adaptedPayload = this.formatForOpenAI(text, images);
|
|
417
417
|
break;
|
|
418
418
|
case "litellm":
|
|
419
|
-
|
|
419
|
+
// LiteLLM uses same format as OpenAI but validate with litellm provider name
|
|
420
|
+
this.validateImageCount(images.length, "litellm");
|
|
421
|
+
adaptedPayload = this.formatForOpenAI(text, images, true);
|
|
420
422
|
break;
|
|
421
423
|
case "mistral":
|
|
422
|
-
|
|
424
|
+
// Mistral uses same format as OpenAI but validate with mistral provider name
|
|
425
|
+
this.validateImageCount(images.length, "mistral");
|
|
426
|
+
adaptedPayload = this.formatForOpenAI(text, images, true);
|
|
423
427
|
break;
|
|
424
428
|
case "bedrock":
|
|
425
|
-
|
|
429
|
+
// Bedrock uses same format as Anthropic but validate with bedrock provider name
|
|
430
|
+
this.validateImageCount(images.length, "bedrock");
|
|
431
|
+
adaptedPayload = this.formatForAnthropic(text, images, true);
|
|
426
432
|
break;
|
|
427
433
|
default:
|
|
428
434
|
throw new Error(`Vision not supported for provider: ${provider}`);
|
|
@@ -666,5 +672,26 @@ export class ProviderImageAdapter {
|
|
|
666
672
|
static getVisionProviders() {
|
|
667
673
|
return Object.keys(VISION_CAPABILITIES);
|
|
668
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* Count total "images" in a message (actual images + PDF pages)
|
|
677
|
+
* PDF pages count toward image limits for providers
|
|
678
|
+
*/
|
|
679
|
+
static countImagesInMessage(images, pdfPages) {
|
|
680
|
+
const imageCount = images?.length || 0;
|
|
681
|
+
const pageCount = pdfPages ?? 0;
|
|
682
|
+
return imageCount + pageCount;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Extract page count from PDF metadata array
|
|
686
|
+
* Returns total pages across all PDFs
|
|
687
|
+
*/
|
|
688
|
+
static countImagesInPages(pdfMetadataArray) {
|
|
689
|
+
if (!pdfMetadataArray || pdfMetadataArray.length === 0) {
|
|
690
|
+
return 0;
|
|
691
|
+
}
|
|
692
|
+
return pdfMetadataArray.reduce((total, pdf) => {
|
|
693
|
+
return total + (pdf.pageCount ?? 0);
|
|
694
|
+
}, 0);
|
|
695
|
+
}
|
|
669
696
|
}
|
|
670
697
|
//# sourceMappingURL=providerImageAdapter.js.map
|
|
@@ -20,12 +20,28 @@ export declare const MESSAGES_PER_TURN = 2;
|
|
|
20
20
|
* Used to enhance system prompts when conversation history exists
|
|
21
21
|
*/
|
|
22
22
|
export declare const CONVERSATION_INSTRUCTIONS = "\n\nIMPORTANT: You are continuing an ongoing conversation. The previous messages in this conversation contain important context including:\n- Names, personal information, and preferences shared by the user\n- Projects, tasks, and topics discussed previously \n- Any decisions, agreements, or conclusions reached\n\nAlways reference and build upon this conversation history when relevant. If the user asks about information mentioned earlier in the conversation, refer to those previous messages to provide accurate, contextual responses.";
|
|
23
|
+
/**
|
|
24
|
+
* Percentage of model context window to use for conversation memory threshold
|
|
25
|
+
* Default: 80% of model's context window
|
|
26
|
+
*/
|
|
27
|
+
export declare const MEMORY_THRESHOLD_PERCENTAGE = 0.8;
|
|
28
|
+
/**
|
|
29
|
+
* Fallback token threshold if model context unknown
|
|
30
|
+
*/
|
|
31
|
+
export declare const DEFAULT_FALLBACK_THRESHOLD = 50000;
|
|
32
|
+
/**
|
|
33
|
+
* Ratio of threshold to keep as recent unsummarized messages
|
|
34
|
+
* When summarization triggers, this percentage of tokens from the end
|
|
35
|
+
* are preserved as detailed messages, while older content gets summarized.
|
|
36
|
+
*/
|
|
37
|
+
export declare const RECENT_MESSAGES_RATIO = 0.3;
|
|
23
38
|
/**
|
|
24
39
|
* Structured output instructions for JSON/structured output mode
|
|
25
40
|
* Used to ensure AI providers output only valid JSON without conversational filler
|
|
26
41
|
* This addresses the issue where models add text like "Excellent!" before JSON output
|
|
42
|
+
* and the case where tools are used but final output must still be pure JSON
|
|
27
43
|
*/
|
|
28
|
-
export declare const STRUCTURED_OUTPUT_INSTRUCTIONS = "\
|
|
44
|
+
export declare const STRUCTURED_OUTPUT_INSTRUCTIONS = "\nOutput ONLY valid JSON. No markdown, text, or decorations\u2014ever.\n\nFORBIDDEN: markdown code blocks, text before/after JSON, explanations, preambles, summaries, conversational text about tools.\n\nREQUIRED: response starts with { and ends with }, valid JSON only, no additional characters.\n\nIF YOU CALLED TOOLS: Incorporate data directly into the JSON structure. Do NOT explain what you did.\n\nWRONG: ```json\n{\"field\": \"value\"}\n```\nWRONG: Based on the data, here's the result: {\"field\": \"value\"}\nCORRECT: {\"field\": \"value\"}\n\nYour entire response = raw JSON object. Nothing else.";
|
|
29
45
|
/**
|
|
30
46
|
* Get default configuration values for conversation memory
|
|
31
47
|
* Reads environment variables when called (not at module load time)
|
|
@@ -26,20 +26,43 @@ IMPORTANT: You are continuing an ongoing conversation. The previous messages in
|
|
|
26
26
|
- Any decisions, agreements, or conclusions reached
|
|
27
27
|
|
|
28
28
|
Always reference and build upon this conversation history when relevant. If the user asks about information mentioned earlier in the conversation, refer to those previous messages to provide accurate, contextual responses.`;
|
|
29
|
+
/**
|
|
30
|
+
* Percentage of model context window to use for conversation memory threshold
|
|
31
|
+
* Default: 80% of model's context window
|
|
32
|
+
*/
|
|
33
|
+
export const MEMORY_THRESHOLD_PERCENTAGE = 0.8;
|
|
34
|
+
/**
|
|
35
|
+
* Fallback token threshold if model context unknown
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_FALLBACK_THRESHOLD = 50000;
|
|
38
|
+
/**
|
|
39
|
+
* Ratio of threshold to keep as recent unsummarized messages
|
|
40
|
+
* When summarization triggers, this percentage of tokens from the end
|
|
41
|
+
* are preserved as detailed messages, while older content gets summarized.
|
|
42
|
+
*/
|
|
43
|
+
export const RECENT_MESSAGES_RATIO = 0.3;
|
|
29
44
|
/**
|
|
30
45
|
* Structured output instructions for JSON/structured output mode
|
|
31
46
|
* Used to ensure AI providers output only valid JSON without conversational filler
|
|
32
47
|
* This addresses the issue where models add text like "Excellent!" before JSON output
|
|
48
|
+
* and the case where tools are used but final output must still be pure JSON
|
|
33
49
|
*/
|
|
34
50
|
export const STRUCTURED_OUTPUT_INSTRUCTIONS = `
|
|
51
|
+
Output ONLY valid JSON. No markdown, text, or decorations—ever.
|
|
52
|
+
|
|
53
|
+
FORBIDDEN: markdown code blocks, text before/after JSON, explanations, preambles, summaries, conversational text about tools.
|
|
35
54
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
REQUIRED: response starts with { and ends with }, valid JSON only, no additional characters.
|
|
56
|
+
|
|
57
|
+
IF YOU CALLED TOOLS: Incorporate data directly into the JSON structure. Do NOT explain what you did.
|
|
58
|
+
|
|
59
|
+
WRONG: \`\`\`json
|
|
60
|
+
{"field": "value"}
|
|
61
|
+
\`\`\`
|
|
62
|
+
WRONG: Based on the data, here's the result: {"field": "value"}
|
|
63
|
+
CORRECT: {"field": "value"}
|
|
64
|
+
|
|
65
|
+
Your entire response = raw JSON object. Nothing else.`;
|
|
43
66
|
/**
|
|
44
67
|
* Get default configuration values for conversation memory
|
|
45
68
|
* Reads environment variables when called (not at module load time)
|
|
@@ -48,13 +71,17 @@ export function getConversationMemoryDefaults() {
|
|
|
48
71
|
return {
|
|
49
72
|
enabled: process.env.NEUROLINK_MEMORY_ENABLED === "true",
|
|
50
73
|
maxSessions: Number(process.env.NEUROLINK_MEMORY_MAX_SESSIONS) || DEFAULT_MAX_SESSIONS,
|
|
74
|
+
enableSummarization: process.env.NEUROLINK_SUMMARIZATION_ENABLED !== "false",
|
|
75
|
+
tokenThreshold: process.env.NEUROLINK_TOKEN_THRESHOLD
|
|
76
|
+
? Number(process.env.NEUROLINK_TOKEN_THRESHOLD)
|
|
77
|
+
: undefined,
|
|
78
|
+
summarizationProvider: process.env.NEUROLINK_SUMMARIZATION_PROVIDER || "vertex",
|
|
79
|
+
summarizationModel: process.env.NEUROLINK_SUMMARIZATION_MODEL || "gemini-2.5-flash",
|
|
80
|
+
// Deprecated (for backward compatibility)
|
|
51
81
|
maxTurnsPerSession: Number(process.env.NEUROLINK_MEMORY_MAX_TURNS_PER_SESSION) ||
|
|
52
82
|
DEFAULT_MAX_TURNS_PER_SESSION,
|
|
53
|
-
enableSummarization: process.env.NEUROLINK_SUMMARIZATION_ENABLED === "true",
|
|
54
83
|
summarizationThresholdTurns: Number(process.env.NEUROLINK_SUMMARIZATION_THRESHOLD_TURNS) || 20,
|
|
55
84
|
summarizationTargetTurns: Number(process.env.NEUROLINK_SUMMARIZATION_TARGET_TURNS) || 10,
|
|
56
|
-
summarizationProvider: process.env.NEUROLINK_SUMMARIZATION_PROVIDER || "vertex",
|
|
57
|
-
summarizationModel: process.env.NEUROLINK_SUMMARIZATION_MODEL || "gemini-2.5-flash",
|
|
58
85
|
};
|
|
59
86
|
}
|
|
60
87
|
//# sourceMappingURL=conversationMemory.js.map
|