expo-ai-kit 0.3.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -7,15 +7,6 @@ import {
7
7
  LLMStreamOptions,
8
8
  LLMStreamEvent,
9
9
  LLMStreamCallback,
10
- LLMSummarizeOptions,
11
- LLMTranslateOptions,
12
- LLMRewriteOptions,
13
- LLMExtractKeyPointsOptions,
14
- LLMAnswerQuestionOptions,
15
- LLMSuggestOptions,
16
- LLMSuggestResponse,
17
- LLMSmartReplyOptions,
18
- LLMAutocompleteOptions,
19
10
  BuiltInModel,
20
11
  DownloadableModel,
21
12
  ModelError,
@@ -24,8 +15,6 @@ import {
24
15
  import { MODEL_REGISTRY, getRegistryEntry } from './models';
25
16
 
26
17
  export * from './types';
27
- export * from './memory';
28
- export * from './hooks';
29
18
  export * from './models';
30
19
 
31
20
  const DEFAULT_SYSTEM_PROMPT =
@@ -37,140 +26,9 @@ function generateSessionId(): string {
37
26
  }
38
27
 
39
28
  // ============================================================================
40
- // Prompt Helper Constants
29
+ // Inference API
41
30
  // ============================================================================
42
31
 
43
- const SUMMARIZE_LENGTH_INSTRUCTIONS = {
44
- short: 'Keep it very brief, around 1-2 sentences.',
45
- medium: 'Provide a moderate summary, around 3-5 sentences.',
46
- long: 'Provide a comprehensive summary covering all main points.',
47
- } as const;
48
-
49
- const SUMMARIZE_STYLE_INSTRUCTIONS = {
50
- paragraph: 'Write the summary as a flowing paragraph.',
51
- bullets: 'Format the summary as bullet points.',
52
- tldr: 'Start with "TL;DR:" and give an extremely concise summary in 1 sentence.',
53
- } as const;
54
-
55
- const TRANSLATE_TONE_INSTRUCTIONS = {
56
- formal: 'Use formal language and honorifics where appropriate.',
57
- informal: 'Use casual, everyday language.',
58
- neutral: 'Use standard, neutral language.',
59
- } as const;
60
-
61
- const REWRITE_STYLE_INSTRUCTIONS = {
62
- formal:
63
- 'Rewrite in a formal, professional tone suitable for business communication.',
64
- casual: 'Rewrite in a casual, conversational tone.',
65
- professional:
66
- 'Rewrite in a clear, professional tone suitable for work contexts.',
67
- friendly: 'Rewrite in a warm, friendly tone.',
68
- concise:
69
- 'Rewrite to be as brief as possible while keeping the meaning intact.',
70
- detailed: 'Expand and add more detail and explanation.',
71
- simple:
72
- 'Rewrite using simple words and short sentences, easy for anyone to understand.',
73
- academic: 'Rewrite in an academic style suitable for scholarly writing.',
74
- } as const;
75
-
76
- const SUGGEST_TONE_INSTRUCTIONS = {
77
- formal: 'Use formal, professional language.',
78
- casual: 'Use casual, everyday language.',
79
- professional: 'Use clear, professional language suitable for work.',
80
- friendly: 'Use warm, friendly language.',
81
- neutral: 'Use standard, neutral language.',
82
- } as const;
83
-
84
- const ANSWER_DETAIL_INSTRUCTIONS = {
85
- brief: 'Give a brief, direct answer in 1-2 sentences.',
86
- medium: 'Provide a clear answer with some explanation.',
87
- detailed:
88
- 'Provide a comprehensive answer with full explanation and relevant details from the context.',
89
- } as const;
90
-
91
- // ============================================================================
92
- // Prompt Builder Helpers
93
- // ============================================================================
94
-
95
- function buildSummarizePrompt(
96
- length: 'short' | 'medium' | 'long',
97
- style: 'paragraph' | 'bullets' | 'tldr'
98
- ): string {
99
- return `You are a summarization assistant. Summarize the provided text accurately and concisely. ${SUMMARIZE_LENGTH_INSTRUCTIONS[length]} ${SUMMARIZE_STYLE_INSTRUCTIONS[style]} Only output the summary, nothing else.`;
100
- }
101
-
102
- function buildTranslatePrompt(
103
- to: string,
104
- from: string | undefined,
105
- tone: 'formal' | 'informal' | 'neutral'
106
- ): string {
107
- const fromClause = from ? `from ${from} ` : '';
108
- return `You are a translation assistant. Translate the provided text ${fromClause}to ${to}. ${TRANSLATE_TONE_INSTRUCTIONS[tone]} Only output the translation, nothing else. Do not include any explanations or notes.`;
109
- }
110
-
111
- function buildRewritePrompt(
112
- style:
113
- | 'formal'
114
- | 'casual'
115
- | 'professional'
116
- | 'friendly'
117
- | 'concise'
118
- | 'detailed'
119
- | 'simple'
120
- | 'academic'
121
- ): string {
122
- return `You are a writing assistant. ${REWRITE_STYLE_INSTRUCTIONS[style]} Preserve the original meaning. Only output the rewritten text, nothing else.`;
123
- }
124
-
125
- function buildExtractKeyPointsPrompt(maxPoints: number): string {
126
- return `You are an analysis assistant. Extract the ${maxPoints} most important key points from the provided text. Format each point as a bullet point starting with "•". Be concise and focus on the most significant information. Only output the bullet points, nothing else.`;
127
- }
128
-
129
- function buildSuggestPrompt(
130
- count: number,
131
- tone: 'formal' | 'casual' | 'professional' | 'friendly' | 'neutral',
132
- context?: string
133
- ): string {
134
- const contextClause = context
135
- ? ` The user is writing in this context: "${context}".`
136
- : '';
137
- return `You are a text suggestion assistant. Given the user's partial text, generate exactly ${count} possible continuations or completions.${contextClause} ${SUGGEST_TONE_INSTRUCTIONS[tone]} Output ONLY the suggestions, one per line, numbered like "1. suggestion here". Do not include any other text or explanation.`;
138
- }
139
-
140
- function buildSmartReplyPrompt(
141
- count: number,
142
- tone: 'formal' | 'casual' | 'professional' | 'friendly' | 'neutral',
143
- persona?: string
144
- ): string {
145
- const personaClause = persona ? ` You are replying as: ${persona}.` : '';
146
- return `You are a smart reply assistant. Given a conversation, generate exactly ${count} short, contextually appropriate reply suggestions that the user could send as their next message.${personaClause} ${SUGGEST_TONE_INSTRUCTIONS[tone]} Each reply should be a complete, ready-to-send message. Output ONLY the replies, one per line, numbered like "1. reply here". Do not include any other text or explanation.`;
147
- }
148
-
149
- function buildAutocompletePrompt(
150
- count: number,
151
- maxWords: number,
152
- context?: string
153
- ): string {
154
- const contextClause = context
155
- ? ` The user is writing about: "${context}".`
156
- : '';
157
- return `You are an autocomplete assistant. Given the user's partial text, generate exactly ${count} natural completions of the current sentence or phrase. Each completion should be at most ${maxWords} words and should seamlessly continue from the user's text.${contextClause} Output ONLY the completions, one per line, numbered like "1. completion here". Do not repeat the user's text. Do not include any other text or explanation.`;
158
- }
159
-
160
- function parseSuggestions(raw: string): { text: string }[] {
161
- return raw
162
- .split('\n')
163
- .map((line) => line.replace(/^\d+[\.\)]\s*/, '').trim())
164
- .filter((line) => line.length > 0)
165
- .map((text) => ({ text }));
166
- }
167
-
168
- function buildAnswerQuestionPrompt(
169
- detail: 'brief' | 'medium' | 'detailed'
170
- ): string {
171
- return `You are a question-answering assistant. Answer questions based ONLY on the provided context. ${ANSWER_DETAIL_INSTRUCTIONS[detail]} If the answer cannot be found in the context, say so. Do not make up information.`;
172
- }
173
-
174
32
  /**
175
33
  * Check if on-device AI is available on the current device.
176
34
  * Returns false on unsupported platforms (web, etc.).
@@ -270,17 +128,6 @@ export async function sendMessage(
270
128
  * // Cancel after 5 seconds
271
129
  * setTimeout(() => stop(), 5000);
272
130
  * ```
273
- *
274
- * @example
275
- * ```ts
276
- * // React state update pattern
277
- * const [text, setText] = useState('');
278
- *
279
- * streamMessage(
280
- * [{ role: 'user', content: 'Hello!' }],
281
- * (event) => setText(event.accumulatedText)
282
- * );
283
- * ```
284
131
  */
285
132
  export function streamMessage(
286
133
  messages: LLMMessage[],
@@ -353,795 +200,6 @@ export function streamMessage(
353
200
  return { promise, stop };
354
201
  }
355
202
 
356
- // ============================================================================
357
- // Prompt Helpers
358
- // ============================================================================
359
-
360
- /**
361
- * Summarize text content using on-device AI.
362
- *
363
- * @param text - The text to summarize
364
- * @param options - Optional settings for summary style and length
365
- * @returns Promise with the generated summary
366
- *
367
- * @example
368
- * ```ts
369
- * // Basic summarization
370
- * const result = await summarize(longArticle);
371
- * console.log(result.text);
372
- * ```
373
- *
374
- * @example
375
- * ```ts
376
- * // Short bullet-point summary
377
- * const result = await summarize(longArticle, {
378
- * length: 'short',
379
- * style: 'bullets'
380
- * });
381
- * ```
382
- *
383
- * @example
384
- * ```ts
385
- * // TL;DR style
386
- * const result = await summarize(longArticle, {
387
- * style: 'tldr'
388
- * });
389
- * ```
390
- */
391
- export async function summarize(
392
- text: string,
393
- options?: LLMSummarizeOptions
394
- ): Promise<LLMResponse> {
395
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
396
- return { text: '' };
397
- }
398
-
399
- if (!text || text.trim().length === 0) {
400
- throw new Error('text cannot be empty');
401
- }
402
-
403
- const length = options?.length ?? 'medium';
404
- const style = options?.style ?? 'paragraph';
405
- const systemPrompt = buildSummarizePrompt(length, style);
406
-
407
- return sendMessage([{ role: 'user', content: text }], { systemPrompt });
408
- }
409
-
410
- /**
411
- * Summarize text with streaming output.
412
- *
413
- * @param text - The text to summarize
414
- * @param onToken - Callback for each token received
415
- * @param options - Optional settings for summary style and length
416
- * @returns Object with stop() function and promise
417
- *
418
- * @example
419
- * ```ts
420
- * const { promise } = streamSummarize(
421
- * longArticle,
422
- * (event) => setSummary(event.accumulatedText),
423
- * { style: 'bullets' }
424
- * );
425
- * await promise;
426
- * ```
427
- */
428
- export function streamSummarize(
429
- text: string,
430
- onToken: LLMStreamCallback,
431
- options?: LLMSummarizeOptions
432
- ): { promise: Promise<LLMResponse>; stop: () => void } {
433
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
434
- return { promise: Promise.resolve({ text: '' }), stop: () => {} };
435
- }
436
-
437
- if (!text || text.trim().length === 0) {
438
- return {
439
- promise: Promise.reject(new Error('text cannot be empty')),
440
- stop: () => {},
441
- };
442
- }
443
-
444
- const length = options?.length ?? 'medium';
445
- const style = options?.style ?? 'paragraph';
446
- const systemPrompt = buildSummarizePrompt(length, style);
447
-
448
- return streamMessage([{ role: 'user', content: text }], onToken, {
449
- systemPrompt,
450
- });
451
- }
452
-
453
- /**
454
- * Translate text to another language using on-device AI.
455
- *
456
- * @param text - The text to translate
457
- * @param options - Translation options including target language
458
- * @returns Promise with the translated text
459
- *
460
- * @example
461
- * ```ts
462
- * // Basic translation
463
- * const result = await translate('Hello, world!', { to: 'Spanish' });
464
- * console.log(result.text); // "¡Hola, mundo!"
465
- * ```
466
- *
467
- * @example
468
- * ```ts
469
- * // Formal translation with source language
470
- * const result = await translate('Hey, what\'s up?', {
471
- * to: 'French',
472
- * from: 'English',
473
- * tone: 'formal'
474
- * });
475
- * ```
476
- */
477
- export async function translate(
478
- text: string,
479
- options: LLMTranslateOptions
480
- ): Promise<LLMResponse> {
481
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
482
- return { text: '' };
483
- }
484
-
485
- if (!text || text.trim().length === 0) {
486
- throw new Error('text cannot be empty');
487
- }
488
-
489
- const { to, from, tone = 'neutral' } = options;
490
- const systemPrompt = buildTranslatePrompt(to, from, tone);
491
-
492
- return sendMessage([{ role: 'user', content: text }], { systemPrompt });
493
- }
494
-
495
- /**
496
- * Translate text with streaming output.
497
- *
498
- * @param text - The text to translate
499
- * @param onToken - Callback for each token received
500
- * @param options - Translation options including target language
501
- * @returns Object with stop() function and promise
502
- *
503
- * @example
504
- * ```ts
505
- * const { promise } = streamTranslate(
506
- * 'Hello, world!',
507
- * (event) => setTranslation(event.accumulatedText),
508
- * { to: 'Japanese' }
509
- * );
510
- * await promise;
511
- * ```
512
- */
513
- export function streamTranslate(
514
- text: string,
515
- onToken: LLMStreamCallback,
516
- options: LLMTranslateOptions
517
- ): { promise: Promise<LLMResponse>; stop: () => void } {
518
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
519
- return { promise: Promise.resolve({ text: '' }), stop: () => {} };
520
- }
521
-
522
- if (!text || text.trim().length === 0) {
523
- return {
524
- promise: Promise.reject(new Error('text cannot be empty')),
525
- stop: () => {},
526
- };
527
- }
528
-
529
- const { to, from, tone = 'neutral' } = options;
530
- const systemPrompt = buildTranslatePrompt(to, from, tone);
531
-
532
- return streamMessage([{ role: 'user', content: text }], onToken, {
533
- systemPrompt,
534
- });
535
- }
536
-
537
- /**
538
- * Rewrite text in a different style using on-device AI.
539
- *
540
- * @param text - The text to rewrite
541
- * @param options - Rewrite options specifying the target style
542
- * @returns Promise with the rewritten text
543
- *
544
- * @example
545
- * ```ts
546
- * // Make text more formal
547
- * const result = await rewrite('hey can u help me out?', {
548
- * style: 'formal'
549
- * });
550
- * console.log(result.text); // "Would you be able to assist me?"
551
- * ```
552
- *
553
- * @example
554
- * ```ts
555
- * // Simplify complex text
556
- * const result = await rewrite(technicalText, { style: 'simple' });
557
- * ```
558
- */
559
- export async function rewrite(
560
- text: string,
561
- options: LLMRewriteOptions
562
- ): Promise<LLMResponse> {
563
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
564
- return { text: '' };
565
- }
566
-
567
- if (!text || text.trim().length === 0) {
568
- throw new Error('text cannot be empty');
569
- }
570
-
571
- const { style } = options;
572
- const systemPrompt = buildRewritePrompt(style);
573
-
574
- return sendMessage([{ role: 'user', content: text }], { systemPrompt });
575
- }
576
-
577
- /**
578
- * Rewrite text with streaming output.
579
- *
580
- * @param text - The text to rewrite
581
- * @param onToken - Callback for each token received
582
- * @param options - Rewrite options specifying the target style
583
- * @returns Object with stop() function and promise
584
- *
585
- * @example
586
- * ```ts
587
- * const { promise } = streamRewrite(
588
- * 'hey whats up',
589
- * (event) => setRewritten(event.accumulatedText),
590
- * { style: 'professional' }
591
- * );
592
- * await promise;
593
- * ```
594
- */
595
- export function streamRewrite(
596
- text: string,
597
- onToken: LLMStreamCallback,
598
- options: LLMRewriteOptions
599
- ): { promise: Promise<LLMResponse>; stop: () => void } {
600
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
601
- return { promise: Promise.resolve({ text: '' }), stop: () => {} };
602
- }
603
-
604
- if (!text || text.trim().length === 0) {
605
- return {
606
- promise: Promise.reject(new Error('text cannot be empty')),
607
- stop: () => {},
608
- };
609
- }
610
-
611
- const { style } = options;
612
- const systemPrompt = buildRewritePrompt(style);
613
-
614
- return streamMessage([{ role: 'user', content: text }], onToken, {
615
- systemPrompt,
616
- });
617
- }
618
-
619
- /**
620
- * Extract key points from text using on-device AI.
621
- *
622
- * @param text - The text to extract key points from
623
- * @param options - Optional settings for extraction
624
- * @returns Promise with the key points as text
625
- *
626
- * @example
627
- * ```ts
628
- * // Extract key points from an article
629
- * const result = await extractKeyPoints(article);
630
- * console.log(result.text);
631
- * // "• Point 1\n• Point 2\n• Point 3"
632
- * ```
633
- *
634
- * @example
635
- * ```ts
636
- * // Limit to 3 key points
637
- * const result = await extractKeyPoints(article, { maxPoints: 3 });
638
- * ```
639
- */
640
- export async function extractKeyPoints(
641
- text: string,
642
- options?: LLMExtractKeyPointsOptions
643
- ): Promise<LLMResponse> {
644
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
645
- return { text: '' };
646
- }
647
-
648
- if (!text || text.trim().length === 0) {
649
- throw new Error('text cannot be empty');
650
- }
651
-
652
- const maxPoints = options?.maxPoints ?? 5;
653
- const systemPrompt = buildExtractKeyPointsPrompt(maxPoints);
654
-
655
- return sendMessage([{ role: 'user', content: text }], { systemPrompt });
656
- }
657
-
658
- /**
659
- * Extract key points with streaming output.
660
- *
661
- * @param text - The text to extract key points from
662
- * @param onToken - Callback for each token received
663
- * @param options - Optional settings for extraction
664
- * @returns Object with stop() function and promise
665
- *
666
- * @example
667
- * ```ts
668
- * const { promise } = streamExtractKeyPoints(
669
- * article,
670
- * (event) => setKeyPoints(event.accumulatedText),
671
- * { maxPoints: 5 }
672
- * );
673
- * await promise;
674
- * ```
675
- */
676
- export function streamExtractKeyPoints(
677
- text: string,
678
- onToken: LLMStreamCallback,
679
- options?: LLMExtractKeyPointsOptions
680
- ): { promise: Promise<LLMResponse>; stop: () => void } {
681
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
682
- return { promise: Promise.resolve({ text: '' }), stop: () => {} };
683
- }
684
-
685
- if (!text || text.trim().length === 0) {
686
- return {
687
- promise: Promise.reject(new Error('text cannot be empty')),
688
- stop: () => {},
689
- };
690
- }
691
-
692
- const maxPoints = options?.maxPoints ?? 5;
693
- const systemPrompt = buildExtractKeyPointsPrompt(maxPoints);
694
-
695
- return streamMessage([{ role: 'user', content: text }], onToken, {
696
- systemPrompt,
697
- });
698
- }
699
-
700
- /**
701
- * Answer a question based on provided context using on-device AI.
702
- *
703
- * @param question - The question to answer
704
- * @param context - The context/document to base the answer on
705
- * @param options - Optional settings for the answer
706
- * @returns Promise with the answer
707
- *
708
- * @example
709
- * ```ts
710
- * // Answer a question about a document
711
- * const result = await answerQuestion(
712
- * 'What is the main topic?',
713
- * documentText
714
- * );
715
- * console.log(result.text);
716
- * ```
717
- *
718
- * @example
719
- * ```ts
720
- * // Get a detailed answer
721
- * const result = await answerQuestion(
722
- * 'Explain the methodology',
723
- * researchPaper,
724
- * { detail: 'detailed' }
725
- * );
726
- * ```
727
- */
728
- export async function answerQuestion(
729
- question: string,
730
- context: string,
731
- options?: LLMAnswerQuestionOptions
732
- ): Promise<LLMResponse> {
733
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
734
- return { text: '' };
735
- }
736
-
737
- if (!question || question.trim().length === 0) {
738
- throw new Error('question cannot be empty');
739
- }
740
-
741
- if (!context || context.trim().length === 0) {
742
- throw new Error('context cannot be empty');
743
- }
744
-
745
- const detail = options?.detail ?? 'medium';
746
- const systemPrompt = buildAnswerQuestionPrompt(detail);
747
- const userContent = `Context:\n${context}\n\nQuestion: ${question}`;
748
-
749
- return sendMessage([{ role: 'user', content: userContent }], { systemPrompt });
750
- }
751
-
752
- /**
753
- * Answer a question with streaming output.
754
- *
755
- * @param question - The question to answer
756
- * @param context - The context/document to base the answer on
757
- * @param onToken - Callback for each token received
758
- * @param options - Optional settings for the answer
759
- * @returns Object with stop() function and promise
760
- *
761
- * @example
762
- * ```ts
763
- * const { promise } = streamAnswerQuestion(
764
- * 'What are the key findings?',
765
- * documentText,
766
- * (event) => setAnswer(event.accumulatedText),
767
- * { detail: 'detailed' }
768
- * );
769
- * await promise;
770
- * ```
771
- */
772
- export function streamAnswerQuestion(
773
- question: string,
774
- context: string,
775
- onToken: LLMStreamCallback,
776
- options?: LLMAnswerQuestionOptions
777
- ): { promise: Promise<LLMResponse>; stop: () => void } {
778
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
779
- return { promise: Promise.resolve({ text: '' }), stop: () => {} };
780
- }
781
-
782
- if (!question || question.trim().length === 0) {
783
- return {
784
- promise: Promise.reject(new Error('question cannot be empty')),
785
- stop: () => {},
786
- };
787
- }
788
-
789
- if (!context || context.trim().length === 0) {
790
- return {
791
- promise: Promise.reject(new Error('context cannot be empty')),
792
- stop: () => {},
793
- };
794
- }
795
-
796
- const detail = options?.detail ?? 'medium';
797
- const systemPrompt = buildAnswerQuestionPrompt(detail);
798
- const userContent = `Context:\n${context}\n\nQuestion: ${question}`;
799
-
800
- return streamMessage([{ role: 'user', content: userContent }], onToken, {
801
- systemPrompt,
802
- });
803
- }
804
-
805
- // ============================================================================
806
- // Smart Suggestions
807
- // ============================================================================
808
-
809
- /**
810
- * Generate text suggestions based on partial input using on-device AI.
811
- *
812
- * Useful for text completion, writing assistance, and predictive text features.
813
- *
814
- * @param partialText - The text the user has typed so far
815
- * @param options - Optional settings for suggestions
816
- * @returns Promise with an array of suggestions
817
- *
818
- * @example
819
- * ```ts
820
- * // Basic suggestions
821
- * const result = await suggest('I think we should');
822
- * result.suggestions.forEach(s => console.log(s.text));
823
- * // "schedule a meeting to discuss this further"
824
- * // "consider an alternative approach"
825
- * // "move forward with the plan"
826
- * ```
827
- *
828
- * @example
829
- * ```ts
830
- * // With context and tone
831
- * const result = await suggest('Dear Mr. Johnson,', {
832
- * count: 5,
833
- * context: 'writing a business email about project delays',
834
- * tone: 'formal'
835
- * });
836
- * ```
837
- */
838
- export async function suggest(
839
- partialText: string,
840
- options?: LLMSuggestOptions
841
- ): Promise<LLMSuggestResponse> {
842
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
843
- return { suggestions: [], raw: '' };
844
- }
845
-
846
- if (!partialText || partialText.trim().length === 0) {
847
- throw new Error('partialText cannot be empty');
848
- }
849
-
850
- const count = options?.count ?? 3;
851
- const tone = options?.tone ?? 'neutral';
852
- const systemPrompt = buildSuggestPrompt(count, tone, options?.context);
853
-
854
- const response = await sendMessage(
855
- [{ role: 'user', content: partialText }],
856
- { systemPrompt }
857
- );
858
-
859
- return {
860
- suggestions: parseSuggestions(response.text),
861
- raw: response.text,
862
- };
863
- }
864
-
865
- /**
866
- * Generate text suggestions with streaming output.
867
- *
868
- * @param partialText - The text the user has typed so far
869
- * @param onToken - Callback for each token received
870
- * @param options - Optional settings for suggestions
871
- * @returns Object with stop() function and promise
872
- *
873
- * @example
874
- * ```ts
875
- * const { promise } = streamSuggest(
876
- * 'The best way to',
877
- * (event) => setRawSuggestions(event.accumulatedText)
878
- * );
879
- * const result = await promise;
880
- * // Parse suggestions from result.text
881
- * ```
882
- */
883
- export function streamSuggest(
884
- partialText: string,
885
- onToken: LLMStreamCallback,
886
- options?: LLMSuggestOptions
887
- ): { promise: Promise<LLMResponse>; stop: () => void } {
888
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
889
- return { promise: Promise.resolve({ text: '' }), stop: () => {} };
890
- }
891
-
892
- if (!partialText || partialText.trim().length === 0) {
893
- return {
894
- promise: Promise.reject(new Error('partialText cannot be empty')),
895
- stop: () => {},
896
- };
897
- }
898
-
899
- const count = options?.count ?? 3;
900
- const tone = options?.tone ?? 'neutral';
901
- const systemPrompt = buildSuggestPrompt(count, tone, options?.context);
902
-
903
- return streamMessage([{ role: 'user', content: partialText }], onToken, {
904
- systemPrompt,
905
- });
906
- }
907
-
908
- /**
909
- * Generate smart reply suggestions for a conversation using on-device AI.
910
- *
911
- * Analyzes the conversation history and generates contextually appropriate
912
- * reply options, similar to Gmail/Messages smart replies.
913
- *
914
- * @param messages - The conversation history to generate replies for
915
- * @param options - Optional settings for reply generation
916
- * @returns Promise with an array of reply suggestions
917
- *
918
- * @example
919
- * ```ts
920
- * // Basic smart replies
921
- * const result = await smartReply([
922
- * { role: 'user', content: 'Hey, are you free for lunch tomorrow?' }
923
- * ]);
924
- * result.suggestions.forEach(s => console.log(s.text));
925
- * // "Sure, what time works for you?"
926
- * // "Sorry, I already have plans."
927
- * // "Let me check my schedule and get back to you."
928
- * ```
929
- *
930
- * @example
931
- * ```ts
932
- * // With persona and tone
933
- * const result = await smartReply(
934
- * [
935
- * { role: 'user', content: 'We need the report by Friday.' },
936
- * { role: 'assistant', content: 'I will do my best.' },
937
- * { role: 'user', content: 'Can you confirm the deadline works?' }
938
- * ],
939
- * { tone: 'professional', persona: 'project manager', count: 4 }
940
- * );
941
- * ```
942
- */
943
- export async function smartReply(
944
- messages: LLMMessage[],
945
- options?: LLMSmartReplyOptions
946
- ): Promise<LLMSuggestResponse> {
947
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
948
- return { suggestions: [], raw: '' };
949
- }
950
-
951
- if (!messages || messages.length === 0) {
952
- throw new Error('messages array cannot be empty');
953
- }
954
-
955
- const count = options?.count ?? 3;
956
- const tone = options?.tone ?? 'neutral';
957
- const systemPrompt = buildSmartReplyPrompt(count, tone, options?.persona);
958
-
959
- const conversation = messages
960
- .map((m) => `${m.role === 'user' ? 'Them' : 'Me'}: ${m.content}`)
961
- .join('\n');
962
-
963
- const response = await sendMessage(
964
- [{ role: 'user', content: `Conversation:\n${conversation}\n\nGenerate ${count} reply suggestions for my next message.` }],
965
- { systemPrompt }
966
- );
967
-
968
- return {
969
- suggestions: parseSuggestions(response.text),
970
- raw: response.text,
971
- };
972
- }
973
-
974
- /**
975
- * Generate smart reply suggestions with streaming output.
976
- *
977
- * @param messages - The conversation history to generate replies for
978
- * @param onToken - Callback for each token received
979
- * @param options - Optional settings for reply generation
980
- * @returns Object with stop() function and promise
981
- *
982
- * @example
983
- * ```ts
984
- * const { promise } = streamSmartReply(
985
- * [{ role: 'user', content: 'Want to grab coffee?' }],
986
- * (event) => setRawReplies(event.accumulatedText)
987
- * );
988
- * const result = await promise;
989
- * ```
990
- */
991
- export function streamSmartReply(
992
- messages: LLMMessage[],
993
- onToken: LLMStreamCallback,
994
- options?: LLMSmartReplyOptions
995
- ): { promise: Promise<LLMResponse>; stop: () => void } {
996
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
997
- return { promise: Promise.resolve({ text: '' }), stop: () => {} };
998
- }
999
-
1000
- if (!messages || messages.length === 0) {
1001
- return {
1002
- promise: Promise.reject(new Error('messages array cannot be empty')),
1003
- stop: () => {},
1004
- };
1005
- }
1006
-
1007
- const count = options?.count ?? 3;
1008
- const tone = options?.tone ?? 'neutral';
1009
- const systemPrompt = buildSmartReplyPrompt(count, tone, options?.persona);
1010
-
1011
- const conversation = messages
1012
- .map((m) => `${m.role === 'user' ? 'Them' : 'Me'}: ${m.content}`)
1013
- .join('\n');
1014
-
1015
- return streamMessage(
1016
- [{ role: 'user', content: `Conversation:\n${conversation}\n\nGenerate ${count} reply suggestions for my next message.` }],
1017
- onToken,
1018
- { systemPrompt }
1019
- );
1020
- }
1021
-
1022
- /**
1023
- * Autocomplete the user's current text using on-device AI.
1024
- *
1025
- * Generates short, natural completions that seamlessly continue from
1026
- * the user's partial input. Ideal for real-time typing suggestions.
1027
- *
1028
- * @param partialText - The text the user has typed so far
1029
- * @param options - Optional settings for autocompletion
1030
- * @returns Promise with an array of completion suggestions
1031
- *
1032
- * @example
1033
- * ```ts
1034
- * // Basic autocomplete
1035
- * const result = await autocomplete('How do I');
1036
- * result.suggestions.forEach(s => console.log(s.text));
1037
- * // "reset my password"
1038
- * // "contact support"
1039
- * // "cancel my subscription"
1040
- * ```
1041
- *
1042
- * @example
1043
- * ```ts
1044
- * // With context for better suggestions
1045
- * const result = await autocomplete('The patient presents with', {
1046
- * context: 'medical notes',
1047
- * maxWords: 15,
1048
- * count: 5
1049
- * });
1050
- * ```
1051
- */
1052
- export async function autocomplete(
1053
- partialText: string,
1054
- options?: LLMAutocompleteOptions
1055
- ): Promise<LLMSuggestResponse> {
1056
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
1057
- return { suggestions: [], raw: '' };
1058
- }
1059
-
1060
- if (!partialText || partialText.trim().length === 0) {
1061
- throw new Error('partialText cannot be empty');
1062
- }
1063
-
1064
- const count = options?.count ?? 3;
1065
- const maxWords = options?.maxWords ?? 10;
1066
- const systemPrompt = buildAutocompletePrompt(count, maxWords, options?.context);
1067
-
1068
- const response = await sendMessage(
1069
- [{ role: 'user', content: partialText }],
1070
- { systemPrompt }
1071
- );
1072
-
1073
- return {
1074
- suggestions: parseSuggestions(response.text),
1075
- raw: response.text,
1076
- };
1077
- }
1078
-
1079
- /**
1080
- * Autocomplete text with streaming output.
1081
- *
1082
- * @param partialText - The text the user has typed so far
1083
- * @param onToken - Callback for each token received
1084
- * @param options - Optional settings for autocompletion
1085
- * @returns Object with stop() function and promise
1086
- *
1087
- * @example
1088
- * ```ts
1089
- * const { promise } = streamAutocomplete(
1090
- * 'I would like to',
1091
- * (event) => setRawCompletions(event.accumulatedText)
1092
- * );
1093
- * const result = await promise;
1094
- * ```
1095
- */
1096
- export function streamAutocomplete(
1097
- partialText: string,
1098
- onToken: LLMStreamCallback,
1099
- options?: LLMAutocompleteOptions
1100
- ): { promise: Promise<LLMResponse>; stop: () => void } {
1101
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
1102
- return { promise: Promise.resolve({ text: '' }), stop: () => {} };
1103
- }
1104
-
1105
- if (!partialText || partialText.trim().length === 0) {
1106
- return {
1107
- promise: Promise.reject(new Error('partialText cannot be empty')),
1108
- stop: () => {},
1109
- };
1110
- }
1111
-
1112
- const count = options?.count ?? 3;
1113
- const maxWords = options?.maxWords ?? 10;
1114
- const systemPrompt = buildAutocompletePrompt(count, maxWords, options?.context);
1115
-
1116
- return streamMessage([{ role: 'user', content: partialText }], onToken, {
1117
- systemPrompt,
1118
- });
1119
- }
1120
-
1121
- /**
1122
- * Parse suggestion text from a suggest/smartReply/autocomplete response.
1123
- *
1124
- * Use this to parse the raw text from streaming responses into structured suggestions.
1125
- *
1126
- * @param raw - Raw text response from the model
1127
- * @returns Array of parsed suggestions
1128
- *
1129
- * @example
1130
- * ```ts
1131
- * const { promise } = streamSuggest('Hello', (event) => {
1132
- * setText(event.accumulatedText);
1133
- * });
1134
- * const result = await promise;
1135
- * const suggestions = parseSuggestResponse(result.text);
1136
- * ```
1137
- */
1138
- export function parseSuggestResponse(raw: string): LLMSuggestResponse {
1139
- return {
1140
- suggestions: parseSuggestions(raw),
1141
- raw,
1142
- };
1143
- }
1144
-
1145
203
  // ============================================================================
1146
204
  // Model Management API
1147
205
  // ============================================================================