expo-ai-kit 0.1.13 β 0.1.14
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 +101 -98
- package/android/src/main/java/expo/modules/aikit/ExpoAiKitModule.kt +16 -5
- package/android/src/main/java/expo/modules/aikit/PromptApiClient.kt +34 -23
- package/build/ExpoAiKitModule.d.ts +2 -11
- package/build/ExpoAiKitModule.d.ts.map +1 -1
- package/build/ExpoAiKitModule.js.map +1 -1
- package/build/index.d.ts +41 -11
- package/build/index.d.ts.map +1 -1
- package/build/index.js +55 -16
- package/build/index.js.map +1 -1
- package/build/types.d.ts +22 -4
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/ios/ExpoAiKit.podspec +1 -1
- package/ios/ExpoAiKitModule.swift +17 -90
- package/package.json +1 -1
- package/src/ExpoAiKitModule.ts +6 -10
- package/src/__tests__/index.test.js +124 -0
- package/src/index.ts +63 -21
- package/src/types.ts +24 -5
package/README.md
CHANGED
|
@@ -18,17 +18,17 @@ On-device AI for Expo apps. Run language models locallyβno API keys, no cloud,
|
|
|
18
18
|
|
|
19
19
|
| Platform | Fallback Behavior |
|
|
20
20
|
|----------|-------------------|
|
|
21
|
-
| iOS < 26 | Returns
|
|
21
|
+
| iOS < 26 | Returns fallback message |
|
|
22
22
|
| Android (unsupported devices) | Returns empty string |
|
|
23
23
|
|
|
24
24
|
## Features
|
|
25
25
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
26
|
+
- **Privacy-first** β All inference happens on-device; no data leaves the user's device
|
|
27
|
+
- **Zero latency** β No network round-trips required
|
|
28
|
+
- **Free forever** β No API costs, rate limits, or subscriptions
|
|
29
|
+
- **Native performance** β Built on Apple Foundation Models (iOS) and Google ML Kit Prompt API (Android)
|
|
30
|
+
- **Multi-turn conversations** β Full conversation context support
|
|
31
|
+
- **Simple API** β Just 2 functions: `isAvailable()` and `sendMessage()`
|
|
32
32
|
|
|
33
33
|
## Requirements
|
|
34
34
|
|
|
@@ -68,25 +68,27 @@ For Android, ensure your `app.json` includes the minimum SDK version:
|
|
|
68
68
|
## Quick Start
|
|
69
69
|
|
|
70
70
|
```tsx
|
|
71
|
-
import { isAvailable,
|
|
71
|
+
import { isAvailable, sendMessage } from 'expo-ai-kit';
|
|
72
72
|
|
|
73
73
|
// Check if on-device AI is available
|
|
74
74
|
const available = await isAvailable();
|
|
75
75
|
|
|
76
76
|
if (available) {
|
|
77
|
-
const response = await
|
|
78
|
-
|
|
77
|
+
const response = await sendMessage([
|
|
78
|
+
{ role: 'user', content: 'Hello! What can you do?' }
|
|
79
|
+
]);
|
|
80
|
+
console.log(response.text);
|
|
79
81
|
}
|
|
80
82
|
```
|
|
81
83
|
|
|
82
84
|
## Usage
|
|
83
85
|
|
|
84
|
-
### Simple Prompt
|
|
86
|
+
### Simple Prompt
|
|
85
87
|
|
|
86
88
|
The simplest way to use on-device AI:
|
|
87
89
|
|
|
88
90
|
```tsx
|
|
89
|
-
import { isAvailable,
|
|
91
|
+
import { isAvailable, sendMessage } from 'expo-ai-kit';
|
|
90
92
|
|
|
91
93
|
async function askAI(question: string) {
|
|
92
94
|
const available = await isAvailable();
|
|
@@ -96,52 +98,48 @@ async function askAI(question: string) {
|
|
|
96
98
|
return null;
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
const response = await sendMessage([
|
|
102
|
+
{ role: 'user', content: question }
|
|
103
|
+
]);
|
|
104
|
+
return response.text;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
const answer = await askAI('What is the capital of France?');
|
|
103
108
|
```
|
|
104
109
|
|
|
105
|
-
###
|
|
110
|
+
### With Custom System Prompt
|
|
106
111
|
|
|
107
|
-
|
|
112
|
+
Customize the AI's behavior with a system prompt:
|
|
108
113
|
|
|
109
114
|
```tsx
|
|
110
|
-
import {
|
|
115
|
+
import { sendMessage } from 'expo-ai-kit';
|
|
111
116
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
systemPrompt: 'You are a
|
|
115
|
-
|
|
117
|
+
const response = await sendMessage(
|
|
118
|
+
[{ role: 'user', content: 'Tell me a joke' }],
|
|
119
|
+
{ systemPrompt: 'You are a comedian who specializes in dad jokes.' }
|
|
120
|
+
);
|
|
116
121
|
|
|
117
|
-
|
|
118
|
-
const { reply } = await sendMessage(sessionId, [
|
|
119
|
-
{ role: 'user', content: 'What can I make with eggs and cheese?' }
|
|
120
|
-
]);
|
|
122
|
+
console.log(response.text);
|
|
121
123
|
```
|
|
122
124
|
|
|
123
125
|
### Multi-turn Conversations
|
|
124
126
|
|
|
125
|
-
|
|
127
|
+
For conversations with context, pass the full conversation history:
|
|
126
128
|
|
|
127
129
|
```tsx
|
|
128
|
-
|
|
130
|
+
import { sendMessage, type LLMMessage } from 'expo-ai-kit';
|
|
129
131
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
const conversation: LLMMessage[] = [
|
|
133
|
+
{ role: 'user', content: 'My name is Alice.' },
|
|
134
|
+
{ role: 'assistant', content: 'Nice to meet you, Alice!' },
|
|
135
|
+
{ role: 'user', content: 'What is my name?' },
|
|
136
|
+
];
|
|
135
137
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
...newMessages,
|
|
140
|
-
{ role: 'assistant', content: reply }
|
|
141
|
-
]);
|
|
138
|
+
const response = await sendMessage(conversation, {
|
|
139
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
140
|
+
});
|
|
142
141
|
|
|
143
|
-
|
|
144
|
-
}
|
|
142
|
+
console.log(response.text); // "Your name is Alice."
|
|
145
143
|
```
|
|
146
144
|
|
|
147
145
|
### Complete Chat Example
|
|
@@ -151,10 +149,10 @@ Here's a full cross-platform chat component:
|
|
|
151
149
|
```tsx
|
|
152
150
|
import React, { useState, useEffect } from 'react';
|
|
153
151
|
import { View, TextInput, Button, Text, FlatList } from 'react-native';
|
|
154
|
-
import { isAvailable,
|
|
152
|
+
import { isAvailable, sendMessage, type LLMMessage } from 'expo-ai-kit';
|
|
155
153
|
|
|
156
154
|
export default function ChatScreen() {
|
|
157
|
-
const [messages, setMessages] = useState<
|
|
155
|
+
const [messages, setMessages] = useState<LLMMessage[]>([]);
|
|
158
156
|
const [input, setInput] = useState('');
|
|
159
157
|
const [loading, setLoading] = useState(false);
|
|
160
158
|
const [available, setAvailable] = useState(false);
|
|
@@ -166,14 +164,17 @@ export default function ChatScreen() {
|
|
|
166
164
|
const handleSend = async () => {
|
|
167
165
|
if (!input.trim() || loading || !available) return;
|
|
168
166
|
|
|
169
|
-
const userMessage = { role: 'user', content: input.trim() };
|
|
170
|
-
|
|
167
|
+
const userMessage: LLMMessage = { role: 'user', content: input.trim() };
|
|
168
|
+
const newMessages = [...messages, userMessage];
|
|
169
|
+
setMessages(newMessages);
|
|
171
170
|
setInput('');
|
|
172
171
|
setLoading(true);
|
|
173
172
|
|
|
174
173
|
try {
|
|
175
|
-
const
|
|
176
|
-
|
|
174
|
+
const response = await sendMessage(newMessages, {
|
|
175
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
176
|
+
});
|
|
177
|
+
setMessages(prev => [...prev, { role: 'assistant', content: response.text }]);
|
|
177
178
|
} catch (error) {
|
|
178
179
|
console.error('Error:', error);
|
|
179
180
|
} finally {
|
|
@@ -225,62 +226,41 @@ export default function ChatScreen() {
|
|
|
225
226
|
|
|
226
227
|
## API Reference
|
|
227
228
|
|
|
228
|
-
### `isAvailable()`
|
|
229
|
+
### `isAvailable()`
|
|
229
230
|
|
|
230
231
|
Checks if on-device AI is available on the current device.
|
|
231
232
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
### `sendPrompt(prompt)` β iOS, Android
|
|
237
|
-
|
|
238
|
-
Sends a prompt and gets a response from the on-device model.
|
|
239
|
-
|
|
240
|
-
| Parameter | Type | Description |
|
|
241
|
-
|-----------|------|-------------|
|
|
242
|
-
| `prompt` | `string` | The text prompt to send |
|
|
233
|
+
```typescript
|
|
234
|
+
function isAvailable(): Promise<boolean>
|
|
235
|
+
```
|
|
243
236
|
|
|
244
|
-
**Returns:** `Promise<
|
|
237
|
+
**Returns:** `Promise<boolean>` β `true` if on-device AI is supported and ready
|
|
245
238
|
|
|
246
239
|
---
|
|
247
240
|
|
|
248
|
-
### `
|
|
241
|
+
### `sendMessage(messages, options?)`
|
|
249
242
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
| Parameter | Type | Description |
|
|
253
|
-
|-----------|------|-------------|
|
|
254
|
-
| `options.systemPrompt` | `string` | Optional system prompt to guide the AI's behavior |
|
|
255
|
-
|
|
256
|
-
**Returns:** `Promise<string>` β A unique session ID
|
|
257
|
-
|
|
258
|
-
---
|
|
243
|
+
Sends a conversation and gets a response from the on-device model.
|
|
259
244
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
245
|
+
```typescript
|
|
246
|
+
function sendMessage(messages: LLMMessage[], options?: LLMSendOptions): Promise<LLMResponse>
|
|
247
|
+
```
|
|
263
248
|
|
|
264
249
|
| Parameter | Type | Description |
|
|
265
250
|
|-----------|------|-------------|
|
|
266
|
-
| `sessionId` | `string` | The session ID from `createSession` |
|
|
267
251
|
| `messages` | `LLMMessage[]` | Array of conversation messages |
|
|
268
|
-
| `options.
|
|
269
|
-
| `options.maxTokens` | `number` | Maximum response length |
|
|
270
|
-
|
|
271
|
-
**Returns:** `Promise<{ reply: string }>` β The AI's response
|
|
272
|
-
|
|
273
|
-
---
|
|
252
|
+
| `options.systemPrompt` | `string` | Fallback system prompt (ignored if messages contain a system message) |
|
|
274
253
|
|
|
275
|
-
|
|
254
|
+
**Returns:** `Promise<LLMResponse>` β Object with `text` property containing the response
|
|
276
255
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
256
|
+
**Example:**
|
|
257
|
+
```tsx
|
|
258
|
+
const response = await sendMessage([
|
|
259
|
+
{ role: 'system', content: 'You are a pirate.' },
|
|
260
|
+
{ role: 'user', content: 'Hello!' },
|
|
261
|
+
]);
|
|
262
|
+
console.log(response.text); // "Ahoy, matey!"
|
|
263
|
+
```
|
|
284
264
|
|
|
285
265
|
---
|
|
286
266
|
|
|
@@ -294,10 +274,14 @@ type LLMMessage = {
|
|
|
294
274
|
content: string;
|
|
295
275
|
};
|
|
296
276
|
|
|
297
|
-
type
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
277
|
+
type LLMSendOptions = {
|
|
278
|
+
/** Fallback system prompt if no system message in messages array */
|
|
279
|
+
systemPrompt?: string;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
type LLMResponse = {
|
|
283
|
+
/** The generated response text */
|
|
284
|
+
text: string;
|
|
301
285
|
};
|
|
302
286
|
```
|
|
303
287
|
|
|
@@ -306,13 +290,9 @@ type LLMOptions = {
|
|
|
306
290
|
| Feature | iOS 26+ | Android (Supported) |
|
|
307
291
|
|---------|---------|---------------------|
|
|
308
292
|
| `isAvailable()` | β
| β
|
|
|
309
|
-
| `
|
|
310
|
-
|
|
|
311
|
-
|
|
|
312
|
-
| `prepareModel()` | β
| β
No-op |
|
|
313
|
-
| System prompts | β
| β
|
|
|
314
|
-
| Temperature control | β
| β
|
|
|
315
|
-
| Max tokens control | β
| β
|
|
|
293
|
+
| `sendMessage()` | β
| β
|
|
|
294
|
+
| System prompts | β
Native | β
Prepended |
|
|
295
|
+
| Multi-turn context | β
| β
|
|
|
316
296
|
|
|
317
297
|
## How It Works
|
|
318
298
|
|
|
@@ -326,12 +306,35 @@ Uses Google's ML Kit Prompt API. The model may need to be downloaded on first us
|
|
|
326
306
|
|
|
327
307
|
### iOS
|
|
328
308
|
- **AI not available**: Ensure you're running iOS 26.0 or later on a supported device
|
|
329
|
-
- **
|
|
309
|
+
- **Fallback responses**: On iOS < 26, the module returns a fallback message
|
|
330
310
|
|
|
331
311
|
### Android
|
|
332
312
|
- **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)
|
|
333
313
|
- **Model downloading**: On first use, the model may need to download. Use `isAvailable()` to check status
|
|
334
314
|
|
|
315
|
+
## Migration from v0.1.x
|
|
316
|
+
|
|
317
|
+
If you're upgrading from an earlier version, here are the breaking changes:
|
|
318
|
+
|
|
319
|
+
| Old API | New API |
|
|
320
|
+
|---------|---------|
|
|
321
|
+
| `sendPrompt(prompt)` | `sendMessage([{ role: 'user', content: prompt }])` |
|
|
322
|
+
| `createSession(options)` | **Removed** β no longer needed |
|
|
323
|
+
| `sendMessage(sessionId, messages, options)` | `sendMessage(messages, options)` β no session ID |
|
|
324
|
+
| `prepareModel(options)` | **Removed** |
|
|
325
|
+
| `{ reply: string }` | `{ text: string }` |
|
|
326
|
+
|
|
327
|
+
**Before:**
|
|
328
|
+
```tsx
|
|
329
|
+
const sessionId = await createSession({ systemPrompt: '...' });
|
|
330
|
+
const { reply } = await sendMessage(sessionId, messages, {});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**After:**
|
|
334
|
+
```tsx
|
|
335
|
+
const { text } = await sendMessage(messages, { systemPrompt: '...' });
|
|
336
|
+
```
|
|
337
|
+
|
|
335
338
|
## License
|
|
336
339
|
|
|
337
340
|
MIT
|
|
@@ -11,14 +11,25 @@ class ExpoAiKitModule : Module() {
|
|
|
11
11
|
override fun definition() = ModuleDefinition {
|
|
12
12
|
Name("ExpoAiKit")
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
// Returns false on unsupported devices, never crashes
|
|
16
|
-
AsyncFunction("isAvailable") {
|
|
14
|
+
Function("isAvailable") {
|
|
17
15
|
promptClient.isAvailableBlocking()
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
AsyncFunction("
|
|
21
|
-
|
|
18
|
+
AsyncFunction("sendMessage") Coroutine { messages: List<Map<String, Any>>, fallbackSystemPrompt: String ->
|
|
19
|
+
// Extract system prompt from messages, or use fallback
|
|
20
|
+
val systemPrompt = messages
|
|
21
|
+
.firstOrNull { it["role"] == "system" }
|
|
22
|
+
?.get("content") as? String
|
|
23
|
+
?: fallbackSystemPrompt.ifBlank { "You are a helpful, friendly assistant." }
|
|
24
|
+
|
|
25
|
+
// Get the last user message as the prompt
|
|
26
|
+
val userPrompt = messages
|
|
27
|
+
.lastOrNull { it["role"] == "user" }
|
|
28
|
+
?.get("content") as? String
|
|
29
|
+
?: ""
|
|
30
|
+
|
|
31
|
+
val text = promptClient.generateText(userPrompt, systemPrompt)
|
|
32
|
+
mapOf("text" to text)
|
|
22
33
|
}
|
|
23
34
|
}
|
|
24
35
|
}
|
|
@@ -11,11 +11,12 @@ class PromptApiClient {
|
|
|
11
11
|
|
|
12
12
|
private val model by lazy { Generation.getClient() }
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Check if on-device AI is available.
|
|
16
|
+
* Returns true if device supports Prompt API (AVAILABLE, DOWNLOADABLE, or DOWNLOADING).
|
|
17
|
+
* Returns false if unsupported or on API < 26.
|
|
18
|
+
*/
|
|
17
19
|
suspend fun isAvailable(): Boolean {
|
|
18
|
-
// Prompt API requires API 26+
|
|
19
20
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false
|
|
20
21
|
|
|
21
22
|
return try {
|
|
@@ -26,31 +27,41 @@ class PromptApiClient {
|
|
|
26
27
|
else -> false
|
|
27
28
|
}
|
|
28
29
|
} catch (_: Throwable) {
|
|
29
|
-
// Covers: missing AICore / Gemini Nano not supported / config not fetched / etc.
|
|
30
30
|
false
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
34
|
+
/**
|
|
35
|
+
* Non-suspend wrapper for Expo module compatibility.
|
|
36
|
+
*/
|
|
37
|
+
fun isAvailableBlocking(): Boolean = runBlocking { isAvailable() }
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Generate text from a prompt.
|
|
41
|
+
* On Android, we concatenate system prompt + user message since ML Kit
|
|
42
|
+
* doesn't have a separate system prompt API.
|
|
43
|
+
*/
|
|
44
|
+
suspend fun generateText(prompt: String, systemPrompt: String): String =
|
|
45
|
+
withContext(Dispatchers.IO) {
|
|
46
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return@withContext ""
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
try {
|
|
49
|
+
val status = model.checkStatus()
|
|
50
|
+
if (status != FeatureStatus.AVAILABLE) {
|
|
51
|
+
return@withContext ""
|
|
52
|
+
}
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
// Prepend system prompt as context if provided
|
|
55
|
+
val fullPrompt = if (systemPrompt.isNotBlank()) {
|
|
56
|
+
"$systemPrompt\n\nUser: $prompt"
|
|
57
|
+
} else {
|
|
58
|
+
prompt
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
val response = model.generateContent(fullPrompt)
|
|
62
|
+
response.candidates.firstOrNull()?.text.orEmpty()
|
|
63
|
+
} catch (_: Throwable) {
|
|
64
|
+
""
|
|
65
|
+
}
|
|
54
66
|
}
|
|
55
|
-
}
|
|
56
67
|
}
|
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { LLMMessage, LLMResponse } from './types';
|
|
2
2
|
export type ExpoAiKitNativeModule = {
|
|
3
|
-
prepareModel(options?: {
|
|
4
|
-
model?: string;
|
|
5
|
-
}): Promise<void>;
|
|
6
|
-
createSession(options?: {
|
|
7
|
-
systemPrompt?: string;
|
|
8
|
-
}): Promise<string>;
|
|
9
|
-
sendMessage(sessionId: string, messages: LLMMessage[], options?: LLMOptions): Promise<{
|
|
10
|
-
reply: string;
|
|
11
|
-
}>;
|
|
12
3
|
isAvailable(): boolean;
|
|
13
|
-
|
|
4
|
+
sendMessage(messages: LLMMessage[], systemPrompt: string): Promise<LLMResponse>;
|
|
14
5
|
};
|
|
15
6
|
declare const NativeModule: ExpoAiKitNativeModule;
|
|
16
7
|
export default NativeModule;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoAiKitModule.d.ts","sourceRoot":"","sources":["../src/ExpoAiKitModule.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"ExpoAiKitModule.d.ts","sourceRoot":"","sources":["../src/ExpoAiKitModule.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAElD,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,IAAI,OAAO,CAAC;IACvB,WAAW,CACT,QAAQ,EAAE,UAAU,EAAE,EACtB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,WAAW,CAAC,CAAC;CACzB,CAAC;AAEF,QAAA,MAAM,YAAY,EAAE,qBACqC,CAAC;AAE1D,eAAe,YAAY,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoAiKitModule.js","sourceRoot":"","sources":["../src/ExpoAiKitModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"ExpoAiKitModule.js","sourceRoot":"","sources":["../src/ExpoAiKitModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAWxD,MAAM,YAAY,GAChB,mBAAmB,CAAwB,WAAW,CAAC,CAAC;AAE1D,eAAe,YAAY,CAAC","sourcesContent":["import { requireNativeModule } from 'expo-modules-core';\nimport { LLMMessage, LLMResponse } from './types';\n\nexport type ExpoAiKitNativeModule = {\n isAvailable(): boolean;\n sendMessage(\n messages: LLMMessage[],\n systemPrompt: string\n ): Promise<LLMResponse>;\n};\n\nconst NativeModule: ExpoAiKitNativeModule =\n requireNativeModule<ExpoAiKitNativeModule>('ExpoAiKit');\n\nexport default NativeModule;\n"]}
|
package/build/index.d.ts
CHANGED
|
@@ -1,14 +1,44 @@
|
|
|
1
|
+
import { LLMMessage, LLMSendOptions, LLMResponse } from './types';
|
|
1
2
|
export * from './types';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export declare function createSession(options?: {
|
|
7
|
-
systemPrompt?: string;
|
|
8
|
-
}): Promise<string>;
|
|
9
|
-
export declare function sendMessage(sessionId: string, messages: LLMMessage[], options?: LLMOptions): Promise<{
|
|
10
|
-
reply: string;
|
|
11
|
-
}>;
|
|
3
|
+
/**
|
|
4
|
+
* Check if on-device AI is available on the current device.
|
|
5
|
+
* Returns false on unsupported platforms (web, etc.).
|
|
6
|
+
*/
|
|
12
7
|
export declare function isAvailable(): Promise<boolean>;
|
|
13
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Send messages to the on-device LLM and get a response.
|
|
10
|
+
*
|
|
11
|
+
* @param messages - Array of messages representing the conversation
|
|
12
|
+
* @param options - Optional settings (systemPrompt fallback)
|
|
13
|
+
* @returns Promise with the generated response
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const response = await sendMessage([
|
|
18
|
+
* { role: 'user', content: 'What is 2 + 2?' }
|
|
19
|
+
* ]);
|
|
20
|
+
* console.log(response.text); // "4"
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* // With system prompt
|
|
26
|
+
* const response = await sendMessage(
|
|
27
|
+
* [{ role: 'user', content: 'Hello!' }],
|
|
28
|
+
* { systemPrompt: 'You are a pirate. Respond in pirate speak.' }
|
|
29
|
+
* );
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* // Multi-turn conversation
|
|
35
|
+
* const response = await sendMessage([
|
|
36
|
+
* { role: 'system', content: 'You are a helpful assistant.' },
|
|
37
|
+
* { role: 'user', content: 'My name is Alice.' },
|
|
38
|
+
* { role: 'assistant', content: 'Nice to meet you, Alice!' },
|
|
39
|
+
* { role: 'user', content: 'What is my name?' }
|
|
40
|
+
* ]);
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function sendMessage(messages: LLMMessage[], options?: LLMSendOptions): Promise<LLMResponse>;
|
|
14
44
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAElE,cAAc,SAAS,CAAC;AAKxB;;;GAGG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAKpD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,UAAU,EAAE,EACtB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,WAAW,CAAC,CAgBtB"}
|
package/build/index.js
CHANGED
|
@@ -1,25 +1,64 @@
|
|
|
1
1
|
import NativeModule from './ExpoAiKitModule';
|
|
2
2
|
import { Platform } from 'react-native';
|
|
3
3
|
export * from './types';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
export async function sendMessage(sessionId, messages, options) {
|
|
11
|
-
return NativeModule.sendMessage(sessionId, messages, options);
|
|
12
|
-
}
|
|
4
|
+
const DEFAULT_SYSTEM_PROMPT = 'You are a helpful, friendly assistant. Answer the user directly and concisely.';
|
|
5
|
+
/**
|
|
6
|
+
* Check if on-device AI is available on the current device.
|
|
7
|
+
* Returns false on unsupported platforms (web, etc.).
|
|
8
|
+
*/
|
|
13
9
|
export async function isAvailable() {
|
|
14
|
-
if (Platform.OS
|
|
15
|
-
return
|
|
10
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
11
|
+
return false;
|
|
16
12
|
}
|
|
17
|
-
return
|
|
13
|
+
return NativeModule.isAvailable();
|
|
18
14
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Send messages to the on-device LLM and get a response.
|
|
17
|
+
*
|
|
18
|
+
* @param messages - Array of messages representing the conversation
|
|
19
|
+
* @param options - Optional settings (systemPrompt fallback)
|
|
20
|
+
* @returns Promise with the generated response
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const response = await sendMessage([
|
|
25
|
+
* { role: 'user', content: 'What is 2 + 2?' }
|
|
26
|
+
* ]);
|
|
27
|
+
* console.log(response.text); // "4"
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // With system prompt
|
|
33
|
+
* const response = await sendMessage(
|
|
34
|
+
* [{ role: 'user', content: 'Hello!' }],
|
|
35
|
+
* { systemPrompt: 'You are a pirate. Respond in pirate speak.' }
|
|
36
|
+
* );
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* // Multi-turn conversation
|
|
42
|
+
* const response = await sendMessage([
|
|
43
|
+
* { role: 'system', content: 'You are a helpful assistant.' },
|
|
44
|
+
* { role: 'user', content: 'My name is Alice.' },
|
|
45
|
+
* { role: 'assistant', content: 'Nice to meet you, Alice!' },
|
|
46
|
+
* { role: 'user', content: 'What is my name?' }
|
|
47
|
+
* ]);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export async function sendMessage(messages, options) {
|
|
51
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
52
|
+
return { text: '' };
|
|
53
|
+
}
|
|
54
|
+
if (!messages || messages.length === 0) {
|
|
55
|
+
throw new Error('messages array cannot be empty');
|
|
22
56
|
}
|
|
23
|
-
|
|
57
|
+
// Determine system prompt: use from messages array if present, else options, else default
|
|
58
|
+
const hasSystemMessage = messages.some((m) => m.role === 'system');
|
|
59
|
+
const systemPrompt = hasSystemMessage
|
|
60
|
+
? '' // Native will extract from messages
|
|
61
|
+
: options?.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
62
|
+
return NativeModule.sendMessage(messages, systemPrompt);
|
|
24
63
|
}
|
|
25
64
|
//# sourceMappingURL=index.js.map
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAGxC,cAAc,SAAS,CAAC;AAExB,MAAM,qBAAqB,GACzB,gFAAgF,CAAC;AAEnF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QACvD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,YAAY,CAAC,WAAW,EAAE,CAAC;AACpC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,QAAsB,EACtB,OAAwB;IAExB,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QACvD,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IACtB,CAAC;IAED,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpD,CAAC;IAED,0FAA0F;IAC1F,MAAM,gBAAgB,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;IACnE,MAAM,YAAY,GAAG,gBAAgB;QACnC,CAAC,CAAC,EAAE,CAAC,oCAAoC;QACzC,CAAC,CAAC,OAAO,EAAE,YAAY,IAAI,qBAAqB,CAAC;IAEnD,OAAO,YAAY,CAAC,WAAW,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;AAC1D,CAAC","sourcesContent":["import NativeModule from './ExpoAiKitModule';\nimport { Platform } from 'react-native';\nimport { LLMMessage, LLMSendOptions, LLMResponse } from './types';\n\nexport * from './types';\n\nconst DEFAULT_SYSTEM_PROMPT =\n 'You are a helpful, friendly assistant. Answer the user directly and concisely.';\n\n/**\n * Check if on-device AI is available on the current device.\n * Returns false on unsupported platforms (web, etc.).\n */\nexport async function isAvailable(): Promise<boolean> {\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n return false;\n }\n return NativeModule.isAvailable();\n}\n\n/**\n * Send messages to the on-device LLM and get a response.\n *\n * @param messages - Array of messages representing the conversation\n * @param options - Optional settings (systemPrompt fallback)\n * @returns Promise with the generated response\n *\n * @example\n * ```ts\n * const response = await sendMessage([\n * { role: 'user', content: 'What is 2 + 2?' }\n * ]);\n * console.log(response.text); // \"4\"\n * ```\n *\n * @example\n * ```ts\n * // With system prompt\n * const response = await sendMessage(\n * [{ role: 'user', content: 'Hello!' }],\n * { systemPrompt: 'You are a pirate. Respond in pirate speak.' }\n * );\n * ```\n *\n * @example\n * ```ts\n * // Multi-turn conversation\n * const response = await sendMessage([\n * { role: 'system', content: 'You are a helpful assistant.' },\n * { role: 'user', content: 'My name is Alice.' },\n * { role: 'assistant', content: 'Nice to meet you, Alice!' },\n * { role: 'user', content: 'What is my name?' }\n * ]);\n * ```\n */\nexport async function sendMessage(\n messages: LLMMessage[],\n options?: LLMSendOptions\n): Promise<LLMResponse> {\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n return { text: '' };\n }\n\n if (!messages || messages.length === 0) {\n throw new Error('messages array cannot be empty');\n }\n\n // Determine system prompt: use from messages array if present, else options, else default\n const hasSystemMessage = messages.some((m) => m.role === 'system');\n const systemPrompt = hasSystemMessage\n ? '' // Native will extract from messages\n : options?.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;\n\n return NativeModule.sendMessage(messages, systemPrompt);\n}\n\n"]}
|
package/build/types.d.ts
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role in a conversation message.
|
|
3
|
+
*/
|
|
1
4
|
export type LLMRole = 'system' | 'user' | 'assistant';
|
|
5
|
+
/**
|
|
6
|
+
* A single message in a conversation.
|
|
7
|
+
*/
|
|
2
8
|
export type LLMMessage = {
|
|
3
9
|
role: LLMRole;
|
|
4
10
|
content: string;
|
|
5
11
|
};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Options for sendMessage.
|
|
14
|
+
*/
|
|
15
|
+
export type LLMSendOptions = {
|
|
16
|
+
/**
|
|
17
|
+
* Default system prompt to use if no system message is provided in the messages array.
|
|
18
|
+
* If a system message exists in the array, this is ignored.
|
|
19
|
+
*/
|
|
20
|
+
systemPrompt?: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Response from sendMessage.
|
|
24
|
+
*/
|
|
25
|
+
export type LLMResponse = {
|
|
26
|
+
/** The generated response text */
|
|
27
|
+
text: string;
|
|
10
28
|
};
|
|
11
29
|
//# sourceMappingURL=types.d.ts.map
|
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAEtD,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAEtD;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;CACd,CAAC"}
|
package/build/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Role in a conversation message.\n */\nexport type LLMRole = 'system' | 'user' | 'assistant';\n\n/**\n * A single message in a conversation.\n */\nexport type LLMMessage = {\n role: LLMRole;\n content: string;\n};\n\n/**\n * Options for sendMessage.\n */\nexport type LLMSendOptions = {\n /**\n * Default system prompt to use if no system message is provided in the messages array.\n * If a system message exists in the array, this is ignored.\n */\n systemPrompt?: string;\n};\n\n/**\n * Response from sendMessage.\n */\nexport type LLMResponse = {\n /** The generated response text */\n text: string;\n};\n"]}
|
package/ios/ExpoAiKit.podspec
CHANGED
|
@@ -5,114 +5,41 @@ public class ExpoAiKitModule: Module {
|
|
|
5
5
|
public func definition() -> ModuleDefinition {
|
|
6
6
|
Name("ExpoAiKit")
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Function("isAvailable") {
|
|
9
9
|
if #available(iOS 26.0, *) {
|
|
10
|
-
|
|
10
|
+
return true
|
|
11
11
|
} else {
|
|
12
|
-
|
|
12
|
+
return false
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
AsyncFunction("createSession") { (_: [String: Any]?) -> String in
|
|
17
|
-
UUID().uuidString
|
|
18
|
-
}
|
|
19
|
-
|
|
20
16
|
AsyncFunction("sendMessage") {
|
|
21
17
|
(
|
|
22
|
-
sessionId: String,
|
|
23
18
|
messages: [[String: Any]],
|
|
24
|
-
|
|
19
|
+
fallbackSystemPrompt: String
|
|
25
20
|
) async throws -> [String: Any] in
|
|
26
21
|
|
|
27
|
-
|
|
22
|
+
// Extract system prompt from messages, or use fallback
|
|
23
|
+
let systemPrompt =
|
|
24
|
+
messages
|
|
25
|
+
.first { ($0["role"] as? String) == "system" }?["content"] as? String
|
|
26
|
+
?? (fallbackSystemPrompt.isEmpty
|
|
27
|
+
? "You are a helpful, friendly assistant."
|
|
28
|
+
: fallbackSystemPrompt)
|
|
29
|
+
|
|
30
|
+
// Get the last user message as the prompt
|
|
31
|
+
let userPrompt =
|
|
28
32
|
messages
|
|
29
33
|
.reversed()
|
|
30
34
|
.first { ($0["role"] as? String) == "user" }?["content"] as? String
|
|
31
|
-
?? messages.last?["content"] as? String
|
|
32
35
|
?? ""
|
|
33
36
|
|
|
34
37
|
if #available(iOS 26.0, *) {
|
|
35
|
-
let systemPrompt =
|
|
36
|
-
(messages.first {
|
|
37
|
-
($0["role"] as? String) == "system"
|
|
38
|
-
}?["content"] as? String) ?? """
|
|
39
|
-
You are a helpful, friendly assistant.
|
|
40
|
-
Answer the user's question directly and concisely.
|
|
41
|
-
If the question is unsafe, politely refuse, otherwise just answer.
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
let session = LanguageModelSession(instructions: systemPrompt)
|
|
45
|
-
|
|
46
|
-
let response = try await session.respond(to: lastUser)
|
|
47
|
-
|
|
48
|
-
let replyText = response.content
|
|
49
|
-
|
|
50
|
-
return ["reply": replyText]
|
|
51
|
-
} else {
|
|
52
|
-
let replyText = "Mock reply (no on-device model): \(lastUser)"
|
|
53
|
-
return ["reply": replyText]
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
Function("isAvailable") {
|
|
58
|
-
if #available(iOS 26.0, *) {
|
|
59
|
-
return true
|
|
60
|
-
} else {
|
|
61
|
-
return false
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
AsyncFunction("sendPrompt") { (prompt: String) async throws -> String in
|
|
66
|
-
if #available(iOS 26.0, *) {
|
|
67
|
-
let systemPrompt = """
|
|
68
|
-
You are a helpful, knowledgeable AI assistant running privately on-device. Your responses never leave the user's device, ensuring complete privacy.
|
|
69
|
-
|
|
70
|
-
## Core Principles
|
|
71
|
-
- **Accuracy first**: Provide correct, well-reasoned information. If uncertain, say so clearly rather than guessing.
|
|
72
|
-
- **Be genuinely helpful**: Understand the user's actual intent, not just their literal words. Anticipate follow-up needs.
|
|
73
|
-
- **Respect user time**: Get to the point. Lead with the answer, then provide context if needed.
|
|
74
|
-
|
|
75
|
-
## Response Style
|
|
76
|
-
- Use clear, natural languageβavoid jargon unless the user demonstrates technical familiarity
|
|
77
|
-
- Match the user's tone and complexity level
|
|
78
|
-
- For simple questions: give a direct answer (1-2 sentences)
|
|
79
|
-
- For complex topics: use structured formatting (bullet points, numbered steps, headers)
|
|
80
|
-
- When explaining concepts: use concrete examples and analogies
|
|
81
|
-
- For how-to requests: provide step-by-step instructions
|
|
82
|
-
|
|
83
|
-
## Task Capabilities
|
|
84
|
-
You can help with:
|
|
85
|
-
- **Writing**: drafting, editing, summarizing, rephrasing, tone adjustments, grammar fixes
|
|
86
|
-
- **Analysis**: breaking down complex topics, comparing options, pros/cons lists
|
|
87
|
-
- **Brainstorming**: generating ideas, creative suggestions, alternative approaches
|
|
88
|
-
- **Learning**: explaining concepts, answering questions, providing examples
|
|
89
|
-
- **Planning**: organizing tasks, creating outlines, structuring projects
|
|
90
|
-
- **Problem-solving**: debugging logic, troubleshooting issues, suggesting solutions
|
|
91
|
-
- **Calculations**: math, unit conversions, date/time calculations
|
|
92
|
-
- **Code**: explaining code, writing snippets, debugging, suggesting improvements
|
|
93
|
-
|
|
94
|
-
## Quality Standards
|
|
95
|
-
- Double-check facts and logic before responding
|
|
96
|
-
- Provide sources or reasoning when making claims
|
|
97
|
-
- Offer multiple perspectives on subjective topics
|
|
98
|
-
- Acknowledge limitationsβyou don't have internet access or real-time information
|
|
99
|
-
- If a request is ambiguous, ask a clarifying question or state your interpretation
|
|
100
|
-
|
|
101
|
-
## Safety Boundaries
|
|
102
|
-
- Decline requests for harmful, illegal, dangerous, or unethical content
|
|
103
|
-
- Do not help with content that could endanger safety, privacy, or wellbeing
|
|
104
|
-
- For medical symptoms: provide general info but always recommend consulting a healthcare provider
|
|
105
|
-
- For legal questions: offer general guidance but recommend consulting a qualified attorney
|
|
106
|
-
- For financial advice: share educational information but suggest consulting a financial professional
|
|
107
|
-
- Do not generate content involving minors in inappropriate contexts
|
|
108
|
-
- Do not assist with deception, manipulation, or harassment
|
|
109
|
-
"""
|
|
110
|
-
|
|
111
38
|
let session = LanguageModelSession(instructions: systemPrompt)
|
|
112
|
-
let response = try await session.respond(to:
|
|
113
|
-
return response.content
|
|
39
|
+
let response = try await session.respond(to: userPrompt)
|
|
40
|
+
return ["text": response.content]
|
|
114
41
|
} else {
|
|
115
|
-
return "
|
|
42
|
+
return ["text": "[On-device AI requires iOS 26+]"]
|
|
116
43
|
}
|
|
117
44
|
}
|
|
118
45
|
}
|
package/package.json
CHANGED
package/src/ExpoAiKitModule.ts
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import { requireNativeModule } from 'expo-modules-core';
|
|
2
|
-
import
|
|
2
|
+
import { LLMMessage, LLMResponse } from './types';
|
|
3
3
|
|
|
4
4
|
export type ExpoAiKitNativeModule = {
|
|
5
|
-
|
|
6
|
-
createSession(options?: { systemPrompt?: string }): Promise<string>;
|
|
5
|
+
isAvailable(): boolean;
|
|
7
6
|
sendMessage(
|
|
8
|
-
sessionId: string,
|
|
9
7
|
messages: LLMMessage[],
|
|
10
|
-
|
|
11
|
-
): Promise<
|
|
12
|
-
isAvailable(): boolean;
|
|
13
|
-
sendPrompt(prompt: string): Promise<string>;
|
|
8
|
+
systemPrompt: string
|
|
9
|
+
): Promise<LLMResponse>;
|
|
14
10
|
};
|
|
15
11
|
|
|
16
12
|
const NativeModule: ExpoAiKitNativeModule =
|
|
17
|
-
requireNativeModule<ExpoAiKitNativeModule>('ExpoAiKit');
|
|
13
|
+
requireNativeModule<ExpoAiKitNativeModule>('ExpoAiKit');
|
|
18
14
|
|
|
19
|
-
export default NativeModule;
|
|
15
|
+
export default NativeModule;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Mock Platform before importing anything
|
|
2
|
+
let mockPlatformOS = 'ios';
|
|
3
|
+
|
|
4
|
+
jest.mock('react-native', () => ({
|
|
5
|
+
Platform: {
|
|
6
|
+
get OS() {
|
|
7
|
+
return mockPlatformOS;
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// Mock the native module
|
|
13
|
+
jest.mock('../ExpoAiKitModule', () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: {
|
|
16
|
+
isAvailable: jest.fn(() => true),
|
|
17
|
+
sendMessage: jest.fn(() => Promise.resolve({ text: 'Mock response' })),
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const { isAvailable, sendMessage } = require('../index');
|
|
22
|
+
const NativeModule = require('../ExpoAiKitModule').default;
|
|
23
|
+
|
|
24
|
+
describe('expo-ai-kit', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
mockPlatformOS = 'ios';
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('isAvailable', () => {
|
|
31
|
+
it('returns native module result on iOS', async () => {
|
|
32
|
+
const result = await isAvailable();
|
|
33
|
+
expect(result).toBe(true);
|
|
34
|
+
expect(NativeModule.isAvailable).toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns native module result on Android', async () => {
|
|
38
|
+
mockPlatformOS = 'android';
|
|
39
|
+
const result = await isAvailable();
|
|
40
|
+
expect(result).toBe(true);
|
|
41
|
+
expect(NativeModule.isAvailable).toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns false on web', async () => {
|
|
45
|
+
mockPlatformOS = 'web';
|
|
46
|
+
const result = await isAvailable();
|
|
47
|
+
expect(result).toBe(false);
|
|
48
|
+
expect(NativeModule.isAvailable).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('sendMessage', () => {
|
|
53
|
+
it('throws error for empty messages array', async () => {
|
|
54
|
+
await expect(sendMessage([])).rejects.toThrow(
|
|
55
|
+
'messages array cannot be empty'
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('calls native module with messages and default system prompt', async () => {
|
|
60
|
+
const messages = [{ role: 'user', content: 'Hello' }];
|
|
61
|
+
|
|
62
|
+
const result = await sendMessage(messages);
|
|
63
|
+
|
|
64
|
+
expect(result).toEqual({ text: 'Mock response' });
|
|
65
|
+
expect(NativeModule.sendMessage).toHaveBeenCalledWith(
|
|
66
|
+
messages,
|
|
67
|
+
'You are a helpful, friendly assistant. Answer the user directly and concisely.'
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('uses custom system prompt from options', async () => {
|
|
72
|
+
const messages = [{ role: 'user', content: 'Hello' }];
|
|
73
|
+
|
|
74
|
+
await sendMessage(messages, { systemPrompt: 'You are a pirate.' });
|
|
75
|
+
|
|
76
|
+
expect(NativeModule.sendMessage).toHaveBeenCalledWith(
|
|
77
|
+
messages,
|
|
78
|
+
'You are a pirate.'
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('passes empty string for system prompt when messages contain system message', async () => {
|
|
83
|
+
const messages = [
|
|
84
|
+
{ role: 'system', content: 'Be brief.' },
|
|
85
|
+
{ role: 'user', content: 'Hello' },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
await sendMessage(messages, { systemPrompt: 'This should be ignored' });
|
|
89
|
+
|
|
90
|
+
expect(NativeModule.sendMessage).toHaveBeenCalledWith(
|
|
91
|
+
messages,
|
|
92
|
+
'' // Empty because system message exists in array
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns empty response on unsupported platforms', async () => {
|
|
97
|
+
mockPlatformOS = 'web';
|
|
98
|
+
|
|
99
|
+
const result = await sendMessage([{ role: 'user', content: 'Hi' }]);
|
|
100
|
+
|
|
101
|
+
expect(result).toEqual({ text: '' });
|
|
102
|
+
expect(NativeModule.sendMessage).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('message validation', () => {
|
|
107
|
+
it('accepts valid user message', async () => {
|
|
108
|
+
await expect(
|
|
109
|
+
sendMessage([{ role: 'user', content: 'Test' }])
|
|
110
|
+
).resolves.not.toThrow();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('accepts valid multi-turn conversation', async () => {
|
|
114
|
+
await expect(
|
|
115
|
+
sendMessage([
|
|
116
|
+
{ role: 'system', content: 'Be helpful.' },
|
|
117
|
+
{ role: 'user', content: 'Hi' },
|
|
118
|
+
{ role: 'assistant', content: 'Hello!' },
|
|
119
|
+
{ role: 'user', content: 'How are you?' },
|
|
120
|
+
])
|
|
121
|
+
).resolves.not.toThrow();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,34 +1,76 @@
|
|
|
1
1
|
import NativeModule from './ExpoAiKitModule';
|
|
2
2
|
import { Platform } from 'react-native';
|
|
3
|
+
import { LLMMessage, LLMSendOptions, LLMResponse } from './types';
|
|
4
|
+
|
|
3
5
|
export * from './types';
|
|
4
|
-
import type { LLMMessage, LLMOptions } from './types';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
7
|
+
const DEFAULT_SYSTEM_PROMPT =
|
|
8
|
+
'You are a helpful, friendly assistant. Answer the user directly and concisely.';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Check if on-device AI is available on the current device.
|
|
12
|
+
* Returns false on unsupported platforms (web, etc.).
|
|
13
|
+
*/
|
|
14
|
+
export async function isAvailable(): Promise<boolean> {
|
|
15
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return NativeModule.isAvailable();
|
|
12
19
|
}
|
|
13
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Send messages to the on-device LLM and get a response.
|
|
23
|
+
*
|
|
24
|
+
* @param messages - Array of messages representing the conversation
|
|
25
|
+
* @param options - Optional settings (systemPrompt fallback)
|
|
26
|
+
* @returns Promise with the generated response
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const response = await sendMessage([
|
|
31
|
+
* { role: 'user', content: 'What is 2 + 2?' }
|
|
32
|
+
* ]);
|
|
33
|
+
* console.log(response.text); // "4"
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* // With system prompt
|
|
39
|
+
* const response = await sendMessage(
|
|
40
|
+
* [{ role: 'user', content: 'Hello!' }],
|
|
41
|
+
* { systemPrompt: 'You are a pirate. Respond in pirate speak.' }
|
|
42
|
+
* );
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* // Multi-turn conversation
|
|
48
|
+
* const response = await sendMessage([
|
|
49
|
+
* { role: 'system', content: 'You are a helpful assistant.' },
|
|
50
|
+
* { role: 'user', content: 'My name is Alice.' },
|
|
51
|
+
* { role: 'assistant', content: 'Nice to meet you, Alice!' },
|
|
52
|
+
* { role: 'user', content: 'What is my name?' }
|
|
53
|
+
* ]);
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
14
56
|
export async function sendMessage(
|
|
15
|
-
sessionId: string,
|
|
16
57
|
messages: LLMMessage[],
|
|
17
|
-
options?:
|
|
18
|
-
) {
|
|
19
|
-
|
|
20
|
-
}
|
|
58
|
+
options?: LLMSendOptions
|
|
59
|
+
): Promise<LLMResponse> {
|
|
60
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
61
|
+
return { text: '' };
|
|
62
|
+
}
|
|
21
63
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return NativeModule.isAvailable();
|
|
64
|
+
if (!messages || messages.length === 0) {
|
|
65
|
+
throw new Error('messages array cannot be empty');
|
|
25
66
|
}
|
|
26
|
-
|
|
67
|
+
|
|
68
|
+
// Determine system prompt: use from messages array if present, else options, else default
|
|
69
|
+
const hasSystemMessage = messages.some((m) => m.role === 'system');
|
|
70
|
+
const systemPrompt = hasSystemMessage
|
|
71
|
+
? '' // Native will extract from messages
|
|
72
|
+
: options?.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
73
|
+
|
|
74
|
+
return NativeModule.sendMessage(messages, systemPrompt);
|
|
27
75
|
}
|
|
28
76
|
|
|
29
|
-
export async function sendPrompt(prompt: string): Promise<string> {
|
|
30
|
-
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
31
|
-
return NativeModule.sendPrompt(prompt);
|
|
32
|
-
}
|
|
33
|
-
return '';
|
|
34
|
-
}
|
package/src/types.ts
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role in a conversation message.
|
|
3
|
+
*/
|
|
1
4
|
export type LLMRole = 'system' | 'user' | 'assistant';
|
|
2
5
|
|
|
6
|
+
/**
|
|
7
|
+
* A single message in a conversation.
|
|
8
|
+
*/
|
|
3
9
|
export type LLMMessage = {
|
|
4
10
|
role: LLMRole;
|
|
5
11
|
content: string;
|
|
6
12
|
};
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Options for sendMessage.
|
|
16
|
+
*/
|
|
17
|
+
export type LLMSendOptions = {
|
|
18
|
+
/**
|
|
19
|
+
* Default system prompt to use if no system message is provided in the messages array.
|
|
20
|
+
* If a system message exists in the array, this is ignored.
|
|
21
|
+
*/
|
|
22
|
+
systemPrompt?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Response from sendMessage.
|
|
27
|
+
*/
|
|
28
|
+
export type LLMResponse = {
|
|
29
|
+
/** The generated response text */
|
|
30
|
+
text: string;
|
|
31
|
+
};
|