expo-ai-kit 0.1.15 → 0.1.17
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 +300 -2
- package/android/src/main/java/expo/modules/aikit/ExpoAiKitModule.kt +47 -0
- package/android/src/main/java/expo/modules/aikit/PromptApiClient.kt +45 -0
- package/build/ExpoAiKitModule.d.ts +12 -5
- package/build/ExpoAiKitModule.d.ts.map +1 -1
- package/build/ExpoAiKitModule.js +2 -2
- package/build/ExpoAiKitModule.js.map +1 -1
- package/build/index.d.ts +292 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +525 -3
- package/build/index.js.map +1 -1
- package/build/types.d.ts +89 -0
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/ios/ExpoAiKitModule.swift +95 -0
- package/package.json +1 -1
- package/src/ExpoAiKitModule.ts +20 -5
- package/src/__tests__/index.test.js +507 -1
- package/src/index.ts +673 -4
- package/src/types.ts +109 -0
package/README.md
CHANGED
|
@@ -28,7 +28,9 @@ On-device AI for Expo apps. Run language models locally—no API keys, no cloud,
|
|
|
28
28
|
- **Free forever** — No API costs, rate limits, or subscriptions
|
|
29
29
|
- **Native performance** — Built on Apple Foundation Models (iOS) and Google ML Kit Prompt API (Android)
|
|
30
30
|
- **Multi-turn conversations** — Full conversation context support
|
|
31
|
-
- **
|
|
31
|
+
- **Streaming support** — Progressive token streaming for responsive UIs
|
|
32
|
+
- **Simple API** — Core functions plus prompt helpers for common tasks
|
|
33
|
+
- **Prompt helpers** — Built-in `summarize()`, `translate()`, `rewrite()`, and more
|
|
32
34
|
|
|
33
35
|
## Requirements
|
|
34
36
|
|
|
@@ -142,6 +144,112 @@ const response = await sendMessage(conversation, {
|
|
|
142
144
|
console.log(response.text); // "Your name is Alice."
|
|
143
145
|
```
|
|
144
146
|
|
|
147
|
+
### Streaming Responses
|
|
148
|
+
|
|
149
|
+
For a ChatGPT-like experience where text appears progressively:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { streamMessage } from 'expo-ai-kit';
|
|
153
|
+
|
|
154
|
+
const [responseText, setResponseText] = useState('');
|
|
155
|
+
|
|
156
|
+
const { promise, stop } = streamMessage(
|
|
157
|
+
[{ role: 'user', content: 'Tell me a story' }],
|
|
158
|
+
(event) => {
|
|
159
|
+
// Update UI with each token
|
|
160
|
+
setResponseText(event.accumulatedText);
|
|
161
|
+
|
|
162
|
+
// event.token - the new token/chunk
|
|
163
|
+
// event.accumulatedText - full text so far
|
|
164
|
+
// event.isDone - whether streaming is complete
|
|
165
|
+
},
|
|
166
|
+
{ systemPrompt: 'You are a creative storyteller.' }
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Optionally cancel the stream
|
|
170
|
+
// stop();
|
|
171
|
+
|
|
172
|
+
// Wait for completion
|
|
173
|
+
await promise;
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Prompt Helpers
|
|
177
|
+
|
|
178
|
+
Use built-in helpers for common AI tasks without crafting prompts:
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
import { summarize, translate, rewrite, extractKeyPoints, answerQuestion } from 'expo-ai-kit';
|
|
182
|
+
|
|
183
|
+
// Summarize text
|
|
184
|
+
const summary = await summarize(longArticle, { length: 'short', style: 'bullets' });
|
|
185
|
+
|
|
186
|
+
// Translate text
|
|
187
|
+
const translated = await translate('Hello, world!', { to: 'Spanish' });
|
|
188
|
+
|
|
189
|
+
// Rewrite in a different style
|
|
190
|
+
const formal = await rewrite('hey whats up', { style: 'formal' });
|
|
191
|
+
|
|
192
|
+
// Extract key points
|
|
193
|
+
const points = await extractKeyPoints(article, { maxPoints: 5 });
|
|
194
|
+
|
|
195
|
+
// Answer questions about content
|
|
196
|
+
const answer = await answerQuestion('What is the main topic?', documentText);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
All helpers also have streaming variants (`streamSummarize`, `streamTranslate`, etc.):
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
const { promise, stop } = streamSummarize(
|
|
203
|
+
longArticle,
|
|
204
|
+
(event) => setSummary(event.accumulatedText),
|
|
205
|
+
{ style: 'bullets' }
|
|
206
|
+
);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Streaming with Cancel Button
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
import { useState, useRef } from 'react';
|
|
213
|
+
import { streamMessage } from 'expo-ai-kit';
|
|
214
|
+
|
|
215
|
+
function ChatWithStreaming() {
|
|
216
|
+
const [text, setText] = useState('');
|
|
217
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
218
|
+
const stopRef = useRef<(() => void) | null>(null);
|
|
219
|
+
|
|
220
|
+
const handleSend = async () => {
|
|
221
|
+
setIsStreaming(true);
|
|
222
|
+
setText('');
|
|
223
|
+
|
|
224
|
+
const { promise, stop } = streamMessage(
|
|
225
|
+
[{ role: 'user', content: 'Write a long story' }],
|
|
226
|
+
(event) => setText(event.accumulatedText)
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
stopRef.current = stop;
|
|
230
|
+
await promise;
|
|
231
|
+
stopRef.current = null;
|
|
232
|
+
setIsStreaming(false);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const handleStop = () => {
|
|
236
|
+
stopRef.current?.();
|
|
237
|
+
setIsStreaming(false);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<View>
|
|
242
|
+
<Text>{text}</Text>
|
|
243
|
+
{isStreaming ? (
|
|
244
|
+
<Button title="Stop" onPress={handleStop} />
|
|
245
|
+
) : (
|
|
246
|
+
<Button title="Send" onPress={handleSend} />
|
|
247
|
+
)}
|
|
248
|
+
</View>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
145
253
|
### Complete Chat Example
|
|
146
254
|
|
|
147
255
|
Here's a full cross-platform chat component:
|
|
@@ -264,6 +372,139 @@ console.log(response.text); // "Ahoy, matey!"
|
|
|
264
372
|
|
|
265
373
|
---
|
|
266
374
|
|
|
375
|
+
### `streamMessage(messages, onToken, options?)`
|
|
376
|
+
|
|
377
|
+
Streams a conversation response with progressive token updates. Ideal for responsive chat UIs.
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
function streamMessage(
|
|
381
|
+
messages: LLMMessage[],
|
|
382
|
+
onToken: LLMStreamCallback,
|
|
383
|
+
options?: LLMStreamOptions
|
|
384
|
+
): { promise: Promise<LLMResponse>; stop: () => void }
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
| Parameter | Type | Description |
|
|
388
|
+
|-----------|------|-------------|
|
|
389
|
+
| `messages` | `LLMMessage[]` | Array of conversation messages |
|
|
390
|
+
| `onToken` | `LLMStreamCallback` | Callback called for each token received |
|
|
391
|
+
| `options.systemPrompt` | `string` | Fallback system prompt (ignored if messages contain a system message) |
|
|
392
|
+
|
|
393
|
+
**Returns:** Object with:
|
|
394
|
+
- `promise: Promise<LLMResponse>` — Resolves when streaming completes
|
|
395
|
+
- `stop: () => void` — Function to cancel the stream
|
|
396
|
+
|
|
397
|
+
**Example:**
|
|
398
|
+
```tsx
|
|
399
|
+
const { promise, stop } = streamMessage(
|
|
400
|
+
[{ role: 'user', content: 'Hello!' }],
|
|
401
|
+
(event) => {
|
|
402
|
+
console.log(event.token); // New token: "Hi"
|
|
403
|
+
console.log(event.accumulatedText); // Full text: "Hi there!"
|
|
404
|
+
console.log(event.isDone); // false until complete
|
|
405
|
+
}
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// Cancel if needed
|
|
409
|
+
setTimeout(() => stop(), 5000);
|
|
410
|
+
|
|
411
|
+
// Wait for completion
|
|
412
|
+
const response = await promise;
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
### `summarize(text, options?)`
|
|
418
|
+
|
|
419
|
+
Summarizes text using on-device AI.
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
function summarize(text: string, options?: LLMSummarizeOptions): Promise<LLMResponse>
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
| Parameter | Type | Description |
|
|
426
|
+
|-----------|------|-------------|
|
|
427
|
+
| `text` | `string` | Text to summarize |
|
|
428
|
+
| `options.length` | `'short' \| 'medium' \| 'long'` | Summary length (default: `'medium'`) |
|
|
429
|
+
| `options.style` | `'paragraph' \| 'bullets' \| 'tldr'` | Output format (default: `'paragraph'`) |
|
|
430
|
+
|
|
431
|
+
**Streaming:** `streamSummarize(text, onToken, options?)`
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
### `translate(text, options)`
|
|
436
|
+
|
|
437
|
+
Translates text to another language.
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
function translate(text: string, options: LLMTranslateOptions): Promise<LLMResponse>
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
| Parameter | Type | Description |
|
|
444
|
+
|-----------|------|-------------|
|
|
445
|
+
| `text` | `string` | Text to translate |
|
|
446
|
+
| `options.to` | `string` | Target language (required) |
|
|
447
|
+
| `options.from` | `string` | Source language (auto-detected if omitted) |
|
|
448
|
+
| `options.tone` | `'formal' \| 'informal' \| 'neutral'` | Translation tone (default: `'neutral'`) |
|
|
449
|
+
|
|
450
|
+
**Streaming:** `streamTranslate(text, onToken, options)`
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
### `rewrite(text, options)`
|
|
455
|
+
|
|
456
|
+
Rewrites text in a different style.
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
function rewrite(text: string, options: LLMRewriteOptions): Promise<LLMResponse>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
| Parameter | Type | Description |
|
|
463
|
+
|-----------|------|-------------|
|
|
464
|
+
| `text` | `string` | Text to rewrite |
|
|
465
|
+
| `options.style` | `string` | Target style (required) |
|
|
466
|
+
|
|
467
|
+
**Available styles:** `'formal'`, `'casual'`, `'professional'`, `'friendly'`, `'concise'`, `'detailed'`, `'simple'`, `'academic'`
|
|
468
|
+
|
|
469
|
+
**Streaming:** `streamRewrite(text, onToken, options)`
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
### `extractKeyPoints(text, options?)`
|
|
474
|
+
|
|
475
|
+
Extracts key points from text as bullet points.
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
function extractKeyPoints(text: string, options?: LLMExtractKeyPointsOptions): Promise<LLMResponse>
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
| Parameter | Type | Description |
|
|
482
|
+
|-----------|------|-------------|
|
|
483
|
+
| `text` | `string` | Text to analyze |
|
|
484
|
+
| `options.maxPoints` | `number` | Maximum points to extract (default: `5`) |
|
|
485
|
+
|
|
486
|
+
**Streaming:** `streamExtractKeyPoints(text, onToken, options?)`
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
### `answerQuestion(question, context, options?)`
|
|
491
|
+
|
|
492
|
+
Answers a question based on provided context.
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
function answerQuestion(question: string, context: string, options?: LLMAnswerQuestionOptions): Promise<LLMResponse>
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
| Parameter | Type | Description |
|
|
499
|
+
|-----------|------|-------------|
|
|
500
|
+
| `question` | `string` | Question to answer |
|
|
501
|
+
| `context` | `string` | Context/document to base answer on |
|
|
502
|
+
| `options.detail` | `'brief' \| 'medium' \| 'detailed'` | Answer detail level (default: `'medium'`) |
|
|
503
|
+
|
|
504
|
+
**Streaming:** `streamAnswerQuestion(question, context, onToken, options?)`
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
267
508
|
### Types
|
|
268
509
|
|
|
269
510
|
```typescript
|
|
@@ -279,10 +520,52 @@ type LLMSendOptions = {
|
|
|
279
520
|
systemPrompt?: string;
|
|
280
521
|
};
|
|
281
522
|
|
|
523
|
+
type LLMStreamOptions = {
|
|
524
|
+
/** Fallback system prompt if no system message in messages array */
|
|
525
|
+
systemPrompt?: string;
|
|
526
|
+
};
|
|
527
|
+
|
|
282
528
|
type LLMResponse = {
|
|
283
529
|
/** The generated response text */
|
|
284
530
|
text: string;
|
|
285
531
|
};
|
|
532
|
+
|
|
533
|
+
type LLMStreamEvent = {
|
|
534
|
+
/** Unique identifier for this streaming session */
|
|
535
|
+
sessionId: string;
|
|
536
|
+
/** The token/chunk of text received */
|
|
537
|
+
token: string;
|
|
538
|
+
/** Accumulated text so far */
|
|
539
|
+
accumulatedText: string;
|
|
540
|
+
/** Whether this is the final chunk */
|
|
541
|
+
isDone: boolean;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
type LLMStreamCallback = (event: LLMStreamEvent) => void;
|
|
545
|
+
|
|
546
|
+
// Prompt Helper Types
|
|
547
|
+
type LLMSummarizeOptions = {
|
|
548
|
+
length?: 'short' | 'medium' | 'long';
|
|
549
|
+
style?: 'paragraph' | 'bullets' | 'tldr';
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
type LLMTranslateOptions = {
|
|
553
|
+
to: string;
|
|
554
|
+
from?: string;
|
|
555
|
+
tone?: 'formal' | 'informal' | 'neutral';
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
type LLMRewriteOptions = {
|
|
559
|
+
style: 'formal' | 'casual' | 'professional' | 'friendly' | 'concise' | 'detailed' | 'simple' | 'academic';
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
type LLMExtractKeyPointsOptions = {
|
|
563
|
+
maxPoints?: number;
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
type LLMAnswerQuestionOptions = {
|
|
567
|
+
detail?: 'brief' | 'medium' | 'detailed';
|
|
568
|
+
};
|
|
286
569
|
```
|
|
287
570
|
|
|
288
571
|
## Feature Comparison
|
|
@@ -291,8 +574,11 @@ type LLMResponse = {
|
|
|
291
574
|
|---------|---------|---------------------|
|
|
292
575
|
| `isAvailable()` | ✅ | ✅ |
|
|
293
576
|
| `sendMessage()` | ✅ | ✅ |
|
|
577
|
+
| `streamMessage()` | ✅ | ✅ |
|
|
578
|
+
| Prompt helpers | ✅ | ✅ |
|
|
294
579
|
| System prompts | ✅ Native | ✅ Prepended |
|
|
295
580
|
| Multi-turn context | ✅ | ✅ |
|
|
581
|
+
| Cancel streaming | ✅ | ✅ |
|
|
296
582
|
|
|
297
583
|
## How It Works
|
|
298
584
|
|
|
@@ -312,7 +598,7 @@ Uses Google's ML Kit Prompt API. The model may need to be downloaded on first us
|
|
|
312
598
|
- **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)
|
|
313
599
|
- **Model downloading**: On first use, the model may need to download. Use `isAvailable()` to check status
|
|
314
600
|
|
|
315
|
-
## Migration from v0.1.
|
|
601
|
+
## Migration from v0.1.4
|
|
316
602
|
|
|
317
603
|
If you're upgrading from an earlier version, here are the breaking changes:
|
|
318
604
|
|
|
@@ -335,6 +621,18 @@ const { reply } = await sendMessage(sessionId, messages, {});
|
|
|
335
621
|
const { text } = await sendMessage(messages, { systemPrompt: '...' });
|
|
336
622
|
```
|
|
337
623
|
|
|
624
|
+
## Roadmap
|
|
625
|
+
|
|
626
|
+
| Feature | Status | Priority |
|
|
627
|
+
|---------|--------|----------|
|
|
628
|
+
| ✅ Streaming responses | Done | - |
|
|
629
|
+
| ✅ Prompt helpers (summarize, translate, etc.) | Done | - |
|
|
630
|
+
| Conversation memory (useChat hook) | Planned | High |
|
|
631
|
+
| Web/generic fallback | Idea | Medium |
|
|
632
|
+
| Configurable hyperparameters (temperature, etc.) | Idea | Low |
|
|
633
|
+
|
|
634
|
+
Have a feature request? [Open an issue](https://github.com/laraelmas/expo-ai-kit/issues)!
|
|
635
|
+
|
|
338
636
|
## License
|
|
339
637
|
|
|
340
638
|
MIT
|
|
@@ -3,14 +3,24 @@ package expo.modules.aikit
|
|
|
3
3
|
import expo.modules.kotlin.modules.Module
|
|
4
4
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
5
|
import expo.modules.kotlin.functions.Coroutine
|
|
6
|
+
import kotlinx.coroutines.Job
|
|
7
|
+
import kotlinx.coroutines.CoroutineScope
|
|
8
|
+
import kotlinx.coroutines.Dispatchers
|
|
9
|
+
import kotlinx.coroutines.launch
|
|
10
|
+
import kotlinx.coroutines.cancel
|
|
6
11
|
|
|
7
12
|
class ExpoAiKitModule : Module() {
|
|
8
13
|
|
|
9
14
|
private val promptClient by lazy { PromptApiClient() }
|
|
15
|
+
private val activeStreamJobs = mutableMapOf<String, Job>()
|
|
16
|
+
private val streamScope = CoroutineScope(Dispatchers.IO)
|
|
10
17
|
|
|
11
18
|
override fun definition() = ModuleDefinition {
|
|
12
19
|
Name("ExpoAiKit")
|
|
13
20
|
|
|
21
|
+
// Declare events that can be sent to JavaScript
|
|
22
|
+
Events("onStreamToken")
|
|
23
|
+
|
|
14
24
|
Function("isAvailable") {
|
|
15
25
|
promptClient.isAvailableBlocking()
|
|
16
26
|
}
|
|
@@ -31,5 +41,42 @@ class ExpoAiKitModule : Module() {
|
|
|
31
41
|
val text = promptClient.generateText(userPrompt, systemPrompt)
|
|
32
42
|
mapOf("text" to text)
|
|
33
43
|
}
|
|
44
|
+
|
|
45
|
+
AsyncFunction("startStreaming") { messages: List<Map<String, Any>>, fallbackSystemPrompt: String, sessionId: String ->
|
|
46
|
+
// Extract system prompt from messages, or use fallback
|
|
47
|
+
val systemPrompt = messages
|
|
48
|
+
.firstOrNull { it["role"] == "system" }
|
|
49
|
+
?.get("content") as? String
|
|
50
|
+
?: fallbackSystemPrompt.ifBlank { "You are a helpful, friendly assistant." }
|
|
51
|
+
|
|
52
|
+
// Get the last user message as the prompt
|
|
53
|
+
val userPrompt = messages
|
|
54
|
+
.lastOrNull { it["role"] == "user" }
|
|
55
|
+
?.get("content") as? String
|
|
56
|
+
?: ""
|
|
57
|
+
|
|
58
|
+
// Launch streaming in a coroutine that can be cancelled
|
|
59
|
+
val job = streamScope.launch {
|
|
60
|
+
promptClient.generateTextStream(userPrompt, systemPrompt) { token, accumulatedText, isDone ->
|
|
61
|
+
sendEvent("onStreamToken", mapOf(
|
|
62
|
+
"sessionId" to sessionId,
|
|
63
|
+
"token" to token,
|
|
64
|
+
"accumulatedText" to accumulatedText,
|
|
65
|
+
"isDone" to isDone
|
|
66
|
+
))
|
|
67
|
+
|
|
68
|
+
if (isDone) {
|
|
69
|
+
activeStreamJobs.remove(sessionId)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
activeStreamJobs[sessionId] = job
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
AsyncFunction("stopStreaming") { sessionId: String ->
|
|
78
|
+
activeStreamJobs[sessionId]?.cancel()
|
|
79
|
+
activeStreamJobs.remove(sessionId)
|
|
80
|
+
}
|
|
34
81
|
}
|
|
35
82
|
}
|
|
@@ -4,6 +4,8 @@ import android.os.Build
|
|
|
4
4
|
import com.google.mlkit.genai.common.FeatureStatus
|
|
5
5
|
import com.google.mlkit.genai.prompt.Generation
|
|
6
6
|
import kotlinx.coroutines.Dispatchers
|
|
7
|
+
import kotlinx.coroutines.Job
|
|
8
|
+
import kotlinx.coroutines.flow.Flow
|
|
7
9
|
import kotlinx.coroutines.runBlocking
|
|
8
10
|
import kotlinx.coroutines.withContext
|
|
9
11
|
|
|
@@ -64,4 +66,47 @@ class PromptApiClient {
|
|
|
64
66
|
""
|
|
65
67
|
}
|
|
66
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate streaming text from a prompt.
|
|
72
|
+
* Returns a Flow that emits chunks of generated text.
|
|
73
|
+
*/
|
|
74
|
+
suspend fun generateTextStream(
|
|
75
|
+
prompt: String,
|
|
76
|
+
systemPrompt: String,
|
|
77
|
+
onChunk: (token: String, accumulatedText: String, isDone: Boolean) -> Unit
|
|
78
|
+
) = withContext(Dispatchers.IO) {
|
|
79
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
80
|
+
onChunk("", "", true)
|
|
81
|
+
return@withContext
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
val status = model.checkStatus()
|
|
86
|
+
if (status != FeatureStatus.AVAILABLE) {
|
|
87
|
+
onChunk("", "", true)
|
|
88
|
+
return@withContext
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Prepend system prompt as context if provided
|
|
92
|
+
val fullPrompt = if (systemPrompt.isNotBlank()) {
|
|
93
|
+
"$systemPrompt\n\nUser: $prompt"
|
|
94
|
+
} else {
|
|
95
|
+
prompt
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var accumulatedText = ""
|
|
99
|
+
|
|
100
|
+
model.generateContentStream(fullPrompt).collect { response ->
|
|
101
|
+
val newChunk = response.candidates.firstOrNull()?.text.orEmpty()
|
|
102
|
+
accumulatedText += newChunk
|
|
103
|
+
onChunk(newChunk, accumulatedText, false)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Send final done event
|
|
107
|
+
onChunk("", accumulatedText, true)
|
|
108
|
+
} catch (e: Throwable) {
|
|
109
|
+
onChunk("", "[Error: ${e.message}]", true)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
67
112
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import type { EventSubscription } from 'expo-modules-core';
|
|
2
|
+
import { LLMMessage, LLMResponse, LLMStreamEvent } from './types';
|
|
3
|
+
export type ExpoAiKitModuleEvents = {
|
|
4
|
+
onStreamToken: (event: LLMStreamEvent) => void;
|
|
5
|
+
};
|
|
6
|
+
export interface ExpoAiKitNativeModule {
|
|
3
7
|
isAvailable(): boolean;
|
|
4
8
|
sendMessage(messages: LLMMessage[], systemPrompt: string): Promise<LLMResponse>;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
startStreaming(messages: LLMMessage[], systemPrompt: string, sessionId: string): Promise<void>;
|
|
10
|
+
stopStreaming(sessionId: string): Promise<void>;
|
|
11
|
+
addListener<K extends keyof ExpoAiKitModuleEvents>(eventName: K, listener: ExpoAiKitModuleEvents[K]): EventSubscription;
|
|
12
|
+
}
|
|
13
|
+
declare const ExpoAiKitModule: ExpoAiKitNativeModule;
|
|
14
|
+
export default ExpoAiKitModule;
|
|
8
15
|
//# sourceMappingURL=ExpoAiKitModule.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoAiKitModule.d.ts","sourceRoot":"","sources":["../src/ExpoAiKitModule.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"ExpoAiKitModule.d.ts","sourceRoot":"","sources":["../src/ExpoAiKitModule.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAElE,MAAM,MAAM,qBAAqB,GAAG;IAClC,aAAa,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,WAAW,IAAI,OAAO,CAAC;IACvB,WAAW,CACT,QAAQ,EAAE,UAAU,EAAE,EACtB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,WAAW,CAAC,CAAC;IACxB,cAAc,CACZ,QAAQ,EAAE,UAAU,EAAE,EACtB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,WAAW,CAAC,CAAC,SAAS,MAAM,qBAAqB,EAC/C,SAAS,EAAE,CAAC,EACZ,QAAQ,EAAE,qBAAqB,CAAC,CAAC,CAAC,GACjC,iBAAiB,CAAC;CACtB;AAED,QAAA,MAAM,eAAe,uBACoC,CAAC;AAE1D,eAAe,eAAe,CAAC"}
|
package/build/ExpoAiKitModule.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { requireNativeModule } from 'expo-modules-core';
|
|
2
|
-
const
|
|
3
|
-
export default
|
|
2
|
+
const ExpoAiKitModule = requireNativeModule('ExpoAiKit');
|
|
3
|
+
export default ExpoAiKitModule;
|
|
4
4
|
//# sourceMappingURL=ExpoAiKitModule.js.map
|
|
@@ -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;AA0BxD,MAAM,eAAe,GACnB,mBAAmB,CAAwB,WAAW,CAAC,CAAC;AAE1D,eAAe,eAAe,CAAC","sourcesContent":["import { requireNativeModule } from 'expo-modules-core';\nimport type { EventSubscription } from 'expo-modules-core';\nimport { LLMMessage, LLMResponse, LLMStreamEvent } from './types';\n\nexport type ExpoAiKitModuleEvents = {\n onStreamToken: (event: LLMStreamEvent) => void;\n};\n\nexport interface ExpoAiKitNativeModule {\n isAvailable(): boolean;\n sendMessage(\n messages: LLMMessage[],\n systemPrompt: string\n ): Promise<LLMResponse>;\n startStreaming(\n messages: LLMMessage[],\n systemPrompt: string,\n sessionId: string\n ): Promise<void>;\n stopStreaming(sessionId: string): Promise<void>;\n addListener<K extends keyof ExpoAiKitModuleEvents>(\n eventName: K,\n listener: ExpoAiKitModuleEvents[K]\n ): EventSubscription;\n}\n\nconst ExpoAiKitModule =\n requireNativeModule<ExpoAiKitNativeModule>('ExpoAiKit');\n\nexport default ExpoAiKitModule;\n"]}
|