expo-ai-kit 0.3.6 → 0.4.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/README.md +68 -1106
- package/build/ExpoAiKitModule.d.ts +1 -1
- package/build/ExpoAiKitModule.d.ts.map +1 -1
- package/build/ExpoAiKitModule.js.map +1 -1
- package/build/index.d.ts +1 -436
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -737
- package/build/index.js.map +1 -1
- package/build/types.d.ts +3 -244
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/package.json +1 -1
- package/src/ExpoAiKitModule.ts +2 -2
- package/src/index.ts +1 -943
- package/src/types.ts +3 -284
- package/build/hooks.d.ts +0 -147
- package/build/hooks.d.ts.map +0 -1
- package/build/hooks.js +0 -426
- package/build/hooks.js.map +0 -1
- package/build/memory.d.ts +0 -235
- package/build/memory.d.ts.map +0 -1
- package/build/memory.js +0 -353
- package/build/memory.js.map +0 -1
- package/src/__tests__/index.test.js +0 -630
- package/src/hooks.ts +0 -502
- package/src/memory.ts +0 -394
package/build/memory.d.ts
DELETED
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chat Memory Management for On-Device AI Models
|
|
3
|
-
*
|
|
4
|
-
* WHY CLIENT-MANAGED MEMORY IS REQUIRED:
|
|
5
|
-
* --------------------------------------
|
|
6
|
-
* On-device AI models (Apple Foundation Models, local LLMs) are stateless.
|
|
7
|
-
* Unlike cloud-based APIs that may maintain server-side conversation state,
|
|
8
|
-
* on-device models have no session persistence or built-in memory.
|
|
9
|
-
*
|
|
10
|
-
* Each generation call accepts a single prompt string and returns a response
|
|
11
|
-
* with no knowledge of previous interactions. To enable multi-turn conversations,
|
|
12
|
-
* the client must:
|
|
13
|
-
*
|
|
14
|
-
* 1. Store all messages locally (this module handles that)
|
|
15
|
-
* 2. Build a complete prompt containing the full conversation history
|
|
16
|
-
* 3. Send the entire context on every generation request
|
|
17
|
-
* 4. Manage memory limits to avoid exceeding model context windows
|
|
18
|
-
*
|
|
19
|
-
* This approach is framework-agnostic and works with React, React Native,
|
|
20
|
-
* Expo, or any JavaScript/TypeScript environment.
|
|
21
|
-
*/
|
|
22
|
-
import { LLMMessage, ChatMemoryOptions, ChatMemorySnapshot } from './types';
|
|
23
|
-
/**
|
|
24
|
-
* Build a single prompt string from an array of messages.
|
|
25
|
-
*
|
|
26
|
-
* Uses a simple, deterministic format optimized for on-device models:
|
|
27
|
-
*
|
|
28
|
-
* ```
|
|
29
|
-
* SYSTEM: You are a helpful assistant.
|
|
30
|
-
* USER: Hello!
|
|
31
|
-
* ASSISTANT: Hi there!
|
|
32
|
-
* USER: How are you?
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* This format is:
|
|
36
|
-
* - Human-readable for debugging
|
|
37
|
-
* - Token-efficient (minimal overhead)
|
|
38
|
-
* - Deterministic (same input = same output)
|
|
39
|
-
* - Compatible with most instruction-following models
|
|
40
|
-
*
|
|
41
|
-
* @param messages - Array of messages to convert to a prompt
|
|
42
|
-
* @returns A single prompt string ready for generation
|
|
43
|
-
*
|
|
44
|
-
* @example
|
|
45
|
-
* ```ts
|
|
46
|
-
* const prompt = buildPrompt([
|
|
47
|
-
* { role: 'system', content: 'You are helpful.' },
|
|
48
|
-
* { role: 'user', content: 'Hi!' },
|
|
49
|
-
* { role: 'assistant', content: 'Hello!' },
|
|
50
|
-
* { role: 'user', content: 'What is 2+2?' }
|
|
51
|
-
* ]);
|
|
52
|
-
* // Returns:
|
|
53
|
-
* // "SYSTEM: You are helpful.\nUSER: Hi!\nASSISTANT: Hello!\nUSER: What is 2+2?"
|
|
54
|
-
* ```
|
|
55
|
-
*/
|
|
56
|
-
export declare function buildPrompt(messages: LLMMessage[]): string;
|
|
57
|
-
/**
|
|
58
|
-
* ChatMemoryManager - Manages conversation history for stateless on-device AI models.
|
|
59
|
-
*
|
|
60
|
-
* IMPORTANT: On-device models have no built-in memory or session state.
|
|
61
|
-
* This class stores messages client-side and provides methods to:
|
|
62
|
-
* - Add user and assistant messages
|
|
63
|
-
* - Build a complete prompt from conversation history
|
|
64
|
-
* - Limit history to prevent context overflow
|
|
65
|
-
* - Clear or reset the conversation
|
|
66
|
-
*
|
|
67
|
-
* The manager automatically trims old messages when the turn limit is exceeded,
|
|
68
|
-
* keeping the most recent exchanges while preserving the system prompt.
|
|
69
|
-
*
|
|
70
|
-
* @example
|
|
71
|
-
* ```ts
|
|
72
|
-
* // Create a memory manager with a system prompt
|
|
73
|
-
* const memory = new ChatMemoryManager({
|
|
74
|
-
* maxTurns: 5,
|
|
75
|
-
* systemPrompt: 'You are a helpful coding assistant.'
|
|
76
|
-
* });
|
|
77
|
-
*
|
|
78
|
-
* // Add a user message
|
|
79
|
-
* memory.addUserMessage('How do I reverse a string in JavaScript?');
|
|
80
|
-
*
|
|
81
|
-
* // Get the full prompt for generation
|
|
82
|
-
* const prompt = memory.getPrompt();
|
|
83
|
-
* // "SYSTEM: You are a helpful coding assistant.\nUSER: How do I reverse a string in JavaScript?"
|
|
84
|
-
*
|
|
85
|
-
* // After generation, add the assistant's response
|
|
86
|
-
* const response = await generate(prompt);
|
|
87
|
-
* memory.addAssistantMessage(response);
|
|
88
|
-
*
|
|
89
|
-
* // Continue the conversation
|
|
90
|
-
* memory.addUserMessage('Can you show me with an arrow function?');
|
|
91
|
-
* const nextPrompt = memory.getPrompt();
|
|
92
|
-
* // Now includes the full conversation history
|
|
93
|
-
* ```
|
|
94
|
-
*/
|
|
95
|
-
export declare class ChatMemoryManager {
|
|
96
|
-
private messages;
|
|
97
|
-
private systemPrompt;
|
|
98
|
-
private maxTurns;
|
|
99
|
-
private contextWindow;
|
|
100
|
-
/**
|
|
101
|
-
* Create a new ChatMemoryManager.
|
|
102
|
-
*
|
|
103
|
-
* @param options - Configuration options
|
|
104
|
-
* @param options.maxTurns - Max turns to keep (default: 10). A turn = 1 user + 1 assistant message.
|
|
105
|
-
* @param options.systemPrompt - Optional system prompt to include in every prompt.
|
|
106
|
-
* @param options.contextWindow - Max tokens for context (default: Infinity). Trims oldest messages when exceeded.
|
|
107
|
-
*/
|
|
108
|
-
constructor(options?: ChatMemoryOptions);
|
|
109
|
-
/**
|
|
110
|
-
* Add a user message to the conversation history.
|
|
111
|
-
*
|
|
112
|
-
* @param content - The user's message content
|
|
113
|
-
*/
|
|
114
|
-
addUserMessage(content: string): void;
|
|
115
|
-
/**
|
|
116
|
-
* Add an assistant message to the conversation history.
|
|
117
|
-
* Call this after receiving a response from the model.
|
|
118
|
-
*
|
|
119
|
-
* @param content - The assistant's response content
|
|
120
|
-
*/
|
|
121
|
-
addAssistantMessage(content: string): void;
|
|
122
|
-
/**
|
|
123
|
-
* Add a message with any role to the conversation history.
|
|
124
|
-
*
|
|
125
|
-
* @param message - The message to add
|
|
126
|
-
*/
|
|
127
|
-
addMessage(message: LLMMessage): void;
|
|
128
|
-
/**
|
|
129
|
-
* Get the complete prompt string for the current conversation.
|
|
130
|
-
*
|
|
131
|
-
* This method builds a single prompt containing:
|
|
132
|
-
* 1. The system prompt (if set)
|
|
133
|
-
* 2. All conversation messages within the turn limit
|
|
134
|
-
*
|
|
135
|
-
* Send this prompt to the on-device model for generation.
|
|
136
|
-
*
|
|
137
|
-
* @returns The complete prompt string
|
|
138
|
-
*/
|
|
139
|
-
getPrompt(): string;
|
|
140
|
-
/**
|
|
141
|
-
* Get all messages including the system prompt as an LLMMessage array.
|
|
142
|
-
*
|
|
143
|
-
* Useful if you need to pass messages to an API that accepts message arrays
|
|
144
|
-
* rather than a single prompt string.
|
|
145
|
-
*
|
|
146
|
-
* @returns Array of all messages including system prompt
|
|
147
|
-
*/
|
|
148
|
-
getAllMessages(): LLMMessage[];
|
|
149
|
-
/**
|
|
150
|
-
* Get only the conversation messages (excludes system prompt).
|
|
151
|
-
*
|
|
152
|
-
* @returns Array of user and assistant messages
|
|
153
|
-
*/
|
|
154
|
-
getMessages(): LLMMessage[];
|
|
155
|
-
/**
|
|
156
|
-
* Get a snapshot of the current memory state.
|
|
157
|
-
*
|
|
158
|
-
* Useful for debugging, persistence, or UI display.
|
|
159
|
-
*
|
|
160
|
-
* @returns Current memory state snapshot
|
|
161
|
-
*/
|
|
162
|
-
getSnapshot(): ChatMemorySnapshot;
|
|
163
|
-
/**
|
|
164
|
-
* Get the current number of conversation turns.
|
|
165
|
-
*
|
|
166
|
-
* A turn is counted as a user message (assistant responses don't add turns).
|
|
167
|
-
* This reflects how many user interactions have occurred.
|
|
168
|
-
*
|
|
169
|
-
* @returns Number of turns in the current conversation
|
|
170
|
-
*/
|
|
171
|
-
getTurnCount(): number;
|
|
172
|
-
/**
|
|
173
|
-
* Update the system prompt.
|
|
174
|
-
*
|
|
175
|
-
* The system prompt is preserved separately and never trimmed by turn limits.
|
|
176
|
-
*
|
|
177
|
-
* @param prompt - New system prompt, or undefined to clear
|
|
178
|
-
*/
|
|
179
|
-
setSystemPrompt(prompt: string | undefined): void;
|
|
180
|
-
/**
|
|
181
|
-
* Get the current system prompt.
|
|
182
|
-
*
|
|
183
|
-
* @returns The system prompt or undefined if not set
|
|
184
|
-
*/
|
|
185
|
-
getSystemPrompt(): string | undefined;
|
|
186
|
-
/**
|
|
187
|
-
* Update the maximum number of turns to retain.
|
|
188
|
-
*
|
|
189
|
-
* If the new limit is lower than current turn count, older messages
|
|
190
|
-
* will be trimmed immediately.
|
|
191
|
-
*
|
|
192
|
-
* @param maxTurns - New maximum turns
|
|
193
|
-
*/
|
|
194
|
-
setMaxTurns(maxTurns: number): void;
|
|
195
|
-
/**
|
|
196
|
-
* Update the context window (max tokens).
|
|
197
|
-
*
|
|
198
|
-
* Called internally when setModel() switches to a model with a different
|
|
199
|
-
* context window. Can also be called manually.
|
|
200
|
-
*
|
|
201
|
-
* @param tokens - Maximum token budget. Use Infinity to disable token-based trimming.
|
|
202
|
-
*/
|
|
203
|
-
setContextWindow(tokens: number): void;
|
|
204
|
-
/**
|
|
205
|
-
* Get the current context window setting.
|
|
206
|
-
*/
|
|
207
|
-
getContextWindow(): number;
|
|
208
|
-
/**
|
|
209
|
-
* Clear all conversation messages but keep the system prompt.
|
|
210
|
-
*
|
|
211
|
-
* Use this to start a new conversation with the same assistant persona.
|
|
212
|
-
*/
|
|
213
|
-
clear(): void;
|
|
214
|
-
/**
|
|
215
|
-
* Reset everything including the system prompt.
|
|
216
|
-
*
|
|
217
|
-
* Use this for a complete fresh start.
|
|
218
|
-
*/
|
|
219
|
-
reset(): void;
|
|
220
|
-
/**
|
|
221
|
-
* Trim conversation history to stay within limits.
|
|
222
|
-
*
|
|
223
|
-
* This preserves the most recent messages while removing older ones.
|
|
224
|
-
* The system prompt is never affected by trimming.
|
|
225
|
-
*
|
|
226
|
-
* Trimming strategy:
|
|
227
|
-
* 1. Trim by turn count (maxTurns) -- remove oldest user+assistant pairs
|
|
228
|
-
* 2. Trim by token budget (contextWindow) -- remove oldest messages until under budget
|
|
229
|
-
* Whichever limit is hit first triggers trimming.
|
|
230
|
-
*/
|
|
231
|
-
private trimHistory;
|
|
232
|
-
private trimByTurns;
|
|
233
|
-
private trimByTokens;
|
|
234
|
-
}
|
|
235
|
-
//# sourceMappingURL=memory.d.ts.map
|
package/build/memory.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,UAAU,EAAW,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAKrF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,MAAM,CAe1D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,qBAAa,iBAAiB;IAE5B,OAAO,CAAC,QAAQ,CAAoB;IAGpC,OAAO,CAAC,YAAY,CAAqB;IAGzC,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,aAAa,CAAS;IAE9B;;;;;;;OAOG;gBACS,OAAO,GAAE,iBAAsB;IAM3C;;;;OAIG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKrC;;;;;OAKG;IACH,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK1C;;;;OAIG;IACH,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAUrC;;;;;;;;;;OAUG;IACH,SAAS,IAAI,MAAM;IAKnB;;;;;;;OAOG;IACH,cAAc,IAAI,UAAU,EAAE;IAY9B;;;;OAIG;IACH,WAAW,IAAI,UAAU,EAAE;IAI3B;;;;;;OAMG;IACH,WAAW,IAAI,kBAAkB;IASjC;;;;;;;OAOG;IACH,YAAY,IAAI,MAAM;IAItB;;;;;;OAMG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAIjD;;;;OAIG;IACH,eAAe,IAAI,MAAM,GAAG,SAAS;IAIrC;;;;;;;OAOG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKnC;;;;;;;OAOG;IACH,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAKtC;;OAEG;IACH,gBAAgB,IAAI,MAAM;IAI1B;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAIb;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAKb;;;;;;;;;;OAUG;IACH,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,WAAW;IA2BnB,OAAO,CAAC,YAAY;CAsBrB"}
|
package/build/memory.js
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chat Memory Management for On-Device AI Models
|
|
3
|
-
*
|
|
4
|
-
* WHY CLIENT-MANAGED MEMORY IS REQUIRED:
|
|
5
|
-
* --------------------------------------
|
|
6
|
-
* On-device AI models (Apple Foundation Models, local LLMs) are stateless.
|
|
7
|
-
* Unlike cloud-based APIs that may maintain server-side conversation state,
|
|
8
|
-
* on-device models have no session persistence or built-in memory.
|
|
9
|
-
*
|
|
10
|
-
* Each generation call accepts a single prompt string and returns a response
|
|
11
|
-
* with no knowledge of previous interactions. To enable multi-turn conversations,
|
|
12
|
-
* the client must:
|
|
13
|
-
*
|
|
14
|
-
* 1. Store all messages locally (this module handles that)
|
|
15
|
-
* 2. Build a complete prompt containing the full conversation history
|
|
16
|
-
* 3. Send the entire context on every generation request
|
|
17
|
-
* 4. Manage memory limits to avoid exceeding model context windows
|
|
18
|
-
*
|
|
19
|
-
* This approach is framework-agnostic and works with React, React Native,
|
|
20
|
-
* Expo, or any JavaScript/TypeScript environment.
|
|
21
|
-
*/
|
|
22
|
-
// Default maximum turns if not specified
|
|
23
|
-
const DEFAULT_MAX_TURNS = 10;
|
|
24
|
-
/**
|
|
25
|
-
* Build a single prompt string from an array of messages.
|
|
26
|
-
*
|
|
27
|
-
* Uses a simple, deterministic format optimized for on-device models:
|
|
28
|
-
*
|
|
29
|
-
* ```
|
|
30
|
-
* SYSTEM: You are a helpful assistant.
|
|
31
|
-
* USER: Hello!
|
|
32
|
-
* ASSISTANT: Hi there!
|
|
33
|
-
* USER: How are you?
|
|
34
|
-
* ```
|
|
35
|
-
*
|
|
36
|
-
* This format is:
|
|
37
|
-
* - Human-readable for debugging
|
|
38
|
-
* - Token-efficient (minimal overhead)
|
|
39
|
-
* - Deterministic (same input = same output)
|
|
40
|
-
* - Compatible with most instruction-following models
|
|
41
|
-
*
|
|
42
|
-
* @param messages - Array of messages to convert to a prompt
|
|
43
|
-
* @returns A single prompt string ready for generation
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* ```ts
|
|
47
|
-
* const prompt = buildPrompt([
|
|
48
|
-
* { role: 'system', content: 'You are helpful.' },
|
|
49
|
-
* { role: 'user', content: 'Hi!' },
|
|
50
|
-
* { role: 'assistant', content: 'Hello!' },
|
|
51
|
-
* { role: 'user', content: 'What is 2+2?' }
|
|
52
|
-
* ]);
|
|
53
|
-
* // Returns:
|
|
54
|
-
* // "SYSTEM: You are helpful.\nUSER: Hi!\nASSISTANT: Hello!\nUSER: What is 2+2?"
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
export function buildPrompt(messages) {
|
|
58
|
-
if (!messages || messages.length === 0) {
|
|
59
|
-
return '';
|
|
60
|
-
}
|
|
61
|
-
// Map role to uppercase label for clarity
|
|
62
|
-
const roleLabels = {
|
|
63
|
-
system: 'SYSTEM',
|
|
64
|
-
user: 'USER',
|
|
65
|
-
assistant: 'ASSISTANT',
|
|
66
|
-
};
|
|
67
|
-
return messages
|
|
68
|
-
.map((msg) => `${roleLabels[msg.role]}: ${msg.content}`)
|
|
69
|
-
.join('\n');
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* ChatMemoryManager - Manages conversation history for stateless on-device AI models.
|
|
73
|
-
*
|
|
74
|
-
* IMPORTANT: On-device models have no built-in memory or session state.
|
|
75
|
-
* This class stores messages client-side and provides methods to:
|
|
76
|
-
* - Add user and assistant messages
|
|
77
|
-
* - Build a complete prompt from conversation history
|
|
78
|
-
* - Limit history to prevent context overflow
|
|
79
|
-
* - Clear or reset the conversation
|
|
80
|
-
*
|
|
81
|
-
* The manager automatically trims old messages when the turn limit is exceeded,
|
|
82
|
-
* keeping the most recent exchanges while preserving the system prompt.
|
|
83
|
-
*
|
|
84
|
-
* @example
|
|
85
|
-
* ```ts
|
|
86
|
-
* // Create a memory manager with a system prompt
|
|
87
|
-
* const memory = new ChatMemoryManager({
|
|
88
|
-
* maxTurns: 5,
|
|
89
|
-
* systemPrompt: 'You are a helpful coding assistant.'
|
|
90
|
-
* });
|
|
91
|
-
*
|
|
92
|
-
* // Add a user message
|
|
93
|
-
* memory.addUserMessage('How do I reverse a string in JavaScript?');
|
|
94
|
-
*
|
|
95
|
-
* // Get the full prompt for generation
|
|
96
|
-
* const prompt = memory.getPrompt();
|
|
97
|
-
* // "SYSTEM: You are a helpful coding assistant.\nUSER: How do I reverse a string in JavaScript?"
|
|
98
|
-
*
|
|
99
|
-
* // After generation, add the assistant's response
|
|
100
|
-
* const response = await generate(prompt);
|
|
101
|
-
* memory.addAssistantMessage(response);
|
|
102
|
-
*
|
|
103
|
-
* // Continue the conversation
|
|
104
|
-
* memory.addUserMessage('Can you show me with an arrow function?');
|
|
105
|
-
* const nextPrompt = memory.getPrompt();
|
|
106
|
-
* // Now includes the full conversation history
|
|
107
|
-
* ```
|
|
108
|
-
*/
|
|
109
|
-
export class ChatMemoryManager {
|
|
110
|
-
// Internal storage for conversation messages (excluding system prompt)
|
|
111
|
-
messages = [];
|
|
112
|
-
// Optional system prompt, stored separately to ensure it's never trimmed
|
|
113
|
-
systemPrompt;
|
|
114
|
-
// Maximum conversation turns to retain
|
|
115
|
-
maxTurns;
|
|
116
|
-
// Maximum context window in tokens. Infinity means no token-based trimming.
|
|
117
|
-
contextWindow;
|
|
118
|
-
/**
|
|
119
|
-
* Create a new ChatMemoryManager.
|
|
120
|
-
*
|
|
121
|
-
* @param options - Configuration options
|
|
122
|
-
* @param options.maxTurns - Max turns to keep (default: 10). A turn = 1 user + 1 assistant message.
|
|
123
|
-
* @param options.systemPrompt - Optional system prompt to include in every prompt.
|
|
124
|
-
* @param options.contextWindow - Max tokens for context (default: Infinity). Trims oldest messages when exceeded.
|
|
125
|
-
*/
|
|
126
|
-
constructor(options = {}) {
|
|
127
|
-
this.maxTurns = options.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
128
|
-
this.systemPrompt = options.systemPrompt;
|
|
129
|
-
this.contextWindow = options.contextWindow ?? Infinity;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Add a user message to the conversation history.
|
|
133
|
-
*
|
|
134
|
-
* @param content - The user's message content
|
|
135
|
-
*/
|
|
136
|
-
addUserMessage(content) {
|
|
137
|
-
this.messages.push({ role: 'user', content });
|
|
138
|
-
this.trimHistory();
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Add an assistant message to the conversation history.
|
|
142
|
-
* Call this after receiving a response from the model.
|
|
143
|
-
*
|
|
144
|
-
* @param content - The assistant's response content
|
|
145
|
-
*/
|
|
146
|
-
addAssistantMessage(content) {
|
|
147
|
-
this.messages.push({ role: 'assistant', content });
|
|
148
|
-
this.trimHistory();
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Add a message with any role to the conversation history.
|
|
152
|
-
*
|
|
153
|
-
* @param message - The message to add
|
|
154
|
-
*/
|
|
155
|
-
addMessage(message) {
|
|
156
|
-
// System messages update the system prompt instead of adding to history
|
|
157
|
-
if (message.role === 'system') {
|
|
158
|
-
this.systemPrompt = message.content;
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
this.messages.push(message);
|
|
162
|
-
this.trimHistory();
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Get the complete prompt string for the current conversation.
|
|
166
|
-
*
|
|
167
|
-
* This method builds a single prompt containing:
|
|
168
|
-
* 1. The system prompt (if set)
|
|
169
|
-
* 2. All conversation messages within the turn limit
|
|
170
|
-
*
|
|
171
|
-
* Send this prompt to the on-device model for generation.
|
|
172
|
-
*
|
|
173
|
-
* @returns The complete prompt string
|
|
174
|
-
*/
|
|
175
|
-
getPrompt() {
|
|
176
|
-
const allMessages = this.getAllMessages();
|
|
177
|
-
return buildPrompt(allMessages);
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Get all messages including the system prompt as an LLMMessage array.
|
|
181
|
-
*
|
|
182
|
-
* Useful if you need to pass messages to an API that accepts message arrays
|
|
183
|
-
* rather than a single prompt string.
|
|
184
|
-
*
|
|
185
|
-
* @returns Array of all messages including system prompt
|
|
186
|
-
*/
|
|
187
|
-
getAllMessages() {
|
|
188
|
-
const result = [];
|
|
189
|
-
// System prompt always comes first (if present)
|
|
190
|
-
if (this.systemPrompt) {
|
|
191
|
-
result.push({ role: 'system', content: this.systemPrompt });
|
|
192
|
-
}
|
|
193
|
-
result.push(...this.messages);
|
|
194
|
-
return result;
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Get only the conversation messages (excludes system prompt).
|
|
198
|
-
*
|
|
199
|
-
* @returns Array of user and assistant messages
|
|
200
|
-
*/
|
|
201
|
-
getMessages() {
|
|
202
|
-
return [...this.messages];
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Get a snapshot of the current memory state.
|
|
206
|
-
*
|
|
207
|
-
* Useful for debugging, persistence, or UI display.
|
|
208
|
-
*
|
|
209
|
-
* @returns Current memory state snapshot
|
|
210
|
-
*/
|
|
211
|
-
getSnapshot() {
|
|
212
|
-
return {
|
|
213
|
-
messages: this.getAllMessages(),
|
|
214
|
-
systemPrompt: this.systemPrompt,
|
|
215
|
-
turnCount: this.getTurnCount(),
|
|
216
|
-
maxTurns: this.maxTurns,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Get the current number of conversation turns.
|
|
221
|
-
*
|
|
222
|
-
* A turn is counted as a user message (assistant responses don't add turns).
|
|
223
|
-
* This reflects how many user interactions have occurred.
|
|
224
|
-
*
|
|
225
|
-
* @returns Number of turns in the current conversation
|
|
226
|
-
*/
|
|
227
|
-
getTurnCount() {
|
|
228
|
-
return this.messages.filter((m) => m.role === 'user').length;
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Update the system prompt.
|
|
232
|
-
*
|
|
233
|
-
* The system prompt is preserved separately and never trimmed by turn limits.
|
|
234
|
-
*
|
|
235
|
-
* @param prompt - New system prompt, or undefined to clear
|
|
236
|
-
*/
|
|
237
|
-
setSystemPrompt(prompt) {
|
|
238
|
-
this.systemPrompt = prompt;
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Get the current system prompt.
|
|
242
|
-
*
|
|
243
|
-
* @returns The system prompt or undefined if not set
|
|
244
|
-
*/
|
|
245
|
-
getSystemPrompt() {
|
|
246
|
-
return this.systemPrompt;
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Update the maximum number of turns to retain.
|
|
250
|
-
*
|
|
251
|
-
* If the new limit is lower than current turn count, older messages
|
|
252
|
-
* will be trimmed immediately.
|
|
253
|
-
*
|
|
254
|
-
* @param maxTurns - New maximum turns
|
|
255
|
-
*/
|
|
256
|
-
setMaxTurns(maxTurns) {
|
|
257
|
-
this.maxTurns = maxTurns;
|
|
258
|
-
this.trimHistory();
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Update the context window (max tokens).
|
|
262
|
-
*
|
|
263
|
-
* Called internally when setModel() switches to a model with a different
|
|
264
|
-
* context window. Can also be called manually.
|
|
265
|
-
*
|
|
266
|
-
* @param tokens - Maximum token budget. Use Infinity to disable token-based trimming.
|
|
267
|
-
*/
|
|
268
|
-
setContextWindow(tokens) {
|
|
269
|
-
this.contextWindow = tokens;
|
|
270
|
-
this.trimHistory();
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Get the current context window setting.
|
|
274
|
-
*/
|
|
275
|
-
getContextWindow() {
|
|
276
|
-
return this.contextWindow;
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Clear all conversation messages but keep the system prompt.
|
|
280
|
-
*
|
|
281
|
-
* Use this to start a new conversation with the same assistant persona.
|
|
282
|
-
*/
|
|
283
|
-
clear() {
|
|
284
|
-
this.messages = [];
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Reset everything including the system prompt.
|
|
288
|
-
*
|
|
289
|
-
* Use this for a complete fresh start.
|
|
290
|
-
*/
|
|
291
|
-
reset() {
|
|
292
|
-
this.messages = [];
|
|
293
|
-
this.systemPrompt = undefined;
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Trim conversation history to stay within limits.
|
|
297
|
-
*
|
|
298
|
-
* This preserves the most recent messages while removing older ones.
|
|
299
|
-
* The system prompt is never affected by trimming.
|
|
300
|
-
*
|
|
301
|
-
* Trimming strategy:
|
|
302
|
-
* 1. Trim by turn count (maxTurns) -- remove oldest user+assistant pairs
|
|
303
|
-
* 2. Trim by token budget (contextWindow) -- remove oldest messages until under budget
|
|
304
|
-
* Whichever limit is hit first triggers trimming.
|
|
305
|
-
*/
|
|
306
|
-
trimHistory() {
|
|
307
|
-
// Step 1: Trim by turn count
|
|
308
|
-
this.trimByTurns();
|
|
309
|
-
// Step 2: Trim by context window (if set)
|
|
310
|
-
if (this.contextWindow !== Infinity) {
|
|
311
|
-
this.trimByTokens();
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
trimByTurns() {
|
|
315
|
-
const userMessageIndices = [];
|
|
316
|
-
this.messages.forEach((msg, idx) => {
|
|
317
|
-
if (msg.role === 'user') {
|
|
318
|
-
userMessageIndices.push(idx);
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
const turnsToRemove = userMessageIndices.length - this.maxTurns;
|
|
322
|
-
if (turnsToRemove <= 0) {
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
const lastTurnToRemoveIdx = userMessageIndices[turnsToRemove - 1];
|
|
326
|
-
let cutIndex = lastTurnToRemoveIdx + 1;
|
|
327
|
-
while (cutIndex < this.messages.length &&
|
|
328
|
-
this.messages[cutIndex].role === 'assistant') {
|
|
329
|
-
cutIndex++;
|
|
330
|
-
}
|
|
331
|
-
this.messages = this.messages.slice(cutIndex);
|
|
332
|
-
}
|
|
333
|
-
trimByTokens() {
|
|
334
|
-
// TODO: chars/4 is a rough heuristic. Different models use different
|
|
335
|
-
// tokenizers (SentencePiece for Gemma, BPE for others). If precision
|
|
336
|
-
// matters, consider per-model tokenizer config. For now this is
|
|
337
|
-
// conservative enough for trimming.
|
|
338
|
-
const estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
339
|
-
const systemTokens = this.systemPrompt
|
|
340
|
-
? estimateTokens(this.systemPrompt)
|
|
341
|
-
: 0;
|
|
342
|
-
let totalTokens = systemTokens;
|
|
343
|
-
for (const msg of this.messages) {
|
|
344
|
-
totalTokens += estimateTokens(msg.content);
|
|
345
|
-
}
|
|
346
|
-
// Remove oldest messages until we're under budget
|
|
347
|
-
while (totalTokens > this.contextWindow && this.messages.length > 0) {
|
|
348
|
-
const removed = this.messages.shift();
|
|
349
|
-
totalTokens -= estimateTokens(removed.content);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
//# sourceMappingURL=memory.js.map
|
package/build/memory.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Chat Memory Management for On-Device AI Models\n *\n * WHY CLIENT-MANAGED MEMORY IS REQUIRED:\n * --------------------------------------\n * On-device AI models (Apple Foundation Models, local LLMs) are stateless.\n * Unlike cloud-based APIs that may maintain server-side conversation state,\n * on-device models have no session persistence or built-in memory.\n *\n * Each generation call accepts a single prompt string and returns a response\n * with no knowledge of previous interactions. To enable multi-turn conversations,\n * the client must:\n *\n * 1. Store all messages locally (this module handles that)\n * 2. Build a complete prompt containing the full conversation history\n * 3. Send the entire context on every generation request\n * 4. Manage memory limits to avoid exceeding model context windows\n *\n * This approach is framework-agnostic and works with React, React Native,\n * Expo, or any JavaScript/TypeScript environment.\n */\n\nimport { LLMMessage, LLMRole, ChatMemoryOptions, ChatMemorySnapshot } from './types';\n\n// Default maximum turns if not specified\nconst DEFAULT_MAX_TURNS = 10;\n\n/**\n * Build a single prompt string from an array of messages.\n *\n * Uses a simple, deterministic format optimized for on-device models:\n *\n * ```\n * SYSTEM: You are a helpful assistant.\n * USER: Hello!\n * ASSISTANT: Hi there!\n * USER: How are you?\n * ```\n *\n * This format is:\n * - Human-readable for debugging\n * - Token-efficient (minimal overhead)\n * - Deterministic (same input = same output)\n * - Compatible with most instruction-following models\n *\n * @param messages - Array of messages to convert to a prompt\n * @returns A single prompt string ready for generation\n *\n * @example\n * ```ts\n * const prompt = buildPrompt([\n * { role: 'system', content: 'You are helpful.' },\n * { role: 'user', content: 'Hi!' },\n * { role: 'assistant', content: 'Hello!' },\n * { role: 'user', content: 'What is 2+2?' }\n * ]);\n * // Returns:\n * // \"SYSTEM: You are helpful.\\nUSER: Hi!\\nASSISTANT: Hello!\\nUSER: What is 2+2?\"\n * ```\n */\nexport function buildPrompt(messages: LLMMessage[]): string {\n if (!messages || messages.length === 0) {\n return '';\n }\n\n // Map role to uppercase label for clarity\n const roleLabels: Record<LLMRole, string> = {\n system: 'SYSTEM',\n user: 'USER',\n assistant: 'ASSISTANT',\n };\n\n return messages\n .map((msg) => `${roleLabels[msg.role]}: ${msg.content}`)\n .join('\\n');\n}\n\n/**\n * ChatMemoryManager - Manages conversation history for stateless on-device AI models.\n *\n * IMPORTANT: On-device models have no built-in memory or session state.\n * This class stores messages client-side and provides methods to:\n * - Add user and assistant messages\n * - Build a complete prompt from conversation history\n * - Limit history to prevent context overflow\n * - Clear or reset the conversation\n *\n * The manager automatically trims old messages when the turn limit is exceeded,\n * keeping the most recent exchanges while preserving the system prompt.\n *\n * @example\n * ```ts\n * // Create a memory manager with a system prompt\n * const memory = new ChatMemoryManager({\n * maxTurns: 5,\n * systemPrompt: 'You are a helpful coding assistant.'\n * });\n *\n * // Add a user message\n * memory.addUserMessage('How do I reverse a string in JavaScript?');\n *\n * // Get the full prompt for generation\n * const prompt = memory.getPrompt();\n * // \"SYSTEM: You are a helpful coding assistant.\\nUSER: How do I reverse a string in JavaScript?\"\n *\n * // After generation, add the assistant's response\n * const response = await generate(prompt);\n * memory.addAssistantMessage(response);\n *\n * // Continue the conversation\n * memory.addUserMessage('Can you show me with an arrow function?');\n * const nextPrompt = memory.getPrompt();\n * // Now includes the full conversation history\n * ```\n */\nexport class ChatMemoryManager {\n // Internal storage for conversation messages (excluding system prompt)\n private messages: LLMMessage[] = [];\n\n // Optional system prompt, stored separately to ensure it's never trimmed\n private systemPrompt: string | undefined;\n\n // Maximum conversation turns to retain\n private maxTurns: number;\n\n // Maximum context window in tokens. Infinity means no token-based trimming.\n private contextWindow: number;\n\n /**\n * Create a new ChatMemoryManager.\n *\n * @param options - Configuration options\n * @param options.maxTurns - Max turns to keep (default: 10). A turn = 1 user + 1 assistant message.\n * @param options.systemPrompt - Optional system prompt to include in every prompt.\n * @param options.contextWindow - Max tokens for context (default: Infinity). Trims oldest messages when exceeded.\n */\n constructor(options: ChatMemoryOptions = {}) {\n this.maxTurns = options.maxTurns ?? DEFAULT_MAX_TURNS;\n this.systemPrompt = options.systemPrompt;\n this.contextWindow = options.contextWindow ?? Infinity;\n }\n\n /**\n * Add a user message to the conversation history.\n *\n * @param content - The user's message content\n */\n addUserMessage(content: string): void {\n this.messages.push({ role: 'user', content });\n this.trimHistory();\n }\n\n /**\n * Add an assistant message to the conversation history.\n * Call this after receiving a response from the model.\n *\n * @param content - The assistant's response content\n */\n addAssistantMessage(content: string): void {\n this.messages.push({ role: 'assistant', content });\n this.trimHistory();\n }\n\n /**\n * Add a message with any role to the conversation history.\n *\n * @param message - The message to add\n */\n addMessage(message: LLMMessage): void {\n // System messages update the system prompt instead of adding to history\n if (message.role === 'system') {\n this.systemPrompt = message.content;\n return;\n }\n this.messages.push(message);\n this.trimHistory();\n }\n\n /**\n * Get the complete prompt string for the current conversation.\n *\n * This method builds a single prompt containing:\n * 1. The system prompt (if set)\n * 2. All conversation messages within the turn limit\n *\n * Send this prompt to the on-device model for generation.\n *\n * @returns The complete prompt string\n */\n getPrompt(): string {\n const allMessages = this.getAllMessages();\n return buildPrompt(allMessages);\n }\n\n /**\n * Get all messages including the system prompt as an LLMMessage array.\n *\n * Useful if you need to pass messages to an API that accepts message arrays\n * rather than a single prompt string.\n *\n * @returns Array of all messages including system prompt\n */\n getAllMessages(): LLMMessage[] {\n const result: LLMMessage[] = [];\n\n // System prompt always comes first (if present)\n if (this.systemPrompt) {\n result.push({ role: 'system', content: this.systemPrompt });\n }\n\n result.push(...this.messages);\n return result;\n }\n\n /**\n * Get only the conversation messages (excludes system prompt).\n *\n * @returns Array of user and assistant messages\n */\n getMessages(): LLMMessage[] {\n return [...this.messages];\n }\n\n /**\n * Get a snapshot of the current memory state.\n *\n * Useful for debugging, persistence, or UI display.\n *\n * @returns Current memory state snapshot\n */\n getSnapshot(): ChatMemorySnapshot {\n return {\n messages: this.getAllMessages(),\n systemPrompt: this.systemPrompt,\n turnCount: this.getTurnCount(),\n maxTurns: this.maxTurns,\n };\n }\n\n /**\n * Get the current number of conversation turns.\n *\n * A turn is counted as a user message (assistant responses don't add turns).\n * This reflects how many user interactions have occurred.\n *\n * @returns Number of turns in the current conversation\n */\n getTurnCount(): number {\n return this.messages.filter((m) => m.role === 'user').length;\n }\n\n /**\n * Update the system prompt.\n *\n * The system prompt is preserved separately and never trimmed by turn limits.\n *\n * @param prompt - New system prompt, or undefined to clear\n */\n setSystemPrompt(prompt: string | undefined): void {\n this.systemPrompt = prompt;\n }\n\n /**\n * Get the current system prompt.\n *\n * @returns The system prompt or undefined if not set\n */\n getSystemPrompt(): string | undefined {\n return this.systemPrompt;\n }\n\n /**\n * Update the maximum number of turns to retain.\n *\n * If the new limit is lower than current turn count, older messages\n * will be trimmed immediately.\n *\n * @param maxTurns - New maximum turns\n */\n setMaxTurns(maxTurns: number): void {\n this.maxTurns = maxTurns;\n this.trimHistory();\n }\n\n /**\n * Update the context window (max tokens).\n *\n * Called internally when setModel() switches to a model with a different\n * context window. Can also be called manually.\n *\n * @param tokens - Maximum token budget. Use Infinity to disable token-based trimming.\n */\n setContextWindow(tokens: number): void {\n this.contextWindow = tokens;\n this.trimHistory();\n }\n\n /**\n * Get the current context window setting.\n */\n getContextWindow(): number {\n return this.contextWindow;\n }\n\n /**\n * Clear all conversation messages but keep the system prompt.\n *\n * Use this to start a new conversation with the same assistant persona.\n */\n clear(): void {\n this.messages = [];\n }\n\n /**\n * Reset everything including the system prompt.\n *\n * Use this for a complete fresh start.\n */\n reset(): void {\n this.messages = [];\n this.systemPrompt = undefined;\n }\n\n /**\n * Trim conversation history to stay within limits.\n *\n * This preserves the most recent messages while removing older ones.\n * The system prompt is never affected by trimming.\n *\n * Trimming strategy:\n * 1. Trim by turn count (maxTurns) -- remove oldest user+assistant pairs\n * 2. Trim by token budget (contextWindow) -- remove oldest messages until under budget\n * Whichever limit is hit first triggers trimming.\n */\n private trimHistory(): void {\n // Step 1: Trim by turn count\n this.trimByTurns();\n\n // Step 2: Trim by context window (if set)\n if (this.contextWindow !== Infinity) {\n this.trimByTokens();\n }\n }\n\n private trimByTurns(): void {\n const userMessageIndices: number[] = [];\n this.messages.forEach((msg, idx) => {\n if (msg.role === 'user') {\n userMessageIndices.push(idx);\n }\n });\n\n const turnsToRemove = userMessageIndices.length - this.maxTurns;\n\n if (turnsToRemove <= 0) {\n return;\n }\n\n const lastTurnToRemoveIdx = userMessageIndices[turnsToRemove - 1];\n\n let cutIndex = lastTurnToRemoveIdx + 1;\n while (\n cutIndex < this.messages.length &&\n this.messages[cutIndex].role === 'assistant'\n ) {\n cutIndex++;\n }\n\n this.messages = this.messages.slice(cutIndex);\n }\n\n private trimByTokens(): void {\n // TODO: chars/4 is a rough heuristic. Different models use different\n // tokenizers (SentencePiece for Gemma, BPE for others). If precision\n // matters, consider per-model tokenizer config. For now this is\n // conservative enough for trimming.\n const estimateTokens = (text: string): number => Math.ceil(text.length / 4);\n\n const systemTokens = this.systemPrompt\n ? estimateTokens(this.systemPrompt)\n : 0;\n\n let totalTokens = systemTokens;\n for (const msg of this.messages) {\n totalTokens += estimateTokens(msg.content);\n }\n\n // Remove oldest messages until we're under budget\n while (totalTokens > this.contextWindow && this.messages.length > 0) {\n const removed = this.messages.shift()!;\n totalTokens -= estimateTokens(removed.content);\n }\n }\n}\n"]}
|