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 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 mock responses |
21
+ | iOS < 26 | Returns fallback message |
22
22
  | Android (unsupported devices) | Returns empty string |
23
23
 
24
24
  ## Features
25
25
 
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** β€” Session-based chat with full conversation context
31
- - πŸŽ›οΈ **Configurable** β€” Temperature and max tokens control for response generation
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, sendPrompt } from 'expo-ai-kit';
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 sendPrompt('Hello! What can you do?');
78
- console.log(response);
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 (Cross-platform)
86
+ ### Simple Prompt
85
87
 
86
88
  The simplest way to use on-device AI:
87
89
 
88
90
  ```tsx
89
- import { isAvailable, sendPrompt } from 'expo-ai-kit';
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
- return await sendPrompt(question);
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
- ### Session-based Chat
110
+ ### With Custom System Prompt
106
111
 
107
- For multi-turn conversations with context, use sessions:
112
+ Customize the AI's behavior with a system prompt:
108
113
 
109
114
  ```tsx
110
- import { createSession, sendMessage } from 'expo-ai-kit';
115
+ import { sendMessage } from 'expo-ai-kit';
111
116
 
112
- // Create a chat session
113
- const sessionId = await createSession({
114
- systemPrompt: 'You are a friendly cooking assistant.',
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
- // Send messages with conversation history
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
- Keep track of the conversation history for context-aware responses:
127
+ For conversations with context, pass the full conversation history:
126
128
 
127
129
  ```tsx
128
- const [messages, setMessages] = useState<LLMMessage[]>([]);
130
+ import { sendMessage, type LLMMessage } from 'expo-ai-kit';
129
131
 
130
- async function chat(userMessage: string) {
131
- const newMessages = [
132
- ...messages,
133
- { role: 'user', content: userMessage }
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
- const { reply } = await sendMessage(sessionId, newMessages);
137
-
138
- setMessages([
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
- return reply;
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, sendPrompt } from 'expo-ai-kit';
152
+ import { isAvailable, sendMessage, type LLMMessage } from 'expo-ai-kit';
155
153
 
156
154
  export default function ChatScreen() {
157
- const [messages, setMessages] = useState<Array<{ role: string; content: string }>>([]);
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
- setMessages(prev => [...prev, userMessage]);
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 reply = await sendPrompt(input.trim());
176
- setMessages(prev => [...prev, { role: 'assistant', content: reply }]);
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()` β€” iOS, Android
229
+ ### `isAvailable()`
229
230
 
230
231
  Checks if on-device AI is available on the current device.
231
232
 
232
- **Returns:** `Promise<boolean>` β€” `true` if on-device AI is supported and ready
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<string>` β€” The AI's response (empty string if unavailable)
237
+ **Returns:** `Promise<boolean>` β€” `true` if on-device AI is supported and ready
245
238
 
246
239
  ---
247
240
 
248
- ### `createSession(options?)` β€” iOS, Android
241
+ ### `sendMessage(messages, options?)`
249
242
 
250
- Creates a new chat session for multi-turn conversations.
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
- ### `sendMessage(sessionId, messages, options?)` β€” iOS, Android
261
-
262
- Sends messages and gets a response from the on-device model with conversation context.
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.temperature` | `number` | Controls randomness (0-1) |
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
- ### `prepareModel(options?)` β€” iOS, Android
254
+ **Returns:** `Promise<LLMResponse>` β€” Object with `text` property containing the response
276
255
 
277
- Pre-loads the model for faster first response.
278
-
279
- | Parameter | Type | Description |
280
- |-----------|------|-------------|
281
- | `options.model` | `string` | Model identifier (optional) |
282
-
283
- **Returns:** `Promise<void>`
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 LLMOptions = {
298
- temperature?: number;
299
- maxTokens?: number;
300
- model?: string;
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
- | `sendPrompt()` | βœ… | βœ… |
310
- | `createSession()` | βœ… Full context | βœ… Basic |
311
- | `sendMessage()` | βœ… Full context | βœ… Basic |
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
- - **Mock responses**: On iOS < 26, the module returns mock responses for testing
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
- // Returns true if device supports Prompt API (AVAILABLE/DOWNLOADABLE/DOWNLOADING)
15
- // Returns false on unsupported devices, never crashes
16
- AsyncFunction("isAvailable") {
14
+ Function("isAvailable") {
17
15
  promptClient.isAvailableBlocking()
18
16
  }
19
17
 
20
- AsyncFunction("sendPrompt") Coroutine { prompt: String ->
21
- promptClient.generateText(prompt)
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
- // βœ… suspend version (matches ML Kit's checkStatus usage)
15
- // Returns true if device supports Prompt API (AVAILABLE, DOWNLOADABLE, or DOWNLOADING)
16
- // Returns false if unsupported or on API < 26
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
- // βœ… non-suspend wrapper so Expo Module can call it without Coroutine DSL ambiguity
35
- fun isAvailableBlocking(): Boolean = runBlocking {
36
- isAvailable()
37
- }
34
+ /**
35
+ * Non-suspend wrapper for Expo module compatibility.
36
+ */
37
+ fun isAvailableBlocking(): Boolean = runBlocking { isAvailable() }
38
38
 
39
- suspend fun generateText(prompt: String): String = withContext(Dispatchers.IO) {
40
- // Prompt API requires API 26+
41
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return@withContext ""
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
- try {
44
- val status = model.checkStatus()
45
- if (status != FeatureStatus.AVAILABLE) {
46
- return@withContext ""
47
- }
48
+ try {
49
+ val status = model.checkStatus()
50
+ if (status != FeatureStatus.AVAILABLE) {
51
+ return@withContext ""
52
+ }
48
53
 
49
- // safest + simplest API across ML Kit alpha versions
50
- val response = model.generateContent(prompt)
51
- response.candidates.firstOrNull()?.text.orEmpty()
52
- } catch (_: Throwable) {
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 type { LLMMessage, LLMOptions } from './types';
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
- sendPrompt(prompt: string): Promise<string>;
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,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEtD,MAAM,MAAM,qBAAqB,GAAG;IAClC,YAAY,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,aAAa,CAAC,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,UAAU,EAAE,EACtB,OAAO,CAAC,EAAE,UAAU,GACnB,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9B,WAAW,IAAI,OAAO,CAAC;IACvB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7C,CAAC;AAEF,QAAA,MAAM,YAAY,EAAE,qBACqC,CAAC;AAE1D,eAAe,YAAY,CAAC"}
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;AAexD,MAAM,YAAY,GAChB,mBAAmB,CAAwB,WAAW,CAAC,CAAC;AAE1D,eAAe,YAAY,CAAC","sourcesContent":["import { requireNativeModule } from 'expo-modules-core';\nimport type { LLMMessage, LLMOptions } from './types';\n\nexport type ExpoAiKitNativeModule = {\n prepareModel(options?: { model?: string }): Promise<void>;\n createSession(options?: { systemPrompt?: string }): Promise<string>;\n sendMessage(\n sessionId: string,\n messages: LLMMessage[],\n options?: LLMOptions\n ): Promise<{ reply: string }>;\n isAvailable(): boolean;\n sendPrompt(prompt: string): Promise<string>;\n};\n\nconst NativeModule: ExpoAiKitNativeModule =\n requireNativeModule<ExpoAiKitNativeModule>('ExpoAiKit'); \n\nexport default NativeModule;"]}
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
- import type { LLMMessage, LLMOptions } from './types';
3
- export declare function prepareModel(options?: {
4
- model?: string;
5
- }): Promise<void>;
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
- export declare function sendPrompt(prompt: string): Promise<string>;
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,SAAS,CAAC;AACxB,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEtD,wBAAsB,YAAY,CAAC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,iBAE9D;AAED,wBAAsB,aAAa,CAAC,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,mBAEtE;AAED,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,UAAU,EAAE,EACtB,OAAO,CAAC,EAAE,UAAU;;GAGrB;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAKpD;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKhE"}
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
- export async function prepareModel(options) {
5
- return NativeModule.prepareModel(options);
6
- }
7
- export async function createSession(options) {
8
- return NativeModule.createSession(options);
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 === 'ios' || Platform.OS === 'android') {
15
- return NativeModule.isAvailable();
10
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
11
+ return false;
16
12
  }
17
- return false;
13
+ return NativeModule.isAvailable();
18
14
  }
19
- export async function sendPrompt(prompt) {
20
- if (Platform.OS === 'ios' || Platform.OS === 'android') {
21
- return NativeModule.sendPrompt(prompt);
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
- return '';
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
@@ -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;AACxC,cAAc,SAAS,CAAC;AAGxB,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAA4B;IAC7D,OAAO,YAAY,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAmC;IACrE,OAAO,YAAY,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,SAAiB,EACjB,QAAsB,EACtB,OAAoB;IAEpB,OAAO,YAAY,CAAC,WAAW,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QACvD,OAAO,YAAY,CAAC,WAAW,EAAE,CAAC;IACpC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,MAAc;IAC7C,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QACvD,OAAO,YAAY,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC","sourcesContent":["import NativeModule from './ExpoAiKitModule';\nimport { Platform } from 'react-native';\nexport * from './types';\nimport type { LLMMessage, LLMOptions } from './types';\n\nexport async function prepareModel(options?: { model?: string }) {\n return NativeModule.prepareModel(options);\n}\n\nexport async function createSession(options?: { systemPrompt?: string }) {\n return NativeModule.createSession(options);\n}\n\nexport async function sendMessage(\n sessionId: string,\n messages: LLMMessage[],\n options?: LLMOptions\n) {\n return NativeModule.sendMessage(sessionId, messages, options);\n}\n\nexport async function isAvailable(): Promise<boolean> {\n if (Platform.OS === 'ios' || Platform.OS === 'android') {\n return NativeModule.isAvailable();\n }\n return false;\n}\n\nexport async function sendPrompt(prompt: string): Promise<string> {\n if (Platform.OS === 'ios' || Platform.OS === 'android') {\n return NativeModule.sendPrompt(prompt);\n }\n return '';\n}"]}
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
- export type LLMOptions = {
7
- temperature?: number;
8
- maxTokens?: number;
9
- model?: string;
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
@@ -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,UAAU,GAAG;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC"}
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"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type LLMRole = 'system' | 'user' | 'assistant';\n\nexport type LLMMessage = {\n role: LLMRole;\n content: string;\n};\n\nexport type LLMOptions = {\n temperature?: number;\n maxTokens?: number;\n model?: string;\n};"]}
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"]}
@@ -25,6 +25,6 @@ Pod::Spec.new do |s|
25
25
  'DEFINES_MODULE' => 'YES',
26
26
  }
27
27
 
28
- s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
28
+ s.source_files = "**/*.swift"
29
29
  end
30
30
 
@@ -5,114 +5,41 @@ public class ExpoAiKitModule: Module {
5
5
  public func definition() -> ModuleDefinition {
6
6
  Name("ExpoAiKit")
7
7
 
8
- AsyncFunction("prepareModel") { (_: [String: Any]?) async throws in
8
+ Function("isAvailable") {
9
9
  if #available(iOS 26.0, *) {
10
- _ = SystemLanguageModel.default
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
- options: [String: Any]?
19
+ fallbackSystemPrompt: String
25
20
  ) async throws -> [String: Any] in
26
21
 
27
- let lastUser =
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: prompt)
113
- return response.content
39
+ let response = try await session.respond(to: userPrompt)
40
+ return ["text": response.content]
114
41
  } else {
115
- return "Mock reply (no on-device model): \(prompt)"
42
+ return ["text": "[On-device AI requires iOS 26+]"]
116
43
  }
117
44
  }
118
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-ai-kit",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Expo AI Kit module",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -1,19 +1,15 @@
1
1
  import { requireNativeModule } from 'expo-modules-core';
2
- import type { LLMMessage, LLMOptions } from './types';
2
+ import { LLMMessage, LLMResponse } from './types';
3
3
 
4
4
  export type ExpoAiKitNativeModule = {
5
- prepareModel(options?: { model?: string }): Promise<void>;
6
- createSession(options?: { systemPrompt?: string }): Promise<string>;
5
+ isAvailable(): boolean;
7
6
  sendMessage(
8
- sessionId: string,
9
7
  messages: LLMMessage[],
10
- options?: LLMOptions
11
- ): Promise<{ reply: string }>;
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
- export async function prepareModel(options?: { model?: string }) {
7
- return NativeModule.prepareModel(options);
8
- }
7
+ const DEFAULT_SYSTEM_PROMPT =
8
+ 'You are a helpful, friendly assistant. Answer the user directly and concisely.';
9
9
 
10
- export async function createSession(options?: { systemPrompt?: string }) {
11
- return NativeModule.createSession(options);
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?: LLMOptions
18
- ) {
19
- return NativeModule.sendMessage(sessionId, messages, options);
20
- }
58
+ options?: LLMSendOptions
59
+ ): Promise<LLMResponse> {
60
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
61
+ return { text: '' };
62
+ }
21
63
 
22
- export async function isAvailable(): Promise<boolean> {
23
- if (Platform.OS === 'ios' || Platform.OS === 'android') {
24
- return NativeModule.isAvailable();
64
+ if (!messages || messages.length === 0) {
65
+ throw new Error('messages array cannot be empty');
25
66
  }
26
- return false;
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
- export type LLMOptions = {
9
- temperature?: number;
10
- maxTokens?: number;
11
- model?: string;
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
+ };