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 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
- - **Simple API** — Just 2 functions: `isAvailable()` and `sendMessage()`
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.x
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 { LLMMessage, LLMResponse } from './types';
2
- export type ExpoAiKitNativeModule = {
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
- declare const NativeModule: ExpoAiKitNativeModule;
7
- export default NativeModule;
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;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
+ {"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"}
@@ -1,4 +1,4 @@
1
1
  import { requireNativeModule } from 'expo-modules-core';
2
- const NativeModule = requireNativeModule('ExpoAiKit');
3
- export default NativeModule;
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;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"]}
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"]}