@dr33m/react-native-litert-lm 0.6.2 → 0.6.4
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.
|
@@ -749,14 +749,36 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
749
749
|
// for it while holding the lock would block loadModel behind it.
|
|
750
750
|
cancelInFlightGeneration()
|
|
751
751
|
synchronized(initLock) {
|
|
752
|
+
/*
|
|
753
|
+
* Dropped first, closed second.
|
|
754
|
+
*
|
|
755
|
+
* These used to be nulled after their `close()` calls, inside one
|
|
756
|
+
* try. A close that threw — and closing a conversation that has
|
|
757
|
+
* just been cancelled is exactly when one does — skipped the rest
|
|
758
|
+
* of the block and left a retired engine still referenced. Nothing
|
|
759
|
+
* downstream could tell: `isReady()` reads `engine != null`, so the
|
|
760
|
+
* instance went on reporting itself loaded, and the next
|
|
761
|
+
* `resetConversation()` called `createConversation` on a closed
|
|
762
|
+
* engine and threw "Engine is not initialized" from deep in the SDK.
|
|
763
|
+
*
|
|
764
|
+
* Letting go of the references first makes that impossible: after
|
|
765
|
+
* this block the instance is unloaded whatever the native side did.
|
|
766
|
+
*/
|
|
767
|
+
val retiredConversation = conversation
|
|
768
|
+
val retiredEngine = engine
|
|
769
|
+
conversation = null
|
|
770
|
+
engine = null
|
|
771
|
+
loadedModelPath = null
|
|
772
|
+
|
|
773
|
+
try {
|
|
774
|
+
retiredConversation?.close()
|
|
775
|
+
} catch (e: Exception) {
|
|
776
|
+
Log.w(TAG, "Error closing conversation: ${e.message}")
|
|
777
|
+
}
|
|
752
778
|
try {
|
|
753
|
-
|
|
754
|
-
conversation = null
|
|
755
|
-
engine?.close() // Direct call
|
|
756
|
-
engine = null
|
|
757
|
-
loadedModelPath = null
|
|
779
|
+
retiredEngine?.close()
|
|
758
780
|
} catch (e: Exception) {
|
|
759
|
-
Log.
|
|
781
|
+
Log.w(TAG, "Error closing engine: ${e.message}")
|
|
760
782
|
}
|
|
761
783
|
}
|
|
762
784
|
}
|
|
@@ -778,7 +800,7 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
778
800
|
}
|
|
779
801
|
conversation = null
|
|
780
802
|
}
|
|
781
|
-
// Map tools
|
|
803
|
+
// Map tools. The engine only parses calls; JS runs them (see `automaticToolCalling`).
|
|
782
804
|
val lmTools: List<ToolProvider>? = tools?.map { toolDef ->
|
|
783
805
|
val apiTool = object : OpenApiTool {
|
|
784
806
|
override fun getToolDescriptionJsonString(): String {
|
|
@@ -790,12 +812,9 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
790
812
|
return fullDesc.toString()
|
|
791
813
|
}
|
|
792
814
|
override fun execute(paramsJsonString: String): String {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
argumentsJson = paramsJsonString
|
|
797
|
-
))
|
|
798
|
-
return "{\"status\": \"pending\", \"message\": \"Tool execution delegated to application\"}"
|
|
815
|
+
// Never invoked: automatic tool calling is off, so tool
|
|
816
|
+
// execution happens on the JS side.
|
|
817
|
+
return "{}"
|
|
799
818
|
}
|
|
800
819
|
}
|
|
801
820
|
tool(apiTool)
|
|
@@ -812,6 +831,17 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
812
831
|
systemInstruction = systemPrompt?.let { Contents.of(Content.Text(it)) },
|
|
813
832
|
initialMessages = initialMessages,
|
|
814
833
|
tools = lmTools ?: emptyList(),
|
|
834
|
+
/*
|
|
835
|
+
* Off, ported from upstream (hung-yueh 2cc938a). The SDK default runs
|
|
836
|
+
* each parsed call against `execute()` above, which was a stub, and
|
|
837
|
+
* feeds that stub's reply straight back to the model in the same
|
|
838
|
+
* turn. The model then answered the stub: prose claiming a result it
|
|
839
|
+
* never had ("I have removed it…"), streamed to the reader, then
|
|
840
|
+
* discarded once JS ran the real tool and generated again. A wasted
|
|
841
|
+
* generation per call, and its tokens stayed in the KV cache.
|
|
842
|
+
* With it off, calls arrive on `Message.toolCalls` instead.
|
|
843
|
+
*/
|
|
844
|
+
automaticToolCalling = false,
|
|
815
845
|
// Available since LiteRT-LM 0.15.0; before that the Kotlin SDK had no
|
|
816
846
|
// way to express it and every Android reply ran to the engine default.
|
|
817
847
|
maxOutputToken = maxOutputTokens,
|
|
@@ -825,7 +855,12 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
825
855
|
// - Kotlin: Not yet available — track at https://github.com/google-ai-edge/LiteRT-LM
|
|
826
856
|
//
|
|
827
857
|
// Once the Kotlin SDK exposes this, wire it via ConversationConfig here.
|
|
828
|
-
|
|
858
|
+
// Checked rather than forced: this runs from `resetConversation`, which
|
|
859
|
+
// JS calls on paths that do not first ask whether anything is loaded.
|
|
860
|
+
// A named error beats a null-pointer dereference from inside the SDK.
|
|
861
|
+
val activeEngine = engine
|
|
862
|
+
?: throw RuntimeException("Cannot create a conversation: no model is loaded.")
|
|
863
|
+
conversation = activeEngine.createConversation(convConfig)
|
|
829
864
|
}
|
|
830
865
|
|
|
831
866
|
|
|
@@ -996,7 +1031,8 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
996
1031
|
history = history,
|
|
997
1032
|
userMessage = userTextRepresentation,
|
|
998
1033
|
onStatsReady = { stats -> lastStats = stats },
|
|
999
|
-
onFailure = { e -> errorRef.set(e) }
|
|
1034
|
+
onFailure = { e -> errorRef.set(e) },
|
|
1035
|
+
onToolCalls = { calls -> captureToolCalls(calls) }
|
|
1000
1036
|
)
|
|
1001
1037
|
|
|
1002
1038
|
try {
|
|
@@ -1034,6 +1070,7 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
1034
1070
|
.joinToString("") { it.text }
|
|
1035
1071
|
|
|
1036
1072
|
val thinkingText = responseMsg.channels["thought"] ?: ""
|
|
1073
|
+
captureToolCalls(responseMsg.toolCalls)
|
|
1037
1074
|
|
|
1038
1075
|
history.add(Message(Role.MODEL, response))
|
|
1039
1076
|
|
|
@@ -1067,6 +1104,15 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
1067
1104
|
}
|
|
1068
1105
|
}
|
|
1069
1106
|
|
|
1107
|
+
/** Records SDK-parsed calls in the shape JS receives on `ExecuteResult.toolCalls`. */
|
|
1108
|
+
private fun captureToolCalls(calls: List<com.google.ai.edge.litertlm.ToolCall>?) {
|
|
1109
|
+
for (call in calls.orEmpty()) {
|
|
1110
|
+
val argumentsJson = org.json.JSONObject(call.arguments).toString()
|
|
1111
|
+
Log.d(TAG, "Tool called: ${call.name} with args: $argumentsJson")
|
|
1112
|
+
pendingToolCalls.add(ToolCall(name = call.name, argumentsJson = argumentsJson))
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1070
1116
|
override fun sendToolResponse(
|
|
1071
1117
|
responses: Array<ToolResponse>,
|
|
1072
1118
|
onToken: ((token: String, done: Boolean) -> Unit)?
|
package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/StreamingCallbackListener.kt
CHANGED
|
@@ -18,6 +18,8 @@ internal class StreamingCallbackListener(
|
|
|
18
18
|
private val onStatsReady: (GenerationStats) -> Unit,
|
|
19
19
|
private val onFailure: ((Throwable) -> Unit)? = null,
|
|
20
20
|
private val onThinkingToken: ((String) -> Unit)? = null,
|
|
21
|
+
/** Tool calls the engine parsed out of this reply. They arrive on the final message. */
|
|
22
|
+
private val onToolCalls: ((List<com.google.ai.edge.litertlm.ToolCall>) -> Unit)? = null,
|
|
21
23
|
) : com.google.ai.edge.litertlm.MessageCallback {
|
|
22
24
|
|
|
23
25
|
private val startTime = System.nanoTime()
|
|
@@ -29,6 +31,8 @@ internal class StreamingCallbackListener(
|
|
|
29
31
|
.filterIsInstance<Content.Text>()
|
|
30
32
|
.joinToString("") { it.text }
|
|
31
33
|
|
|
34
|
+
if (message.toolCalls.isNotEmpty()) onToolCalls?.invoke(message.toolCalls)
|
|
35
|
+
|
|
32
36
|
// Capture thinking from the "thought" channel
|
|
33
37
|
val thinkingChunk = message.channels["thought"]
|
|
34
38
|
if (!thinkingChunk.isNullOrEmpty()) {
|