@juspay/neurolink 8.19.1 → 8.20.1
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/cli/loop/optionsSchema.js +4 -0
- package/dist/config/conversationMemory.d.ts +15 -0
- package/dist/config/conversationMemory.js +22 -3
- 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/redisConversationMemoryManager.d.ts +28 -13
- package/dist/core/redisConversationMemoryManager.js +211 -121
- package/dist/lib/config/conversationMemory.d.ts +15 -0
- package/dist/lib/config/conversationMemory.js +22 -3
- 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/redisConversationMemoryManager.d.ts +28 -13
- package/dist/lib/core/redisConversationMemoryManager.js +211 -121
- 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/imageProcessor.d.ts +1 -0
- package/dist/lib/utils/imageProcessor.js +29 -1
- package/dist/lib/utils/redis.js +0 -5
- 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/imageProcessor.d.ts +1 -0
- package/dist/utils/imageProcessor.js +29 -1
- package/dist/utils/redis.js +0 -5
- package/package.json +1 -1
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
* Handles in-memory conversation storage, session management, and context injection
|
|
4
4
|
*/
|
|
5
5
|
import { ConversationMemoryError } from "../types/conversation.js";
|
|
6
|
-
import {
|
|
6
|
+
import { DEFAULT_MAX_SESSIONS, MEMORY_THRESHOLD_PERCENTAGE, RECENT_MESSAGES_RATIO, MESSAGES_PER_TURN, } from "../config/conversationMemory.js";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
|
-
import {
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
import { TokenUtils } from "../constants/tokens.js";
|
|
10
|
+
import { buildContextFromPointer, getEffectiveTokenThreshold, generateSummary, } from "../utils/conversationMemory.js";
|
|
9
11
|
export class ConversationMemoryManager {
|
|
10
12
|
sessions = new Map();
|
|
11
13
|
config;
|
|
12
14
|
isInitialized = false;
|
|
15
|
+
/**
|
|
16
|
+
* Track sessions currently being summarized to prevent race conditions
|
|
17
|
+
*/
|
|
18
|
+
summarizationInProgress = new Set();
|
|
13
19
|
constructor(config) {
|
|
14
20
|
this.config = config;
|
|
15
21
|
}
|
|
@@ -34,121 +40,209 @@ export class ConversationMemoryManager {
|
|
|
34
40
|
}
|
|
35
41
|
/**
|
|
36
42
|
* Store a conversation turn for a session
|
|
37
|
-
*
|
|
43
|
+
* TOKEN-BASED: Validates message size and triggers summarization based on tokens
|
|
38
44
|
*/
|
|
39
|
-
async storeConversationTurn(
|
|
45
|
+
async storeConversationTurn(options) {
|
|
40
46
|
await this.ensureInitialized();
|
|
41
47
|
try {
|
|
42
48
|
// Get or create session
|
|
43
|
-
let session = this.sessions.get(sessionId);
|
|
49
|
+
let session = this.sessions.get(options.sessionId);
|
|
44
50
|
if (!session) {
|
|
45
|
-
session = this.createNewSession(sessionId, userId);
|
|
46
|
-
this.sessions.set(sessionId, session);
|
|
51
|
+
session = this.createNewSession(options.sessionId, options.userId);
|
|
52
|
+
this.sessions.set(options.sessionId, session);
|
|
47
53
|
}
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
const tokenThreshold = options.providerDetails
|
|
55
|
+
? getEffectiveTokenThreshold(options.providerDetails.provider, options.providerDetails.model, this.config.tokenThreshold, session.tokenThreshold)
|
|
56
|
+
: this.config.tokenThreshold || 50000;
|
|
57
|
+
const userMsg = await this.validateAndPrepareMessage(options.userMessage, "user", tokenThreshold);
|
|
58
|
+
const assistantMsg = await this.validateAndPrepareMessage(options.aiResponse, "assistant", tokenThreshold);
|
|
59
|
+
session.messages.push(userMsg, assistantMsg);
|
|
50
60
|
session.lastActivity = Date.now();
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
const shouldSummarize = options.enableSummarization !== undefined
|
|
62
|
+
? options.enableSummarization
|
|
63
|
+
: this.config.enableSummarization;
|
|
64
|
+
if (shouldSummarize) {
|
|
65
|
+
// Only trigger summarization if not already in progress for this session
|
|
66
|
+
if (!this.summarizationInProgress.has(options.sessionId)) {
|
|
67
|
+
setImmediate(async () => {
|
|
68
|
+
try {
|
|
69
|
+
await this.checkAndSummarize(session, tokenThreshold);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
logger.error("Background summarization failed", {
|
|
73
|
+
sessionId: session.sessionId,
|
|
74
|
+
error: error instanceof Error ? error.message : String(error),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
56
78
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (session.messages.length > maxMessages) {
|
|
62
|
-
session.messages = session.messages.slice(-maxMessages);
|
|
79
|
+
else {
|
|
80
|
+
logger.debug("[ConversationMemoryManager] Summarization already in progress, skipping", {
|
|
81
|
+
sessionId: options.sessionId,
|
|
82
|
+
});
|
|
63
83
|
}
|
|
64
84
|
}
|
|
65
85
|
this.enforceSessionLimit();
|
|
66
86
|
}
|
|
67
87
|
catch (error) {
|
|
68
|
-
throw new ConversationMemoryError(`Failed to store conversation turn for session ${sessionId}`, "STORAGE_ERROR", {
|
|
69
|
-
sessionId,
|
|
88
|
+
throw new ConversationMemoryError(`Failed to store conversation turn for session ${options.sessionId}`, "STORAGE_ERROR", {
|
|
89
|
+
sessionId: options.sessionId,
|
|
70
90
|
error: error instanceof Error ? error.message : String(error),
|
|
71
91
|
});
|
|
72
92
|
}
|
|
73
93
|
}
|
|
74
94
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
95
|
+
* Validate and prepare a message before adding to session
|
|
96
|
+
* Truncates if message exceeds token limit
|
|
97
|
+
*/
|
|
98
|
+
async validateAndPrepareMessage(content, role, threshold) {
|
|
99
|
+
const id = randomUUID();
|
|
100
|
+
const tokenCount = TokenUtils.estimateTokenCount(content);
|
|
101
|
+
const maxMessageSize = Math.floor(threshold * MEMORY_THRESHOLD_PERCENTAGE);
|
|
102
|
+
if (tokenCount > maxMessageSize) {
|
|
103
|
+
const truncated = TokenUtils.truncateToTokenLimit(content, maxMessageSize);
|
|
104
|
+
logger.warn("Message truncated due to token limit", {
|
|
105
|
+
id,
|
|
106
|
+
role,
|
|
107
|
+
originalTokens: tokenCount,
|
|
108
|
+
threshold,
|
|
109
|
+
truncatedTo: maxMessageSize,
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
id,
|
|
113
|
+
role,
|
|
114
|
+
content: truncated,
|
|
115
|
+
timestamp: new Date().toISOString(),
|
|
116
|
+
metadata: {
|
|
117
|
+
truncated: true,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
id,
|
|
123
|
+
role,
|
|
124
|
+
content,
|
|
125
|
+
timestamp: new Date().toISOString(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Check if summarization is needed based on token count
|
|
130
|
+
*/
|
|
131
|
+
async checkAndSummarize(session, threshold) {
|
|
132
|
+
// Acquire lock - if already in progress, skip
|
|
133
|
+
if (this.summarizationInProgress.has(session.sessionId)) {
|
|
134
|
+
logger.debug("[ConversationMemoryManager] Summarization already in progress, skipping", {
|
|
135
|
+
sessionId: session.sessionId,
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.summarizationInProgress.add(session.sessionId);
|
|
140
|
+
try {
|
|
141
|
+
const contextMessages = buildContextFromPointer(session);
|
|
142
|
+
const tokenCount = this.estimateTokens(contextMessages);
|
|
143
|
+
session.lastTokenCount = tokenCount;
|
|
144
|
+
session.lastCountedAt = Date.now();
|
|
145
|
+
logger.debug("Token count check", {
|
|
146
|
+
sessionId: session.sessionId,
|
|
147
|
+
tokenCount,
|
|
148
|
+
threshold,
|
|
149
|
+
needsSummarization: tokenCount >= threshold,
|
|
150
|
+
});
|
|
151
|
+
if (tokenCount >= threshold) {
|
|
152
|
+
await this.summarizeSessionTokenBased(session, threshold);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
logger.error("Token counting or summarization failed", {
|
|
157
|
+
sessionId: session.sessionId,
|
|
158
|
+
error: error instanceof Error ? error.message : String(error),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
// Release lock when done
|
|
163
|
+
this.summarizationInProgress.delete(session.sessionId);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Estimate total tokens for a list of messages
|
|
168
|
+
*/
|
|
169
|
+
estimateTokens(messages) {
|
|
170
|
+
return messages.reduce((total, msg) => {
|
|
171
|
+
return total + TokenUtils.estimateTokenCount(msg.content);
|
|
172
|
+
}, 0);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Build context messages for AI prompt injection (TOKEN-BASED)
|
|
176
|
+
* Returns messages from pointer onwards (or all if no pointer)
|
|
77
177
|
* Now consistently async to match Redis implementation
|
|
78
178
|
*/
|
|
79
179
|
async buildContextMessages(sessionId) {
|
|
80
180
|
const session = this.sessions.get(sessionId);
|
|
81
|
-
return session ? session
|
|
181
|
+
return session ? buildContextFromPointer(session) : [];
|
|
82
182
|
}
|
|
83
183
|
getSession(sessionId) {
|
|
84
184
|
return this.sessions.get(sessionId);
|
|
85
185
|
}
|
|
86
|
-
createSummarySystemMessage(content) {
|
|
186
|
+
createSummarySystemMessage(content, summarizesFrom, summarizesTo) {
|
|
87
187
|
return {
|
|
188
|
+
id: `summary-${randomUUID()}`,
|
|
88
189
|
role: "system",
|
|
89
190
|
content: `Summary of previous conversation turns:\n\n${content}`,
|
|
191
|
+
timestamp: new Date().toISOString(),
|
|
192
|
+
metadata: {
|
|
193
|
+
isSummary: true,
|
|
194
|
+
summarizesFrom,
|
|
195
|
+
summarizesTo,
|
|
196
|
+
},
|
|
90
197
|
};
|
|
91
198
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Token-based summarization (pointer-based, non-destructive)
|
|
201
|
+
*/
|
|
202
|
+
async summarizeSessionTokenBased(session, threshold) {
|
|
203
|
+
const startIndex = session.summarizedUpToMessageId
|
|
204
|
+
? session.messages.findIndex((m) => m.id === session.summarizedUpToMessageId) + 1
|
|
205
|
+
: 0;
|
|
206
|
+
const recentMessages = session.messages.slice(startIndex);
|
|
207
|
+
if (recentMessages.length === 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const targetRecentTokens = threshold * RECENT_MESSAGES_RATIO;
|
|
211
|
+
const splitIndex = await this.findSplitIndexByTokens(recentMessages, targetRecentTokens);
|
|
212
|
+
const messagesToSummarize = recentMessages.slice(0, splitIndex);
|
|
98
213
|
if (messagesToSummarize.length === 0) {
|
|
99
214
|
return;
|
|
100
215
|
}
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
216
|
+
const summary = await generateSummary(messagesToSummarize, this.config, "[ConversationMemory]", session.summarizedMessage);
|
|
217
|
+
if (!summary) {
|
|
218
|
+
logger.warn(`[ConversationMemory] Summary generation failed for session ${session.sessionId}`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const lastSummarized = messagesToSummarize[messagesToSummarize.length - 1];
|
|
222
|
+
session.summarizedUpToMessageId = lastSummarized.id;
|
|
223
|
+
session.summarizedMessage = summary; // Store summary separately
|
|
224
|
+
logger.info(`[ConversationMemory] Summarization complete for session ${session.sessionId}`, {
|
|
225
|
+
summarizedCount: messagesToSummarize.length,
|
|
226
|
+
totalMessages: session.messages.length,
|
|
227
|
+
pointer: session.summarizedUpToMessageId,
|
|
104
228
|
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const summaryResult = await summarizer.generate({
|
|
118
|
-
input: { text: summarizationPrompt },
|
|
119
|
-
provider: mappedProvider,
|
|
120
|
-
model: this.config.summarizationModel,
|
|
121
|
-
disableTools: true,
|
|
122
|
-
});
|
|
123
|
-
if (summaryResult.content) {
|
|
124
|
-
session.messages = [
|
|
125
|
-
this.createSummarySystemMessage(summaryResult.content),
|
|
126
|
-
...recentMessages,
|
|
127
|
-
];
|
|
128
|
-
logger.info(`[ConversationMemory] Summarization complete for session ${session.sessionId}.`);
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
logger.warn(`[ConversationMemory] Summarization failed for session ${session.sessionId}. History not modified.`);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Find split index to keep recent messages within target token count
|
|
232
|
+
*/
|
|
233
|
+
async findSplitIndexByTokens(messages, targetRecentTokens) {
|
|
234
|
+
let recentTokens = 0;
|
|
235
|
+
let splitIndex = messages.length;
|
|
236
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
237
|
+
const msgTokens = TokenUtils.estimateTokenCount(messages[i].content);
|
|
238
|
+
if (recentTokens + msgTokens > targetRecentTokens) {
|
|
239
|
+
splitIndex = i + 1;
|
|
240
|
+
break;
|
|
132
241
|
}
|
|
242
|
+
recentTokens += msgTokens;
|
|
133
243
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
_createSummarizationPrompt(history) {
|
|
139
|
-
const formattedHistory = history
|
|
140
|
-
.map((msg) => `${msg.role}: ${msg.content}`)
|
|
141
|
-
.join("\n\n");
|
|
142
|
-
return `
|
|
143
|
-
You are a context summarization AI. Your task is to condense the following conversation history for another AI assistant.
|
|
144
|
-
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.
|
|
145
|
-
Ensure the summary flows logically and is ready to be used as context for the next turn in the conversation.
|
|
146
|
-
|
|
147
|
-
Conversation History to Summarize:
|
|
148
|
-
---
|
|
149
|
-
${formattedHistory}
|
|
150
|
-
---
|
|
151
|
-
`.trim();
|
|
244
|
+
// To ensure at least one message is summarized
|
|
245
|
+
return Math.max(1, splitIndex);
|
|
152
246
|
}
|
|
153
247
|
async ensureInitialized() {
|
|
154
248
|
if (!this.isInitialized) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Redis Conversation Memory Manager for NeuroLink
|
|
3
3
|
* Redis-based implementation of conversation storage with same interface as ConversationMemoryManager
|
|
4
4
|
*/
|
|
5
|
-
import type { ConversationMemoryConfig, ConversationMemoryStats, ChatMessage, RedisStorageConfig, SessionMetadata, RedisConversationObject } from "../types/conversation.js";
|
|
5
|
+
import type { ConversationMemoryConfig, ConversationMemoryStats, ChatMessage, RedisStorageConfig, SessionMetadata, RedisConversationObject, StoreConversationTurnOptions } from "../types/conversation.js";
|
|
6
6
|
/**
|
|
7
7
|
* Redis-based implementation of the ConversationMemoryManager
|
|
8
8
|
* Uses the same interface but stores data in Redis
|
|
@@ -22,6 +22,11 @@ export declare class RedisConversationMemoryManager {
|
|
|
22
22
|
* Key format: "${sessionId}:${userId}"
|
|
23
23
|
*/
|
|
24
24
|
private titleGenerationInProgress;
|
|
25
|
+
/**
|
|
26
|
+
* Track sessions currently being summarized to prevent race conditions
|
|
27
|
+
* Key format: "${sessionId}:${userId}"
|
|
28
|
+
*/
|
|
29
|
+
private summarizationInProgress;
|
|
25
30
|
constructor(config: ConversationMemoryConfig, redisConfig?: RedisStorageConfig);
|
|
26
31
|
/**
|
|
27
32
|
* Initialize the memory manager with Redis connection
|
|
@@ -39,18 +44,10 @@ export declare class RedisConversationMemoryManager {
|
|
|
39
44
|
* Remove a session from user's session set (private method)
|
|
40
45
|
*/
|
|
41
46
|
private removeUserSession;
|
|
42
|
-
/**
|
|
43
|
-
* Generate next message ID for a conversation
|
|
44
|
-
*/
|
|
45
|
-
private generateMessageId;
|
|
46
47
|
/**
|
|
47
48
|
* Generate current timestamp in ISO format
|
|
48
49
|
*/
|
|
49
50
|
private generateTimestamp;
|
|
50
|
-
/**
|
|
51
|
-
* Generate a unique conversation ID using UUID v4
|
|
52
|
-
*/
|
|
53
|
-
private generateUniqueId;
|
|
54
51
|
/**
|
|
55
52
|
* Store tool execution data for a session (temporarily to avoid race conditions)
|
|
56
53
|
*/
|
|
@@ -68,11 +65,29 @@ export declare class RedisConversationMemoryManager {
|
|
|
68
65
|
/**
|
|
69
66
|
* Store a conversation turn for a session
|
|
70
67
|
*/
|
|
71
|
-
storeConversationTurn(
|
|
68
|
+
storeConversationTurn(options: StoreConversationTurnOptions): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Check if summarization is needed based on token count
|
|
71
|
+
*/
|
|
72
|
+
private checkAndSummarize;
|
|
73
|
+
/**
|
|
74
|
+
* Estimate total tokens for a list of messages
|
|
75
|
+
*/
|
|
76
|
+
private estimateTokens;
|
|
77
|
+
/**
|
|
78
|
+
* Token-based summarization (pointer-based, non-destructive)
|
|
79
|
+
*/
|
|
80
|
+
private summarizeSessionTokenBased;
|
|
81
|
+
/**
|
|
82
|
+
* Find split index to keep recent messages within target token count
|
|
83
|
+
*/
|
|
84
|
+
private findSplitIndexByTokens;
|
|
72
85
|
/**
|
|
73
|
-
* Build context messages for AI prompt injection
|
|
86
|
+
* Build context messages for AI prompt injection (TOKEN-BASED)
|
|
87
|
+
* Returns messages from pointer onwards (or all if no pointer)
|
|
88
|
+
* Filters out tool_call and tool_result messages when summarization is enabled
|
|
74
89
|
*/
|
|
75
|
-
buildContextMessages(sessionId: string, userId?: string): Promise<ChatMessage[]>;
|
|
90
|
+
buildContextMessages(sessionId: string, userId?: string, enableSummarization?: boolean): Promise<ChatMessage[]>;
|
|
76
91
|
/**
|
|
77
92
|
* Get session metadata for a specific user session (optimized for listing)
|
|
78
93
|
* Fetches only essential metadata without heavy message arrays
|
|
@@ -110,7 +125,7 @@ export declare class RedisConversationMemoryManager {
|
|
|
110
125
|
/**
|
|
111
126
|
* Create summary system message
|
|
112
127
|
*/
|
|
113
|
-
createSummarySystemMessage(content: string): ChatMessage;
|
|
128
|
+
createSummarySystemMessage(content: string, summarizesFrom?: string, summarizesTo?: string): ChatMessage;
|
|
114
129
|
/**
|
|
115
130
|
* Close Redis connection
|
|
116
131
|
*/
|