@juspay/neurolink 8.19.1 → 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 +6 -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/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/redis.js +0 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [8.20.0](https://github.com/juspay/neurolink/compare/v8.19.1...v8.20.0) (2025-12-22)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- **(memory):** Implement token based summarization ([ffdc902](https://github.com/juspay/neurolink/commit/ffdc902f534c97a5aff38d7de419021fcabcd791))
|
|
6
|
+
|
|
1
7
|
## [8.19.1](https://github.com/juspay/neurolink/compare/v8.19.0...v8.19.1) (2025-12-20)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
|
@@ -61,5 +61,9 @@ export const textGenerationOptionsSchema = {
|
|
|
61
61
|
type: "string",
|
|
62
62
|
description: "Context about tools/MCPs used in the interaction.",
|
|
63
63
|
},
|
|
64
|
+
enableSummarization: {
|
|
65
|
+
type: "boolean",
|
|
66
|
+
description: "Enable or disable automatic conversation summarization for this request.",
|
|
67
|
+
},
|
|
64
68
|
};
|
|
65
69
|
//# sourceMappingURL=optionsSchema.js.map
|
|
@@ -20,6 +20,21 @@ 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,6 +26,21 @@ 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
|
|
@@ -56,12 +71,16 @@ export function getConversationMemoryDefaults() {
|
|
|
56
71
|
return {
|
|
57
72
|
enabled: process.env.NEUROLINK_MEMORY_ENABLED === "true",
|
|
58
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)
|
|
59
81
|
maxTurnsPerSession: Number(process.env.NEUROLINK_MEMORY_MAX_TURNS_PER_SESSION) ||
|
|
60
82
|
DEFAULT_MAX_TURNS_PER_SESSION,
|
|
61
|
-
enableSummarization: process.env.NEUROLINK_SUMMARIZATION_ENABLED === "true",
|
|
62
83
|
summarizationThresholdTurns: Number(process.env.NEUROLINK_SUMMARIZATION_THRESHOLD_TURNS) || 20,
|
|
63
84
|
summarizationTargetTurns: Number(process.env.NEUROLINK_SUMMARIZATION_TARGET_TURNS) || 10,
|
|
64
|
-
summarizationProvider: process.env.NEUROLINK_SUMMARIZATION_PROVIDER || "vertex",
|
|
65
|
-
summarizationModel: process.env.NEUROLINK_SUMMARIZATION_MODEL || "gemini-2.5-flash",
|
|
66
85
|
};
|
|
67
86
|
}
|
|
@@ -14,10 +14,7 @@ export function createConversationMemoryManager(config, storageType = "memory",
|
|
|
14
14
|
config: {
|
|
15
15
|
enabled: config.enabled,
|
|
16
16
|
maxSessions: config.maxSessions,
|
|
17
|
-
maxTurnsPerSession: config.maxTurnsPerSession,
|
|
18
17
|
enableSummarization: config.enableSummarization,
|
|
19
|
-
summarizationThresholdTurns: config.summarizationThresholdTurns,
|
|
20
|
-
summarizationTargetTurns: config.summarizationTargetTurns,
|
|
21
18
|
summarizationProvider: config.summarizationProvider,
|
|
22
19
|
summarizationModel: config.summarizationModel,
|
|
23
20
|
},
|
|
@@ -62,15 +62,7 @@ export async function initializeConversationMemory(config) {
|
|
|
62
62
|
"RedisConversationMemoryManager",
|
|
63
63
|
hasConfig: !!redisMemoryManager?.config,
|
|
64
64
|
});
|
|
65
|
-
logger.info("[conversationMemoryInitializer] Redis conversation memory manager created successfully"
|
|
66
|
-
configSource,
|
|
67
|
-
host: redisConfig.host || "localhost",
|
|
68
|
-
port: redisConfig.port || 6379,
|
|
69
|
-
keyPrefix: redisConfig.keyPrefix || "neurolink:conversation:",
|
|
70
|
-
maxSessions: memoryConfig.maxSessions,
|
|
71
|
-
maxTurnsPerSession: memoryConfig.maxTurnsPerSession,
|
|
72
|
-
managerType: redisMemoryManager?.constructor?.name,
|
|
73
|
-
});
|
|
65
|
+
logger.info("[conversationMemoryInitializer] Redis conversation memory manager created successfully");
|
|
74
66
|
// Perform basic validation
|
|
75
67
|
if (redisMemoryManager?.constructor?.name !==
|
|
76
68
|
"RedisConversationMemoryManager") {
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* Conversation Memory Manager for NeuroLink
|
|
3
3
|
* Handles in-memory conversation storage, session management, and context injection
|
|
4
4
|
*/
|
|
5
|
-
import type { ConversationMemoryConfig, SessionMemory, ConversationMemoryStats, ChatMessage } from "../types/conversation.js";
|
|
5
|
+
import type { ConversationMemoryConfig, SessionMemory, ConversationMemoryStats, ChatMessage, StoreConversationTurnOptions } from "../types/conversation.js";
|
|
6
6
|
export declare class ConversationMemoryManager {
|
|
7
7
|
private sessions;
|
|
8
8
|
config: ConversationMemoryConfig;
|
|
9
9
|
private isInitialized;
|
|
10
|
+
/**
|
|
11
|
+
* Track sessions currently being summarized to prevent race conditions
|
|
12
|
+
*/
|
|
13
|
+
private summarizationInProgress;
|
|
10
14
|
constructor(config: ConversationMemoryConfig);
|
|
11
15
|
/**
|
|
12
16
|
* Initialize the memory manager
|
|
@@ -14,19 +18,38 @@ export declare class ConversationMemoryManager {
|
|
|
14
18
|
initialize(): Promise<void>;
|
|
15
19
|
/**
|
|
16
20
|
* Store a conversation turn for a session
|
|
17
|
-
*
|
|
21
|
+
* TOKEN-BASED: Validates message size and triggers summarization based on tokens
|
|
22
|
+
*/
|
|
23
|
+
storeConversationTurn(options: StoreConversationTurnOptions): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Validate and prepare a message before adding to session
|
|
26
|
+
* Truncates if message exceeds token limit
|
|
27
|
+
*/
|
|
28
|
+
private validateAndPrepareMessage;
|
|
29
|
+
/**
|
|
30
|
+
* Check if summarization is needed based on token count
|
|
18
31
|
*/
|
|
19
|
-
|
|
32
|
+
private checkAndSummarize;
|
|
20
33
|
/**
|
|
21
|
-
*
|
|
22
|
-
|
|
34
|
+
* Estimate total tokens for a list of messages
|
|
35
|
+
*/
|
|
36
|
+
private estimateTokens;
|
|
37
|
+
/**
|
|
38
|
+
* Build context messages for AI prompt injection (TOKEN-BASED)
|
|
39
|
+
* Returns messages from pointer onwards (or all if no pointer)
|
|
23
40
|
* Now consistently async to match Redis implementation
|
|
24
41
|
*/
|
|
25
42
|
buildContextMessages(sessionId: string): Promise<ChatMessage[]>;
|
|
26
43
|
getSession(sessionId: string): SessionMemory | undefined;
|
|
27
|
-
createSummarySystemMessage(content: string): ChatMessage;
|
|
28
|
-
|
|
29
|
-
|
|
44
|
+
createSummarySystemMessage(content: string, summarizesFrom?: string, summarizesTo?: string): ChatMessage;
|
|
45
|
+
/**
|
|
46
|
+
* Token-based summarization (pointer-based, non-destructive)
|
|
47
|
+
*/
|
|
48
|
+
private summarizeSessionTokenBased;
|
|
49
|
+
/**
|
|
50
|
+
* Find split index to keep recent messages within target token count
|
|
51
|
+
*/
|
|
52
|
+
private findSplitIndexByTokens;
|
|
30
53
|
private ensureInitialized;
|
|
31
54
|
private createNewSession;
|
|
32
55
|
private enforceSessionLimit;
|
|
@@ -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
|
*/
|