expo-ai-kit 0.3.5 → 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/android/src/main/java/expo/modules/aikit/ExpoAiKitModule.kt +30 -20
- package/android/src/main/java/expo/modules/aikit/GemmaInferenceClient.kt +21 -6
- 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/README.md
CHANGED
|
@@ -1,1169 +1,131 @@
|
|
|
1
1
|
# expo-ai-kit
|
|
2
2
|
|
|
3
|
-
On-device AI for Expo
|
|
4
|
-
|
|
5
|
-
**Now with Gemma 4 support** — Download and run Google's [Gemma 4](https://blog.google/innovation-and-ai/technology/developers-tools/gemma-4/) E2B (2.3B) and E4B (4.5B) models directly on Android devices via [LiteRT-LM](https://ai.google.dev/edge/litert-lm). Full on-device inference with GPU acceleration, streaming, and zero cloud dependency.
|
|
3
|
+
On-device AI for Expo & React Native. Run LLMs locally — no API keys, no cloud, no cost.
|
|
6
4
|
|
|
7
5
|
[](https://www.npmjs.com/package/expo-ai-kit)
|
|
8
6
|
[](https://opensource.org/licenses/MIT)
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
### Supported
|
|
13
|
-
|
|
14
|
-
| Platform | Details |
|
|
15
|
-
|----------|---------|
|
|
16
|
-
| iOS 26+ | [Apple Foundation Models](https://developer.apple.com/documentation/FoundationModels) |
|
|
17
|
-
| Android (supported devices) | [ML Kit Prompt API](https://developers.google.com/ml-kit/genai#prompt-device) |
|
|
18
|
-
|
|
19
|
-
### Downloadable Models (Gemma 4)
|
|
20
|
-
|
|
21
|
-
| Platform | Status |
|
|
22
|
-
|----------|--------|
|
|
23
|
-
| Android | Gemma 4 E2B (2.3B) and E4B (4.5B) via [LiteRT-LM](https://ai.google.dev/edge/litert-lm) |
|
|
24
|
-
| iOS | Coming soon — waiting for LiteRT-LM Swift APIs from Google |
|
|
25
|
-
|
|
26
|
-
> **Note:** iOS downloadable model support (Gemma 4 E2B/E4B) is planned for a future release. We are waiting for Google to ship native Swift APIs for LiteRT-LM. Built-in Apple Foundation Models work on iOS 26+ today.
|
|
27
|
-
|
|
28
|
-
### Unsupported
|
|
29
|
-
|
|
30
|
-
| Platform | Fallback Behavior |
|
|
31
|
-
|----------|-------------------|
|
|
32
|
-
| iOS < 26 | Returns fallback message |
|
|
33
|
-
| Android (unsupported devices) | Returns empty string |
|
|
34
|
-
|
|
35
|
-
## Features
|
|
36
|
-
|
|
37
|
-
- **Privacy-first** — All inference happens on-device; no data leaves the user's device
|
|
38
|
-
- **Zero latency** — No network round-trips required
|
|
39
|
-
- **Free forever** — No API costs, rate limits, or subscriptions
|
|
40
|
-
- **Gemma 4 on-device** — Download and run Gemma 4 E2B/E4B models directly on Android with GPU acceleration
|
|
41
|
-
- **Native performance** — Built on Apple Foundation Models (iOS), ML Kit (Android), and LiteRT-LM (Gemma 4)
|
|
42
|
-
- **Multi-turn conversations** — Full conversation context support
|
|
43
|
-
- **Streaming support** — Progressive token streaming for responsive UIs
|
|
44
|
-
- **Simple API** — Core functions plus prompt helpers for common tasks
|
|
45
|
-
- **Prompt helpers** — Built-in `summarize()`, `translate()`, `rewrite()`, and more
|
|
46
|
-
- **Smart suggestions** — `suggest()`, `smartReply()`, and `autocomplete()` for predictive text
|
|
47
|
-
- **React Hooks** — `useChat`, `useCompletion`, and `useOnDeviceAI` for plug-and-play integration
|
|
48
|
-
- **Chat memory** — Built-in `ChatMemoryManager` for managing conversation history
|
|
49
|
-
|
|
50
|
-
## Requirements
|
|
8
|
+
Run Google's **Gemma 4** (E2B / E4B), **Apple Foundation Models**, and **ML Kit**
|
|
9
|
+
entirely on-device — chat, streaming, and downloadable models, all local.
|
|
51
10
|
|
|
52
|
-
-
|
|
53
|
-
- **
|
|
54
|
-
- **
|
|
11
|
+
- **Private** — inference never leaves the device
|
|
12
|
+
- **Free** — no API costs, rate limits, or subscriptions
|
|
13
|
+
- **Native** — Apple Foundation Models (iOS 26+), ML Kit (Android), Gemma 4 via [LiteRT-LM](https://ai.google.dev/edge/litert-lm)
|
|
14
|
+
- **Streaming** — progressive token output with cancellation
|
|
15
|
+
- **Model management** — download, load, and switch Gemma 4 models at runtime
|
|
55
16
|
|
|
56
|
-
##
|
|
17
|
+
## Install
|
|
57
18
|
|
|
58
19
|
```bash
|
|
59
20
|
npx expo install expo-ai-kit
|
|
60
21
|
```
|
|
61
22
|
|
|
62
|
-
|
|
23
|
+
Bare React Native projects: run `npx pod-install` afterwards. Android needs
|
|
24
|
+
`minSdkVersion 26` — set it via `expo-build-properties` in `app.json`.
|
|
63
25
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
For Android, ensure your `app.json` includes the minimum SDK version:
|
|
67
|
-
|
|
68
|
-
```json
|
|
69
|
-
{
|
|
70
|
-
"expo": {
|
|
71
|
-
"plugins": [
|
|
72
|
-
[
|
|
73
|
-
"expo-build-properties",
|
|
74
|
-
{
|
|
75
|
-
"android": {
|
|
76
|
-
"minSdkVersion": 26
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
]
|
|
80
|
-
]
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## Quick Start
|
|
26
|
+
## Quick start
|
|
86
27
|
|
|
87
28
|
```tsx
|
|
88
29
|
import { isAvailable, sendMessage } from 'expo-ai-kit';
|
|
89
30
|
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
if (available) {
|
|
94
|
-
const response = await sendMessage([
|
|
95
|
-
{ role: 'user', content: 'Hello! What can you do?' }
|
|
31
|
+
if (await isAvailable()) {
|
|
32
|
+
const { text } = await sendMessage([
|
|
33
|
+
{ role: 'user', content: 'What is the capital of France?' },
|
|
96
34
|
]);
|
|
97
|
-
console.log(
|
|
35
|
+
console.log(text); // "Paris"
|
|
98
36
|
}
|
|
99
37
|
```
|
|
100
38
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
### Simple Prompt
|
|
104
|
-
|
|
105
|
-
The simplest way to use on-device AI:
|
|
39
|
+
Add a system prompt, or include `system`/`assistant` messages for multi-turn context.
|
|
40
|
+
On-device models are stateless — pass the full message history on every call.
|
|
106
41
|
|
|
107
42
|
```tsx
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
async function askAI(question: string) {
|
|
111
|
-
const available = await isAvailable();
|
|
112
|
-
|
|
113
|
-
if (!available) {
|
|
114
|
-
console.log('On-device AI not available');
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const response = await sendMessage([
|
|
119
|
-
{ role: 'user', content: question }
|
|
120
|
-
]);
|
|
121
|
-
return response.text;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const answer = await askAI('What is the capital of France?');
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### With Custom System Prompt
|
|
128
|
-
|
|
129
|
-
Customize the AI's behavior with a system prompt:
|
|
130
|
-
|
|
131
|
-
```tsx
|
|
132
|
-
import { sendMessage } from 'expo-ai-kit';
|
|
133
|
-
|
|
134
|
-
const response = await sendMessage(
|
|
43
|
+
const { text } = await sendMessage(
|
|
135
44
|
[{ role: 'user', content: 'Tell me a joke' }],
|
|
136
|
-
{ systemPrompt: 'You are a comedian
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
console.log(response.text);
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### Multi-turn Conversations
|
|
143
|
-
|
|
144
|
-
For conversations with context, use `ChatMemoryManager` to manage history:
|
|
145
|
-
|
|
146
|
-
```tsx
|
|
147
|
-
import { ChatMemoryManager, streamMessage } from 'expo-ai-kit';
|
|
148
|
-
|
|
149
|
-
// Create a memory manager (handles history automatically)
|
|
150
|
-
const memory = new ChatMemoryManager({
|
|
151
|
-
maxTurns: 10,
|
|
152
|
-
systemPrompt: 'You are a helpful assistant.',
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// Add user message and get response
|
|
156
|
-
memory.addUserMessage('My name is Alice.');
|
|
157
|
-
const { promise } = streamMessage(
|
|
158
|
-
memory.getAllMessages(),
|
|
159
|
-
(event) => console.log(event.accumulatedText)
|
|
160
|
-
);
|
|
161
|
-
const response = await promise;
|
|
162
|
-
|
|
163
|
-
// Store assistant response in memory
|
|
164
|
-
memory.addAssistantMessage(response.text);
|
|
165
|
-
|
|
166
|
-
// Continue the conversation (memory includes full history)
|
|
167
|
-
memory.addUserMessage('What is my name?');
|
|
168
|
-
const { promise: p2 } = streamMessage(
|
|
169
|
-
memory.getAllMessages(),
|
|
170
|
-
(event) => console.log(event.accumulatedText)
|
|
45
|
+
{ systemPrompt: 'You are a stand-up comedian.' }
|
|
171
46
|
);
|
|
172
|
-
// Response: "Your name is Alice."
|
|
173
47
|
```
|
|
174
48
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
```tsx
|
|
178
|
-
import { sendMessage, type LLMMessage } from 'expo-ai-kit';
|
|
179
|
-
|
|
180
|
-
const conversation: LLMMessage[] = [
|
|
181
|
-
{ role: 'user', content: 'My name is Alice.' },
|
|
182
|
-
{ role: 'assistant', content: 'Nice to meet you, Alice!' },
|
|
183
|
-
{ role: 'user', content: 'What is my name?' },
|
|
184
|
-
];
|
|
185
|
-
|
|
186
|
-
const response = await sendMessage(conversation, {
|
|
187
|
-
systemPrompt: 'You are a helpful assistant.',
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
console.log(response.text); // "Your name is Alice."
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### Streaming Responses
|
|
194
|
-
|
|
195
|
-
For a ChatGPT-like experience where text appears progressively:
|
|
49
|
+
## Streaming
|
|
196
50
|
|
|
197
51
|
```tsx
|
|
198
52
|
import { streamMessage } from 'expo-ai-kit';
|
|
199
53
|
|
|
200
|
-
const [responseText, setResponseText] = useState('');
|
|
201
|
-
|
|
202
54
|
const { promise, stop } = streamMessage(
|
|
203
|
-
[{ role: 'user', content: '
|
|
204
|
-
(event) =>
|
|
205
|
-
// Update UI with each token
|
|
206
|
-
setResponseText(event.accumulatedText);
|
|
207
|
-
|
|
208
|
-
// event.token - the new token/chunk
|
|
209
|
-
// event.accumulatedText - full text so far
|
|
210
|
-
// event.isDone - whether streaming is complete
|
|
211
|
-
},
|
|
212
|
-
{ systemPrompt: 'You are a creative storyteller.' }
|
|
55
|
+
[{ role: 'user', content: 'Write a short story' }],
|
|
56
|
+
(event) => setText(event.accumulatedText), // fired per token
|
|
213
57
|
);
|
|
214
58
|
|
|
215
|
-
//
|
|
216
|
-
// stop();
|
|
217
|
-
|
|
218
|
-
// Wait for completion
|
|
219
|
-
await promise;
|
|
59
|
+
await promise; // resolves with the final { text }
|
|
60
|
+
// stop(); // cancel at any point
|
|
220
61
|
```
|
|
221
62
|
|
|
222
|
-
|
|
63
|
+
## Downloadable models (Gemma 4)
|
|
223
64
|
|
|
224
|
-
|
|
65
|
+
On Android, download and run Gemma 4 models on top of the built-in OS model.
|
|
225
66
|
|
|
226
67
|
```tsx
|
|
227
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
getDownloadableModels, downloadModel, setModel, sendMessage,
|
|
70
|
+
} from 'expo-ai-kit';
|
|
228
71
|
|
|
229
|
-
//
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
// Translate text
|
|
233
|
-
const translated = await translate('Hello, world!', { to: 'Spanish' });
|
|
234
|
-
|
|
235
|
-
// Rewrite in a different style
|
|
236
|
-
const formal = await rewrite('hey whats up', { style: 'formal' });
|
|
237
|
-
|
|
238
|
-
// Extract key points
|
|
239
|
-
const points = await extractKeyPoints(article, { maxPoints: 5 });
|
|
240
|
-
|
|
241
|
-
// Answer questions about content
|
|
242
|
-
const answer = await answerQuestion('What is the main topic?', documentText);
|
|
243
|
-
```
|
|
72
|
+
// List models with their on-device status
|
|
73
|
+
const models = await getDownloadableModels();
|
|
244
74
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
```tsx
|
|
250
|
-
import { suggest, smartReply, autocomplete } from 'expo-ai-kit';
|
|
251
|
-
|
|
252
|
-
// Text suggestions — continue partial text
|
|
253
|
-
const suggestions = await suggest('I think we should', {
|
|
254
|
-
count: 3,
|
|
255
|
-
tone: 'professional',
|
|
256
|
-
context: 'team meeting notes'
|
|
257
|
-
});
|
|
258
|
-
suggestions.suggestions.forEach(s => console.log(s.text));
|
|
259
|
-
// "schedule a follow-up meeting to discuss next steps"
|
|
260
|
-
// "prioritize the Q2 deliverables before moving forward"
|
|
261
|
-
// "assign clear owners for each action item"
|
|
262
|
-
|
|
263
|
-
// Smart replies — Gmail/iMessage-style reply suggestions
|
|
264
|
-
const replies = await smartReply([
|
|
265
|
-
{ role: 'user', content: 'Hey, are you free for lunch tomorrow?' }
|
|
266
|
-
], { tone: 'friendly' });
|
|
267
|
-
replies.suggestions.forEach(s => console.log(s.text));
|
|
268
|
-
// "Sure, what time works for you?"
|
|
269
|
-
// "Sorry, I already have plans tomorrow."
|
|
270
|
-
// "Let me check my schedule and get back to you!"
|
|
271
|
-
|
|
272
|
-
// Autocomplete — short, instant completions for search bars and inputs
|
|
273
|
-
const completions = await autocomplete('How do I', {
|
|
274
|
-
context: 'cooking app',
|
|
275
|
-
maxWords: 8
|
|
75
|
+
// Download with progress, then activate
|
|
76
|
+
await downloadModel('gemma-e2b', {
|
|
77
|
+
onProgress: (p) => console.log(`${Math.round(p * 100)}%`),
|
|
276
78
|
});
|
|
277
|
-
|
|
278
|
-
// "make pasta from scratch"
|
|
279
|
-
// "preheat the oven correctly"
|
|
280
|
-
// "chop onions without crying"
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
All smart suggestion functions also have streaming variants (`streamSuggest`, `streamSmartReply`, `streamAutocomplete`). Use `parseSuggestResponse()` to parse streaming results:
|
|
284
|
-
|
|
285
|
-
```tsx
|
|
286
|
-
const { promise } = streamSuggest(
|
|
287
|
-
'The best way to',
|
|
288
|
-
(event) => setRawText(event.accumulatedText),
|
|
289
|
-
{ count: 3 }
|
|
290
|
-
);
|
|
291
|
-
const result = await promise;
|
|
292
|
-
const parsed = parseSuggestResponse(result.text);
|
|
293
|
-
// parsed.suggestions = [{ text: "..." }, { text: "..." }, { text: "..." }]
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
All helpers also have streaming variants (`streamSummarize`, `streamTranslate`, etc.):
|
|
297
|
-
|
|
298
|
-
```tsx
|
|
299
|
-
const { promise, stop } = streamSummarize(
|
|
300
|
-
longArticle,
|
|
301
|
-
(event) => setSummary(event.accumulatedText),
|
|
302
|
-
{ style: 'bullets' }
|
|
303
|
-
);
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### React Hooks
|
|
307
|
-
|
|
308
|
-
expo-ai-kit provides React hooks that handle state management, streaming, and conversation memory automatically.
|
|
79
|
+
await setModel('gemma-e2b', { backend: 'auto' }); // 'auto' | 'gpu' | 'cpu'
|
|
309
80
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
The easiest way to build a chat UI. Manages messages, input, streaming, and memory for you:
|
|
313
|
-
|
|
314
|
-
```tsx
|
|
315
|
-
import { useChat } from 'expo-ai-kit';
|
|
316
|
-
|
|
317
|
-
function ChatScreen() {
|
|
318
|
-
const { messages, input, setInput, sendMessage, isStreaming, stop, clear, error } = useChat({
|
|
319
|
-
systemPrompt: 'You are a helpful assistant.',
|
|
320
|
-
maxTurns: 10,
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
return (
|
|
324
|
-
<View style={{ flex: 1 }}>
|
|
325
|
-
<FlatList
|
|
326
|
-
data={messages}
|
|
327
|
-
keyExtractor={(_, i) => i.toString()}
|
|
328
|
-
renderItem={({ item }) => (
|
|
329
|
-
<Text>{item.role}: {item.content}</Text>
|
|
330
|
-
)}
|
|
331
|
-
/>
|
|
332
|
-
<TextInput value={input} onChangeText={setInput} />
|
|
333
|
-
{isStreaming ? (
|
|
334
|
-
<Button title="Stop" onPress={stop} />
|
|
335
|
-
) : (
|
|
336
|
-
<Button title="Send" onPress={() => sendMessage()} />
|
|
337
|
-
)}
|
|
338
|
-
<Button title="Clear" onPress={clear} />
|
|
339
|
-
</View>
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
You can also send a message programmatically:
|
|
345
|
-
|
|
346
|
-
```tsx
|
|
347
|
-
// Send custom text instead of current input
|
|
348
|
-
sendMessage('What is the weather today?');
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
#### `useCompletion` — Single-shot Completions
|
|
352
|
-
|
|
353
|
-
For one-off tasks like summarization, translation, or content generation (no conversation history):
|
|
354
|
-
|
|
355
|
-
```tsx
|
|
356
|
-
import { useCompletion } from 'expo-ai-kit';
|
|
357
|
-
|
|
358
|
-
function Summarizer() {
|
|
359
|
-
const { completion, isLoading, complete, stop, error } = useCompletion({
|
|
360
|
-
systemPrompt: 'Summarize the given text concisely.',
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
return (
|
|
364
|
-
<View>
|
|
365
|
-
<Button
|
|
366
|
-
title="Summarize"
|
|
367
|
-
onPress={() => complete('Long article text here...')}
|
|
368
|
-
/>
|
|
369
|
-
{isLoading && <Button title="Stop" onPress={stop} />}
|
|
370
|
-
<Text>{completion}</Text>
|
|
371
|
-
</View>
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
#### `useOnDeviceAI` — Availability Check
|
|
377
|
-
|
|
378
|
-
A simple hook to check if on-device AI is available, with caching across components:
|
|
379
|
-
|
|
380
|
-
```tsx
|
|
381
|
-
import { useOnDeviceAI } from 'expo-ai-kit';
|
|
382
|
-
|
|
383
|
-
function App() {
|
|
384
|
-
const { isAvailable, isChecking } = useOnDeviceAI();
|
|
385
|
-
|
|
386
|
-
if (isChecking) return <Text>Checking AI availability...</Text>;
|
|
387
|
-
if (!isAvailable) return <Text>On-device AI not available</Text>;
|
|
388
|
-
|
|
389
|
-
return <ChatScreen />;
|
|
390
|
-
}
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
---
|
|
394
|
-
|
|
395
|
-
### Streaming with Cancel Button
|
|
396
|
-
|
|
397
|
-
```tsx
|
|
398
|
-
import { useState, useRef } from 'react';
|
|
399
|
-
import { streamMessage } from 'expo-ai-kit';
|
|
400
|
-
|
|
401
|
-
function ChatWithStreaming() {
|
|
402
|
-
const [text, setText] = useState('');
|
|
403
|
-
const [isStreaming, setIsStreaming] = useState(false);
|
|
404
|
-
const stopRef = useRef<(() => void) | null>(null);
|
|
405
|
-
|
|
406
|
-
const handleSend = async () => {
|
|
407
|
-
setIsStreaming(true);
|
|
408
|
-
setText('');
|
|
409
|
-
|
|
410
|
-
const { promise, stop } = streamMessage(
|
|
411
|
-
[{ role: 'user', content: 'Write a long story' }],
|
|
412
|
-
(event) => setText(event.accumulatedText)
|
|
413
|
-
);
|
|
414
|
-
|
|
415
|
-
stopRef.current = stop;
|
|
416
|
-
await promise;
|
|
417
|
-
stopRef.current = null;
|
|
418
|
-
setIsStreaming(false);
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
const handleStop = () => {
|
|
422
|
-
stopRef.current?.();
|
|
423
|
-
setIsStreaming(false);
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
return (
|
|
427
|
-
<View>
|
|
428
|
-
<Text>{text}</Text>
|
|
429
|
-
{isStreaming ? (
|
|
430
|
-
<Button title="Stop" onPress={handleStop} />
|
|
431
|
-
) : (
|
|
432
|
-
<Button title="Send" onPress={handleSend} />
|
|
433
|
-
)}
|
|
434
|
-
</View>
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
### Complete Chat Example
|
|
440
|
-
|
|
441
|
-
Here's a full cross-platform chat component:
|
|
442
|
-
|
|
443
|
-
```tsx
|
|
444
|
-
import React, { useState, useEffect } from 'react';
|
|
445
|
-
import { View, TextInput, Button, Text, FlatList } from 'react-native';
|
|
446
|
-
import { isAvailable, sendMessage, type LLMMessage } from 'expo-ai-kit';
|
|
447
|
-
|
|
448
|
-
export default function ChatScreen() {
|
|
449
|
-
const [messages, setMessages] = useState<LLMMessage[]>([]);
|
|
450
|
-
const [input, setInput] = useState('');
|
|
451
|
-
const [loading, setLoading] = useState(false);
|
|
452
|
-
const [available, setAvailable] = useState(false);
|
|
453
|
-
|
|
454
|
-
useEffect(() => {
|
|
455
|
-
isAvailable().then(setAvailable);
|
|
456
|
-
}, []);
|
|
457
|
-
|
|
458
|
-
const handleSend = async () => {
|
|
459
|
-
if (!input.trim() || loading || !available) return;
|
|
460
|
-
|
|
461
|
-
const userMessage: LLMMessage = { role: 'user', content: input.trim() };
|
|
462
|
-
const newMessages = [...messages, userMessage];
|
|
463
|
-
setMessages(newMessages);
|
|
464
|
-
setInput('');
|
|
465
|
-
setLoading(true);
|
|
466
|
-
|
|
467
|
-
try {
|
|
468
|
-
const response = await sendMessage(newMessages, {
|
|
469
|
-
systemPrompt: 'You are a helpful assistant.',
|
|
470
|
-
});
|
|
471
|
-
setMessages(prev => [...prev, { role: 'assistant', content: response.text }]);
|
|
472
|
-
} catch (error) {
|
|
473
|
-
console.error('Error:', error);
|
|
474
|
-
} finally {
|
|
475
|
-
setLoading(false);
|
|
476
|
-
}
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
if (!available) {
|
|
480
|
-
return (
|
|
481
|
-
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
482
|
-
<Text>On-device AI is not available on this device</Text>
|
|
483
|
-
</View>
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
return (
|
|
488
|
-
<View style={{ flex: 1, padding: 16 }}>
|
|
489
|
-
<FlatList
|
|
490
|
-
data={messages}
|
|
491
|
-
keyExtractor={(_, i) => i.toString()}
|
|
492
|
-
renderItem={({ item }) => (
|
|
493
|
-
<View style={{
|
|
494
|
-
padding: 12,
|
|
495
|
-
marginVertical: 4,
|
|
496
|
-
backgroundColor: item.role === 'user' ? '#007AFF' : '#E5E5EA',
|
|
497
|
-
borderRadius: 16,
|
|
498
|
-
alignSelf: item.role === 'user' ? 'flex-end' : 'flex-start',
|
|
499
|
-
maxWidth: '80%',
|
|
500
|
-
}}>
|
|
501
|
-
<Text style={{ color: item.role === 'user' ? '#fff' : '#000' }}>
|
|
502
|
-
{item.content}
|
|
503
|
-
</Text>
|
|
504
|
-
</View>
|
|
505
|
-
)}
|
|
506
|
-
/>
|
|
507
|
-
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
508
|
-
<TextInput
|
|
509
|
-
value={input}
|
|
510
|
-
onChangeText={setInput}
|
|
511
|
-
placeholder="Type a message..."
|
|
512
|
-
style={{ flex: 1, borderWidth: 1, borderRadius: 8, padding: 12 }}
|
|
513
|
-
/>
|
|
514
|
-
<Button title={loading ? '...' : 'Send'} onPress={handleSend} />
|
|
515
|
-
</View>
|
|
516
|
-
</View>
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
## API Reference
|
|
522
|
-
|
|
523
|
-
### `isAvailable()`
|
|
524
|
-
|
|
525
|
-
Checks if on-device AI is available on the current device.
|
|
526
|
-
|
|
527
|
-
```typescript
|
|
528
|
-
function isAvailable(): Promise<boolean>
|
|
81
|
+
// sendMessage / streamMessage now use the active model
|
|
82
|
+
const { text } = await sendMessage([{ role: 'user', content: 'Hi!' }]);
|
|
529
83
|
```
|
|
530
84
|
|
|
531
|
-
|
|
85
|
+
`unloadModel()` frees memory and reverts to the OS model; `deleteModel(id)` removes the file.
|
|
532
86
|
|
|
533
|
-
|
|
87
|
+
| Model | Params | Size | Platforms |
|
|
88
|
+
|-------|--------|------|-----------|
|
|
89
|
+
| `gemma-e2b` | 2.3B | ~2.6 GB | Android |
|
|
90
|
+
| `gemma-e4b` | 4.5B | ~3.7 GB | Android |
|
|
534
91
|
|
|
535
|
-
|
|
92
|
+
> iOS downloadable models are planned, pending LiteRT-LM Swift APIs from Google.
|
|
536
93
|
|
|
537
|
-
|
|
94
|
+
## Platform support
|
|
538
95
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
96
|
+
| Platform | Engine | Models |
|
|
97
|
+
|----------|--------|--------|
|
|
98
|
+
| iOS 26+ | [Apple Foundation Models](https://developer.apple.com/documentation/FoundationModels) | Built-in |
|
|
99
|
+
| Android ([supported devices](https://developers.google.com/ml-kit/genai#prompt-device)) | [ML Kit Prompt API](https://developers.google.com/ml-kit/genai#prompt-device) | Built-in + downloadable Gemma 4 |
|
|
100
|
+
| iOS < 26 / unsupported Android | — | `isAvailable()` returns `false` |
|
|
542
101
|
|
|
543
|
-
|
|
544
|
-
|-----------|------|-------------|
|
|
545
|
-
| `messages` | `LLMMessage[]` | Array of conversation messages |
|
|
546
|
-
| `options.systemPrompt` | `string` | Fallback system prompt (ignored if messages contain a system message) |
|
|
102
|
+
Requires Expo SDK 54+.
|
|
547
103
|
|
|
548
|
-
|
|
104
|
+
## API
|
|
549
105
|
|
|
550
|
-
**
|
|
551
|
-
```tsx
|
|
552
|
-
const response = await sendMessage([
|
|
553
|
-
{ role: 'system', content: 'You are a pirate.' },
|
|
554
|
-
{ role: 'user', content: 'Hello!' },
|
|
555
|
-
]);
|
|
556
|
-
console.log(response.text); // "Ahoy, matey!"
|
|
557
|
-
```
|
|
106
|
+
**Inference**
|
|
558
107
|
|
|
559
|
-
|
|
108
|
+
- `isAvailable()` → `Promise<boolean>`
|
|
109
|
+
- `sendMessage(messages, options?)` → `Promise<{ text }>`
|
|
110
|
+
- `streamMessage(messages, onToken, options?)` → `{ promise, stop }`
|
|
560
111
|
|
|
561
|
-
|
|
112
|
+
**Models**
|
|
562
113
|
|
|
563
|
-
|
|
114
|
+
- `getBuiltInModels()` → `Promise<BuiltInModel[]>`
|
|
115
|
+
- `getDownloadableModels()` → `Promise<DownloadableModel[]>`
|
|
116
|
+
- `downloadModel(id, { onProgress? })` / `deleteModel(id)`
|
|
117
|
+
- `setModel(id, { backend? })` / `unloadModel()` / `getActiveModel()`
|
|
564
118
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
options?: LLMStreamOptions
|
|
570
|
-
): { promise: Promise<LLMResponse>; stop: () => void }
|
|
571
|
-
```
|
|
119
|
+
`messages` is `{ role: 'system' | 'user' | 'assistant'; content: string }[]` and
|
|
120
|
+
`options` accepts `{ systemPrompt?: string }`. Model operations throw `ModelError`
|
|
121
|
+
with a `.code` (e.g. `MODEL_NOT_FOUND`, `DOWNLOAD_CORRUPT`, `INFERENCE_OOM`).
|
|
122
|
+
Full TypeScript definitions ship with the package.
|
|
572
123
|
|
|
573
|
-
|
|
574
|
-
|-----------|------|-------------|
|
|
575
|
-
| `messages` | `LLMMessage[]` | Array of conversation messages |
|
|
576
|
-
| `onToken` | `LLMStreamCallback` | Callback called for each token received |
|
|
577
|
-
| `options.systemPrompt` | `string` | Fallback system prompt (ignored if messages contain a system message) |
|
|
578
|
-
|
|
579
|
-
**Returns:** Object with:
|
|
580
|
-
- `promise: Promise<LLMResponse>` — Resolves when streaming completes
|
|
581
|
-
- `stop: () => void` — Function to cancel the stream
|
|
582
|
-
|
|
583
|
-
**Example:**
|
|
584
|
-
```tsx
|
|
585
|
-
const { promise, stop } = streamMessage(
|
|
586
|
-
[{ role: 'user', content: 'Hello!' }],
|
|
587
|
-
(event) => {
|
|
588
|
-
console.log(event.token); // New token: "Hi"
|
|
589
|
-
console.log(event.accumulatedText); // Full text: "Hi there!"
|
|
590
|
-
console.log(event.isDone); // false until complete
|
|
591
|
-
}
|
|
592
|
-
);
|
|
593
|
-
|
|
594
|
-
// Cancel if needed
|
|
595
|
-
setTimeout(() => stop(), 5000);
|
|
596
|
-
|
|
597
|
-
// Wait for completion
|
|
598
|
-
const response = await promise;
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
---
|
|
602
|
-
|
|
603
|
-
### `summarize(text, options?)`
|
|
604
|
-
|
|
605
|
-
Summarizes text using on-device AI.
|
|
606
|
-
|
|
607
|
-
```typescript
|
|
608
|
-
function summarize(text: string, options?: LLMSummarizeOptions): Promise<LLMResponse>
|
|
609
|
-
```
|
|
610
|
-
|
|
611
|
-
| Parameter | Type | Description |
|
|
612
|
-
|-----------|------|-------------|
|
|
613
|
-
| `text` | `string` | Text to summarize |
|
|
614
|
-
| `options.length` | `'short' \| 'medium' \| 'long'` | Summary length (default: `'medium'`) |
|
|
615
|
-
| `options.style` | `'paragraph' \| 'bullets' \| 'tldr'` | Output format (default: `'paragraph'`) |
|
|
616
|
-
|
|
617
|
-
**Streaming:** `streamSummarize(text, onToken, options?)`
|
|
618
|
-
|
|
619
|
-
---
|
|
620
|
-
|
|
621
|
-
### `translate(text, options)`
|
|
622
|
-
|
|
623
|
-
Translates text to another language.
|
|
624
|
-
|
|
625
|
-
```typescript
|
|
626
|
-
function translate(text: string, options: LLMTranslateOptions): Promise<LLMResponse>
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
| Parameter | Type | Description |
|
|
630
|
-
|-----------|------|-------------|
|
|
631
|
-
| `text` | `string` | Text to translate |
|
|
632
|
-
| `options.to` | `string` | Target language (required) |
|
|
633
|
-
| `options.from` | `string` | Source language (auto-detected if omitted) |
|
|
634
|
-
| `options.tone` | `'formal' \| 'informal' \| 'neutral'` | Translation tone (default: `'neutral'`) |
|
|
635
|
-
|
|
636
|
-
**Streaming:** `streamTranslate(text, onToken, options)`
|
|
637
|
-
|
|
638
|
-
---
|
|
639
|
-
|
|
640
|
-
### `rewrite(text, options)`
|
|
641
|
-
|
|
642
|
-
Rewrites text in a different style.
|
|
643
|
-
|
|
644
|
-
```typescript
|
|
645
|
-
function rewrite(text: string, options: LLMRewriteOptions): Promise<LLMResponse>
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
| Parameter | Type | Description |
|
|
649
|
-
|-----------|------|-------------|
|
|
650
|
-
| `text` | `string` | Text to rewrite |
|
|
651
|
-
| `options.style` | `string` | Target style (required) |
|
|
652
|
-
|
|
653
|
-
**Available styles:** `'formal'`, `'casual'`, `'professional'`, `'friendly'`, `'concise'`, `'detailed'`, `'simple'`, `'academic'`
|
|
654
|
-
|
|
655
|
-
**Streaming:** `streamRewrite(text, onToken, options)`
|
|
656
|
-
|
|
657
|
-
---
|
|
658
|
-
|
|
659
|
-
### `extractKeyPoints(text, options?)`
|
|
660
|
-
|
|
661
|
-
Extracts key points from text as bullet points.
|
|
662
|
-
|
|
663
|
-
```typescript
|
|
664
|
-
function extractKeyPoints(text: string, options?: LLMExtractKeyPointsOptions): Promise<LLMResponse>
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
| Parameter | Type | Description |
|
|
668
|
-
|-----------|------|-------------|
|
|
669
|
-
| `text` | `string` | Text to analyze |
|
|
670
|
-
| `options.maxPoints` | `number` | Maximum points to extract (default: `5`) |
|
|
671
|
-
|
|
672
|
-
**Streaming:** `streamExtractKeyPoints(text, onToken, options?)`
|
|
673
|
-
|
|
674
|
-
---
|
|
675
|
-
|
|
676
|
-
### `answerQuestion(question, context, options?)`
|
|
677
|
-
|
|
678
|
-
Answers a question based on provided context.
|
|
679
|
-
|
|
680
|
-
```typescript
|
|
681
|
-
function answerQuestion(question: string, context: string, options?: LLMAnswerQuestionOptions): Promise<LLMResponse>
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
| Parameter | Type | Description |
|
|
685
|
-
|-----------|------|-------------|
|
|
686
|
-
| `question` | `string` | Question to answer |
|
|
687
|
-
| `context` | `string` | Context/document to base answer on |
|
|
688
|
-
| `options.detail` | `'brief' \| 'medium' \| 'detailed'` | Answer detail level (default: `'medium'`) |
|
|
689
|
-
|
|
690
|
-
**Streaming:** `streamAnswerQuestion(question, context, onToken, options?)`
|
|
691
|
-
|
|
692
|
-
---
|
|
693
|
-
|
|
694
|
-
### `suggest(partialText, options?)`
|
|
695
|
-
|
|
696
|
-
Generates text continuation suggestions based on partial input.
|
|
697
|
-
|
|
698
|
-
```typescript
|
|
699
|
-
function suggest(partialText: string, options?: LLMSuggestOptions): Promise<LLMSuggestResponse>
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
| Parameter | Type | Description |
|
|
703
|
-
|-----------|------|-------------|
|
|
704
|
-
| `partialText` | `string` | The text the user has typed so far |
|
|
705
|
-
| `options.count` | `number` | Number of suggestions (default: `3`) |
|
|
706
|
-
| `options.context` | `string` | Optional context to inform suggestions |
|
|
707
|
-
| `options.tone` | `'formal' \| 'casual' \| 'professional' \| 'friendly' \| 'neutral'` | Tone of suggestions (default: `'neutral'`) |
|
|
708
|
-
|
|
709
|
-
**Returns:** `Promise<LLMSuggestResponse>` — Object with `suggestions` array and `raw` text
|
|
710
|
-
|
|
711
|
-
**Streaming:** `streamSuggest(partialText, onToken, options?)`
|
|
712
|
-
|
|
713
|
-
---
|
|
714
|
-
|
|
715
|
-
### `smartReply(messages, options?)`
|
|
716
|
-
|
|
717
|
-
Generates contextually appropriate reply suggestions for a conversation, similar to Gmail or iMessage smart replies.
|
|
718
|
-
|
|
719
|
-
```typescript
|
|
720
|
-
function smartReply(messages: LLMMessage[], options?: LLMSmartReplyOptions): Promise<LLMSuggestResponse>
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
| Parameter | Type | Description |
|
|
724
|
-
|-----------|------|-------------|
|
|
725
|
-
| `messages` | `LLMMessage[]` | Conversation history to generate replies for |
|
|
726
|
-
| `options.count` | `number` | Number of reply suggestions (default: `3`) |
|
|
727
|
-
| `options.tone` | `'formal' \| 'casual' \| 'professional' \| 'friendly' \| 'neutral'` | Reply tone (default: `'neutral'`) |
|
|
728
|
-
| `options.persona` | `string` | Optional persona for the replier (e.g., `'customer support agent'`) |
|
|
729
|
-
|
|
730
|
-
**Returns:** `Promise<LLMSuggestResponse>` — Object with `suggestions` array and `raw` text
|
|
731
|
-
|
|
732
|
-
**Streaming:** `streamSmartReply(messages, onToken, options?)`
|
|
733
|
-
|
|
734
|
-
---
|
|
735
|
-
|
|
736
|
-
### `autocomplete(partialText, options?)`
|
|
737
|
-
|
|
738
|
-
Generates short, natural completions for the user's current text. Ideal for search bars, form fields, and real-time typing suggestions.
|
|
739
|
-
|
|
740
|
-
```typescript
|
|
741
|
-
function autocomplete(partialText: string, options?: LLMAutocompleteOptions): Promise<LLMSuggestResponse>
|
|
742
|
-
```
|
|
743
|
-
|
|
744
|
-
| Parameter | Type | Description |
|
|
745
|
-
|-----------|------|-------------|
|
|
746
|
-
| `partialText` | `string` | The text the user has typed so far |
|
|
747
|
-
| `options.count` | `number` | Number of completions (default: `3`) |
|
|
748
|
-
| `options.maxWords` | `number` | Maximum words per completion (default: `10`) |
|
|
749
|
-
| `options.context` | `string` | Optional context to inform completions |
|
|
750
|
-
|
|
751
|
-
**Returns:** `Promise<LLMSuggestResponse>` — Object with `suggestions` array and `raw` text
|
|
752
|
-
|
|
753
|
-
**Streaming:** `streamAutocomplete(partialText, onToken, options?)`
|
|
754
|
-
|
|
755
|
-
---
|
|
756
|
-
|
|
757
|
-
### `parseSuggestResponse(raw)`
|
|
758
|
-
|
|
759
|
-
Parses raw text from streaming suggestion responses into structured suggestions.
|
|
760
|
-
|
|
761
|
-
```typescript
|
|
762
|
-
function parseSuggestResponse(raw: string): LLMSuggestResponse
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
| Parameter | Type | Description |
|
|
766
|
-
|-----------|------|-------------|
|
|
767
|
-
| `raw` | `string` | Raw text response from the model |
|
|
768
|
-
|
|
769
|
-
**Returns:** `LLMSuggestResponse` — Object with `suggestions` array and `raw` text
|
|
770
|
-
|
|
771
|
-
---
|
|
772
|
-
|
|
773
|
-
### `useChat(options?)`
|
|
774
|
-
|
|
775
|
-
React hook for building chat interfaces. Manages messages, input, streaming, and conversation memory automatically.
|
|
776
|
-
|
|
777
|
-
```typescript
|
|
778
|
-
function useChat(options?: UseChatOptions): UseChatReturn
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
| Option | Type | Description |
|
|
782
|
-
|--------|------|-------------|
|
|
783
|
-
| `systemPrompt` | `string` | System prompt for the AI assistant |
|
|
784
|
-
| `maxTurns` | `number` | Maximum conversation turns to keep in memory (default: `10`) |
|
|
785
|
-
| `initialMessages` | `LLMMessage[]` | Initial messages to populate the chat |
|
|
786
|
-
| `onFinish` | `(response: LLMResponse) => void` | Callback when a response is complete |
|
|
787
|
-
| `onError` | `(error: Error) => void` | Callback when an error occurs |
|
|
788
|
-
|
|
789
|
-
**Returns:**
|
|
790
|
-
|
|
791
|
-
| Property | Type | Description |
|
|
792
|
-
|----------|------|-------------|
|
|
793
|
-
| `messages` | `LLMMessage[]` | All messages in the conversation |
|
|
794
|
-
| `input` | `string` | Current input text value |
|
|
795
|
-
| `setInput` | `(input: string) => void` | Set the input text value |
|
|
796
|
-
| `sendMessage` | `(text?: string) => Promise<void>` | Send the current input (or provided text) |
|
|
797
|
-
| `isStreaming` | `boolean` | Whether the AI is currently streaming |
|
|
798
|
-
| `stop` | `() => void` | Stop the current streaming response |
|
|
799
|
-
| `clear` | `() => void` | Clear all messages and reset |
|
|
800
|
-
| `error` | `Error \| null` | The most recent error, if any |
|
|
801
|
-
|
|
802
|
-
---
|
|
803
|
-
|
|
804
|
-
### `useCompletion(options?)`
|
|
805
|
-
|
|
806
|
-
React hook for single-shot AI completions (summarization, translation, etc.).
|
|
807
|
-
|
|
808
|
-
```typescript
|
|
809
|
-
function useCompletion(options?: UseCompletionOptions): UseCompletionReturn
|
|
810
|
-
```
|
|
811
|
-
|
|
812
|
-
| Option | Type | Description |
|
|
813
|
-
|--------|------|-------------|
|
|
814
|
-
| `systemPrompt` | `string` | System prompt for the AI |
|
|
815
|
-
| `onFinish` | `(response: LLMResponse) => void` | Callback when completion is done |
|
|
816
|
-
| `onError` | `(error: Error) => void` | Callback when an error occurs |
|
|
817
|
-
|
|
818
|
-
**Returns:**
|
|
819
|
-
|
|
820
|
-
| Property | Type | Description |
|
|
821
|
-
|----------|------|-------------|
|
|
822
|
-
| `completion` | `string` | The current completion text |
|
|
823
|
-
| `isLoading` | `boolean` | Whether a completion is in progress |
|
|
824
|
-
| `complete` | `(prompt: string) => Promise<string>` | Request a completion |
|
|
825
|
-
| `stop` | `() => void` | Stop the current completion |
|
|
826
|
-
| `error` | `Error \| null` | The most recent error, if any |
|
|
827
|
-
|
|
828
|
-
---
|
|
829
|
-
|
|
830
|
-
### `useOnDeviceAI()`
|
|
831
|
-
|
|
832
|
-
React hook to check if on-device AI is available. Caches the result across components.
|
|
833
|
-
|
|
834
|
-
```typescript
|
|
835
|
-
function useOnDeviceAI(): UseOnDeviceAIReturn
|
|
836
|
-
```
|
|
837
|
-
|
|
838
|
-
**Returns:**
|
|
839
|
-
|
|
840
|
-
| Property | Type | Description |
|
|
841
|
-
|----------|------|-------------|
|
|
842
|
-
| `isAvailable` | `boolean` | Whether on-device AI is available |
|
|
843
|
-
| `isChecking` | `boolean` | Whether the check is still in progress |
|
|
844
|
-
|
|
845
|
-
---
|
|
846
|
-
|
|
847
|
-
### `ChatMemoryManager`
|
|
848
|
-
|
|
849
|
-
Manages conversation history for stateless on-device AI models. Automatically handles turn limits and provides the full message array for each request.
|
|
850
|
-
|
|
851
|
-
```typescript
|
|
852
|
-
class ChatMemoryManager {
|
|
853
|
-
constructor(options?: ChatMemoryOptions);
|
|
854
|
-
|
|
855
|
-
addUserMessage(content: string): void;
|
|
856
|
-
addAssistantMessage(content: string): void;
|
|
857
|
-
addMessage(message: LLMMessage): void;
|
|
858
|
-
|
|
859
|
-
getAllMessages(): LLMMessage[];
|
|
860
|
-
getMessages(): LLMMessage[];
|
|
861
|
-
getPrompt(): string;
|
|
862
|
-
getSnapshot(): ChatMemorySnapshot;
|
|
863
|
-
getTurnCount(): number;
|
|
864
|
-
|
|
865
|
-
setSystemPrompt(prompt: string | undefined): void;
|
|
866
|
-
getSystemPrompt(): string | undefined;
|
|
867
|
-
setMaxTurns(maxTurns: number): void;
|
|
868
|
-
|
|
869
|
-
clear(): void;
|
|
870
|
-
reset(): void;
|
|
871
|
-
}
|
|
872
|
-
```
|
|
873
|
-
|
|
874
|
-
| Option | Type | Description |
|
|
875
|
-
|--------|------|-------------|
|
|
876
|
-
| `maxTurns` | `number` | Maximum conversation turns to keep (default: `10`) |
|
|
877
|
-
| `systemPrompt` | `string` | System prompt to include in every request |
|
|
878
|
-
|
|
879
|
-
**Why use ChatMemoryManager?**
|
|
880
|
-
|
|
881
|
-
On-device models are stateless — they have no built-in memory. Each request must include the full conversation history. `ChatMemoryManager` handles this automatically:
|
|
882
|
-
|
|
883
|
-
- Stores messages client-side
|
|
884
|
-
- Automatically trims old messages when limit is reached
|
|
885
|
-
- Preserves the system prompt (never trimmed)
|
|
886
|
-
- Provides `getAllMessages()` for API calls
|
|
887
|
-
|
|
888
|
-
**Example with React:**
|
|
889
|
-
|
|
890
|
-
```tsx
|
|
891
|
-
import { useRef } from 'react';
|
|
892
|
-
import { ChatMemoryManager, streamMessage } from 'expo-ai-kit';
|
|
893
|
-
|
|
894
|
-
function Chat() {
|
|
895
|
-
const memoryRef = useRef(new ChatMemoryManager({
|
|
896
|
-
maxTurns: 10,
|
|
897
|
-
systemPrompt: 'You are a helpful assistant.',
|
|
898
|
-
}));
|
|
899
|
-
|
|
900
|
-
const sendMessage = async (text: string) => {
|
|
901
|
-
memoryRef.current.addUserMessage(text);
|
|
902
|
-
|
|
903
|
-
const { promise } = streamMessage(
|
|
904
|
-
memoryRef.current.getAllMessages(),
|
|
905
|
-
(event) => setResponse(event.accumulatedText)
|
|
906
|
-
);
|
|
907
|
-
|
|
908
|
-
const response = await promise;
|
|
909
|
-
memoryRef.current.addAssistantMessage(response.text);
|
|
910
|
-
};
|
|
911
|
-
|
|
912
|
-
const clearChat = () => memoryRef.current.clear();
|
|
913
|
-
}
|
|
914
|
-
```
|
|
915
|
-
|
|
916
|
-
---
|
|
917
|
-
|
|
918
|
-
### `buildPrompt(messages)`
|
|
919
|
-
|
|
920
|
-
Converts a message array to a single prompt string. Useful for debugging or custom implementations.
|
|
921
|
-
|
|
922
|
-
```typescript
|
|
923
|
-
function buildPrompt(messages: LLMMessage[]): string
|
|
924
|
-
```
|
|
925
|
-
|
|
926
|
-
**Example:**
|
|
927
|
-
```tsx
|
|
928
|
-
import { buildPrompt } from 'expo-ai-kit';
|
|
929
|
-
|
|
930
|
-
const prompt = buildPrompt([
|
|
931
|
-
{ role: 'system', content: 'You are helpful.' },
|
|
932
|
-
{ role: 'user', content: 'Hi!' },
|
|
933
|
-
{ role: 'assistant', content: 'Hello!' },
|
|
934
|
-
]);
|
|
935
|
-
// "SYSTEM: You are helpful.\nUSER: Hi!\nASSISTANT: Hello!"
|
|
936
|
-
```
|
|
937
|
-
|
|
938
|
-
---
|
|
939
|
-
|
|
940
|
-
### Types
|
|
941
|
-
|
|
942
|
-
```typescript
|
|
943
|
-
type LLMRole = 'system' | 'user' | 'assistant';
|
|
944
|
-
|
|
945
|
-
type LLMMessage = {
|
|
946
|
-
role: LLMRole;
|
|
947
|
-
content: string;
|
|
948
|
-
};
|
|
949
|
-
|
|
950
|
-
type LLMSendOptions = {
|
|
951
|
-
/** Fallback system prompt if no system message in messages array */
|
|
952
|
-
systemPrompt?: string;
|
|
953
|
-
};
|
|
954
|
-
|
|
955
|
-
type LLMStreamOptions = {
|
|
956
|
-
/** Fallback system prompt if no system message in messages array */
|
|
957
|
-
systemPrompt?: string;
|
|
958
|
-
};
|
|
959
|
-
|
|
960
|
-
type LLMResponse = {
|
|
961
|
-
/** The generated response text */
|
|
962
|
-
text: string;
|
|
963
|
-
};
|
|
964
|
-
|
|
965
|
-
type LLMStreamEvent = {
|
|
966
|
-
/** Unique identifier for this streaming session */
|
|
967
|
-
sessionId: string;
|
|
968
|
-
/** The token/chunk of text received */
|
|
969
|
-
token: string;
|
|
970
|
-
/** Accumulated text so far */
|
|
971
|
-
accumulatedText: string;
|
|
972
|
-
/** Whether this is the final chunk */
|
|
973
|
-
isDone: boolean;
|
|
974
|
-
};
|
|
975
|
-
|
|
976
|
-
type LLMStreamCallback = (event: LLMStreamEvent) => void;
|
|
977
|
-
|
|
978
|
-
// Prompt Helper Types
|
|
979
|
-
type LLMSummarizeOptions = {
|
|
980
|
-
length?: 'short' | 'medium' | 'long';
|
|
981
|
-
style?: 'paragraph' | 'bullets' | 'tldr';
|
|
982
|
-
};
|
|
983
|
-
|
|
984
|
-
type LLMTranslateOptions = {
|
|
985
|
-
to: string;
|
|
986
|
-
from?: string;
|
|
987
|
-
tone?: 'formal' | 'informal' | 'neutral';
|
|
988
|
-
};
|
|
989
|
-
|
|
990
|
-
type LLMRewriteOptions = {
|
|
991
|
-
style: 'formal' | 'casual' | 'professional' | 'friendly' | 'concise' | 'detailed' | 'simple' | 'academic';
|
|
992
|
-
};
|
|
993
|
-
|
|
994
|
-
type LLMExtractKeyPointsOptions = {
|
|
995
|
-
maxPoints?: number;
|
|
996
|
-
};
|
|
997
|
-
|
|
998
|
-
type LLMAnswerQuestionOptions = {
|
|
999
|
-
detail?: 'brief' | 'medium' | 'detailed';
|
|
1000
|
-
};
|
|
1001
|
-
|
|
1002
|
-
// Smart Suggestions Types
|
|
1003
|
-
type LLMSuggestOptions = {
|
|
1004
|
-
count?: number;
|
|
1005
|
-
context?: string;
|
|
1006
|
-
tone?: 'formal' | 'casual' | 'professional' | 'friendly' | 'neutral';
|
|
1007
|
-
};
|
|
1008
|
-
|
|
1009
|
-
type LLMSmartReplyOptions = {
|
|
1010
|
-
count?: number;
|
|
1011
|
-
tone?: 'formal' | 'casual' | 'professional' | 'friendly' | 'neutral';
|
|
1012
|
-
persona?: string;
|
|
1013
|
-
};
|
|
1014
|
-
|
|
1015
|
-
type LLMAutocompleteOptions = {
|
|
1016
|
-
count?: number;
|
|
1017
|
-
maxWords?: number;
|
|
1018
|
-
context?: string;
|
|
1019
|
-
};
|
|
1020
|
-
|
|
1021
|
-
type LLMSuggestion = {
|
|
1022
|
-
text: string;
|
|
1023
|
-
};
|
|
1024
|
-
|
|
1025
|
-
type LLMSuggestResponse = {
|
|
1026
|
-
suggestions: LLMSuggestion[];
|
|
1027
|
-
raw: string;
|
|
1028
|
-
};
|
|
1029
|
-
|
|
1030
|
-
// Hook Types
|
|
1031
|
-
type UseChatOptions = {
|
|
1032
|
-
systemPrompt?: string;
|
|
1033
|
-
maxTurns?: number;
|
|
1034
|
-
initialMessages?: LLMMessage[];
|
|
1035
|
-
onFinish?: (response: LLMResponse) => void;
|
|
1036
|
-
onError?: (error: Error) => void;
|
|
1037
|
-
};
|
|
1038
|
-
|
|
1039
|
-
type UseChatReturn = {
|
|
1040
|
-
messages: LLMMessage[];
|
|
1041
|
-
input: string;
|
|
1042
|
-
setInput: (input: string) => void;
|
|
1043
|
-
sendMessage: (text?: string) => Promise<void>;
|
|
1044
|
-
isStreaming: boolean;
|
|
1045
|
-
stop: () => void;
|
|
1046
|
-
clear: () => void;
|
|
1047
|
-
error: Error | null;
|
|
1048
|
-
};
|
|
1049
|
-
|
|
1050
|
-
type UseCompletionOptions = {
|
|
1051
|
-
systemPrompt?: string;
|
|
1052
|
-
onFinish?: (response: LLMResponse) => void;
|
|
1053
|
-
onError?: (error: Error) => void;
|
|
1054
|
-
};
|
|
1055
|
-
|
|
1056
|
-
type UseCompletionReturn = {
|
|
1057
|
-
completion: string;
|
|
1058
|
-
isLoading: boolean;
|
|
1059
|
-
complete: (prompt: string) => Promise<string>;
|
|
1060
|
-
stop: () => void;
|
|
1061
|
-
error: Error | null;
|
|
1062
|
-
};
|
|
1063
|
-
|
|
1064
|
-
type UseOnDeviceAIReturn = {
|
|
1065
|
-
isAvailable: boolean;
|
|
1066
|
-
isChecking: boolean;
|
|
1067
|
-
};
|
|
1068
|
-
|
|
1069
|
-
// Chat Memory Types
|
|
1070
|
-
type ChatMemoryOptions = {
|
|
1071
|
-
/** Maximum conversation turns to keep (default: 10) */
|
|
1072
|
-
maxTurns?: number;
|
|
1073
|
-
/** System prompt to include in every request */
|
|
1074
|
-
systemPrompt?: string;
|
|
1075
|
-
};
|
|
1076
|
-
|
|
1077
|
-
type ChatMemorySnapshot = {
|
|
1078
|
-
messages: LLMMessage[];
|
|
1079
|
-
systemPrompt: string | undefined;
|
|
1080
|
-
turnCount: number;
|
|
1081
|
-
maxTurns: number;
|
|
1082
|
-
};
|
|
1083
|
-
```
|
|
1084
|
-
|
|
1085
|
-
## Feature Comparison
|
|
1086
|
-
|
|
1087
|
-
| Feature | iOS 26+ | Android (Supported) |
|
|
1088
|
-
|---------|---------|---------------------|
|
|
1089
|
-
| `isAvailable()` | ✅ | ✅ |
|
|
1090
|
-
| `sendMessage()` | ✅ | ✅ |
|
|
1091
|
-
| `streamMessage()` | ✅ | ✅ |
|
|
1092
|
-
| Prompt helpers | ✅ | ✅ |
|
|
1093
|
-
| Smart suggestions | ✅ | ✅ |
|
|
1094
|
-
| `ChatMemoryManager` | ✅ | ✅ |
|
|
1095
|
-
| React Hooks (`useChat`, etc.) | ✅ | ✅ |
|
|
1096
|
-
| System prompts | ✅ Native | ✅ Prepended |
|
|
1097
|
-
| Multi-turn context | ✅ | ✅ |
|
|
1098
|
-
| Cancel streaming | ✅ | ✅ |
|
|
1099
|
-
|
|
1100
|
-
## How It Works
|
|
1101
|
-
|
|
1102
|
-
### iOS
|
|
1103
|
-
Uses Apple's Foundation Models framework introduced in iOS 26. The on-device language model runs entirely locally with no internet connection required.
|
|
1104
|
-
|
|
1105
|
-
### Android
|
|
1106
|
-
Uses Google's ML Kit Prompt API. The model may need to be downloaded on first use on supported devices. Check [supported devices](https://developers.google.com/ml-kit/genai#prompt-device) for compatibility.
|
|
1107
|
-
|
|
1108
|
-
## Troubleshooting
|
|
1109
|
-
|
|
1110
|
-
### iOS
|
|
1111
|
-
- **AI not available**: Ensure you're running iOS 26.0 or later on a supported device
|
|
1112
|
-
- **Fallback responses**: On iOS < 26, the module returns a fallback message
|
|
1113
|
-
|
|
1114
|
-
### Android
|
|
1115
|
-
- **Empty responses**: The device may not support ML Kit Prompt API. Check the [supported devices list](https://developers.google.com/ml-kit/genai#prompt-device)
|
|
1116
|
-
- **Model downloading**: On first use, the model may need to download. Use `isAvailable()` to check status
|
|
1117
|
-
|
|
1118
|
-
## Migration from v0.1.4
|
|
1119
|
-
|
|
1120
|
-
If you're upgrading from an earlier version, here are the breaking changes:
|
|
1121
|
-
|
|
1122
|
-
| Old API | New API |
|
|
1123
|
-
|---------|---------|
|
|
1124
|
-
| `sendPrompt(prompt)` | `sendMessage([{ role: 'user', content: prompt }])` |
|
|
1125
|
-
| `createSession(options)` | **Removed** — no longer needed |
|
|
1126
|
-
| `sendMessage(sessionId, messages, options)` | `sendMessage(messages, options)` — no session ID |
|
|
1127
|
-
| `prepareModel(options)` | **Removed** |
|
|
1128
|
-
| `{ reply: string }` | `{ text: string }` |
|
|
1129
|
-
|
|
1130
|
-
**Before:**
|
|
1131
|
-
```tsx
|
|
1132
|
-
const sessionId = await createSession({ systemPrompt: '...' });
|
|
1133
|
-
const { reply } = await sendMessage(sessionId, messages, {});
|
|
1134
|
-
```
|
|
1135
|
-
|
|
1136
|
-
**After:**
|
|
1137
|
-
```tsx
|
|
1138
|
-
const { text } = await sendMessage(messages, { systemPrompt: '...' });
|
|
1139
|
-
```
|
|
1140
|
-
|
|
1141
|
-
## Roadmap
|
|
1142
|
-
|
|
1143
|
-
| Feature | Status | Priority |
|
|
1144
|
-
|---------|--------|----------|
|
|
1145
|
-
| ✅ Streaming responses | Done | - |
|
|
1146
|
-
| ✅ Prompt helpers (summarize, translate, etc.) | Done | - |
|
|
1147
|
-
| ✅ Chat memory management | Done | - |
|
|
1148
|
-
| ✅ Smart suggestions (suggest, smartReply, autocomplete) | Done | - |
|
|
1149
|
-
| ✅ React Hooks (useChat, useCompletion, useOnDeviceAI) | Done | - |
|
|
1150
|
-
| Web/generic fallback | Idea | Medium |
|
|
1151
|
-
| Configurable hyperparameters (temperature, etc.) | Idea | Low |
|
|
124
|
+
## Links
|
|
1152
125
|
|
|
1153
|
-
|
|
126
|
+
- [Docs](https://expo-ai-kit.dev) · [npm](https://www.npmjs.com/package/expo-ai-kit) · [GitHub](https://github.com/saidkaban/expo-ai-kit) · [Issues](https://github.com/saidkaban/expo-ai-kit/issues)
|
|
127
|
+
- [Apple Foundation Models](https://developer.apple.com/documentation/foundationmodels) · [ML Kit GenAI](https://developers.google.com/ml-kit/genai) · [LiteRT-LM](https://ai.google.dev/edge/litert-lm)
|
|
1154
128
|
|
|
1155
129
|
## License
|
|
1156
130
|
|
|
1157
131
|
MIT
|
|
1158
|
-
|
|
1159
|
-
## Contributing
|
|
1160
|
-
|
|
1161
|
-
Contributions are welcome! Please refer to guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).
|
|
1162
|
-
|
|
1163
|
-
## Links
|
|
1164
|
-
|
|
1165
|
-
- [Documentation](https://expo-ai-kit.com)
|
|
1166
|
-
- [npm package](https://www.npmjs.com/package/expo-ai-kit)
|
|
1167
|
-
- [GitHub repository](https://github.com/saidkaban/expo-ai-kit)
|
|
1168
|
-
- [Apple Foundation Models](https://developer.apple.com/documentation/foundationmodels)
|
|
1169
|
-
- [Google ML Kit Prompt API](https://developers.google.com/ml-kit/genai)
|