expo-ai-kit 0.3.5 → 0.3.6
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.
|
@@ -49,18 +49,26 @@ class ExpoAiKitModule : Module() {
|
|
|
49
49
|
|
|
50
50
|
// Build conversation history prompt from all non-system messages
|
|
51
51
|
// On-device models are stateless, so we must include full history in each request
|
|
52
|
-
val
|
|
53
|
-
.filter { it["role"] != "system" }
|
|
54
|
-
.joinToString("\n") { msg ->
|
|
55
|
-
val role = (msg["role"] as? String ?: "user").uppercase()
|
|
56
|
-
val content = msg["content"] as? String ?: ""
|
|
57
|
-
"$role: $content"
|
|
58
|
-
} + "\nASSISTANT:"
|
|
52
|
+
val nonSystemMessages = messages.filter { it["role"] != "system" }
|
|
59
53
|
|
|
60
54
|
// Route to active model
|
|
61
55
|
val text = if (activeModelId == "mlkit") {
|
|
56
|
+
// ML Kit: use role-prefixed format since it has no conversation API
|
|
57
|
+
val conversationPrompt = nonSystemMessages
|
|
58
|
+
.joinToString("\n") { msg ->
|
|
59
|
+
val role = (msg["role"] as? String ?: "user").uppercase()
|
|
60
|
+
val content = msg["content"] as? String ?: ""
|
|
61
|
+
"$role: $content"
|
|
62
|
+
} + "\nASSISTANT:"
|
|
62
63
|
promptClient.generateText(conversationPrompt, systemPrompt)
|
|
63
64
|
} else {
|
|
65
|
+
// Gemma/LiteRT-LM: pass raw content — the Conversation API handles
|
|
66
|
+
// turn formatting internally. Adding "USER:"/"ASSISTANT:" markers
|
|
67
|
+
// causes double-formatting and garbled output.
|
|
68
|
+
val conversationPrompt = nonSystemMessages
|
|
69
|
+
.joinToString("\n") { msg ->
|
|
70
|
+
msg["content"] as? String ?: ""
|
|
71
|
+
}
|
|
64
72
|
gemmaClient.generateText(conversationPrompt, systemPrompt)
|
|
65
73
|
}
|
|
66
74
|
mapOf("text" to text)
|
|
@@ -73,25 +81,15 @@ class ExpoAiKitModule : Module() {
|
|
|
73
81
|
?.get("content") as? String
|
|
74
82
|
?: fallbackSystemPrompt.ifBlank { "You are a helpful, friendly assistant." }
|
|
75
83
|
|
|
76
|
-
|
|
77
|
-
// On-device models are stateless, so we must include full history in each request
|
|
78
|
-
val conversationPrompt = messages
|
|
79
|
-
.filter { it["role"] != "system" }
|
|
80
|
-
.joinToString("\n") { msg ->
|
|
81
|
-
val role = (msg["role"] as? String ?: "user").uppercase()
|
|
82
|
-
val content = msg["content"] as? String ?: ""
|
|
83
|
-
"$role: $content"
|
|
84
|
-
} + "\nASSISTANT:"
|
|
84
|
+
val nonSystemMessages = messages.filter { it["role"] != "system" }
|
|
85
85
|
|
|
86
86
|
// Launch streaming in a coroutine that can be cancelled
|
|
87
87
|
val job = streamScope.launch {
|
|
88
|
-
val
|
|
89
|
-
val streamCallback = { token: String, _: String, isDone: Boolean ->
|
|
90
|
-
streamAccumulator.append(token)
|
|
88
|
+
val streamCallback = { token: String, accumulatedText: String, isDone: Boolean ->
|
|
91
89
|
sendEvent("onStreamToken", mapOf(
|
|
92
90
|
"sessionId" to sessionId,
|
|
93
91
|
"token" to token,
|
|
94
|
-
"accumulatedText" to
|
|
92
|
+
"accumulatedText" to accumulatedText,
|
|
95
93
|
"isDone" to isDone
|
|
96
94
|
))
|
|
97
95
|
|
|
@@ -102,8 +100,20 @@ class ExpoAiKitModule : Module() {
|
|
|
102
100
|
|
|
103
101
|
// Route to active model
|
|
104
102
|
if (activeModelId == "mlkit") {
|
|
103
|
+
// ML Kit: use role-prefixed format since it has no conversation API
|
|
104
|
+
val conversationPrompt = nonSystemMessages
|
|
105
|
+
.joinToString("\n") { msg ->
|
|
106
|
+
val role = (msg["role"] as? String ?: "user").uppercase()
|
|
107
|
+
val content = msg["content"] as? String ?: ""
|
|
108
|
+
"$role: $content"
|
|
109
|
+
} + "\nASSISTANT:"
|
|
105
110
|
promptClient.generateTextStream(conversationPrompt, systemPrompt, streamCallback)
|
|
106
111
|
} else {
|
|
112
|
+
// Gemma/LiteRT-LM: pass raw content — Conversation API handles turn formatting
|
|
113
|
+
val conversationPrompt = nonSystemMessages
|
|
114
|
+
.joinToString("\n") { msg ->
|
|
115
|
+
msg["content"] as? String ?: ""
|
|
116
|
+
}
|
|
107
117
|
gemmaClient.generateTextStream(conversationPrompt, systemPrompt, streamCallback)
|
|
108
118
|
}
|
|
109
119
|
}
|
|
@@ -219,22 +219,37 @@ class GemmaInferenceClient(private val context: Context) {
|
|
|
219
219
|
try {
|
|
220
220
|
withContext(Dispatchers.IO) {
|
|
221
221
|
suspendCancellableCoroutine<Unit> { continuation ->
|
|
222
|
+
val accumulatedBuilder = StringBuilder()
|
|
222
223
|
var previousText = ""
|
|
223
224
|
conv.sendMessageAsync(
|
|
224
225
|
Contents.of(fullPrompt),
|
|
225
226
|
object : MessageCallback {
|
|
226
227
|
override fun onMessage(message: Message) {
|
|
227
|
-
val
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
val messageText = message.toString()
|
|
229
|
+
|
|
230
|
+
// LiteRT-LM may deliver accumulated text or delta tokens depending
|
|
231
|
+
// on the version. Detect which by checking if messageText extends
|
|
232
|
+
// what we've seen before.
|
|
233
|
+
val token: String
|
|
234
|
+
if (messageText.startsWith(previousText) && messageText.length >= previousText.length) {
|
|
235
|
+
// Accumulated text — extract delta
|
|
236
|
+
token = messageText.substring(previousText.length)
|
|
237
|
+
previousText = messageText
|
|
238
|
+
accumulatedBuilder.clear()
|
|
239
|
+
accumulatedBuilder.append(messageText)
|
|
230
240
|
} else {
|
|
231
|
-
|
|
241
|
+
// Delta token — accumulate ourselves
|
|
242
|
+
token = messageText
|
|
243
|
+
accumulatedBuilder.append(messageText)
|
|
244
|
+
previousText = accumulatedBuilder.toString()
|
|
232
245
|
}
|
|
233
|
-
|
|
246
|
+
|
|
247
|
+
val accumulated = accumulatedBuilder.toString()
|
|
234
248
|
onChunk(token, accumulated, false)
|
|
235
249
|
}
|
|
236
250
|
override fun onDone() {
|
|
237
|
-
|
|
251
|
+
val finalText = accumulatedBuilder.toString()
|
|
252
|
+
onChunk("", finalText, true)
|
|
238
253
|
continuation.resume(Unit)
|
|
239
254
|
}
|
|
240
255
|
override fun onError(throwable: Throwable) {
|
package/package.json
CHANGED