@capgo/capacitor-llm 8.0.19 → 8.1.1
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/CapgoCapacitorLlm.podspec +1 -9
- package/Package.swift +21 -3
- package/README.md +138 -163
- package/android/build.gradle +3 -2
- package/android/src/main/java/ee/forgr/capgo_llm/LLM.java +618 -282
- package/android/src/main/java/ee/forgr/capgo_llm/LLMPlugin.java +38 -63
- package/dist/docs.json +78 -35
- package/dist/esm/definitions.d.ts +34 -20
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +6 -0
- package/dist/esm/web.js +68 -39
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +68 -39
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +68 -39
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/LLMPlugin/LLMPlugin.swift +398 -613
- package/ios/Sources/LiteRTLM/Benchmark.swift +83 -0
- package/ios/Sources/LiteRTLM/Capabilities.swift +41 -0
- package/ios/Sources/LiteRTLM/Config.swift +172 -0
- package/ios/Sources/LiteRTLM/Conversation.swift +472 -0
- package/ios/Sources/LiteRTLM/Engine.swift +208 -0
- package/ios/Sources/LiteRTLM/ExperimentalFlags.swift +142 -0
- package/ios/Sources/LiteRTLM/LiteRTLMError.swift +156 -0
- package/ios/Sources/LiteRTLM/Message.swift +225 -0
- package/ios/Sources/LiteRTLM/Tool.swift +291 -0
- package/ios/Sources/LiteRTLM/ToolManager.swift +152 -0
- package/package.json +6 -5
|
@@ -1,40 +1,72 @@
|
|
|
1
1
|
package ee.forgr.capgo_llm;
|
|
2
2
|
|
|
3
3
|
import android.content.Context;
|
|
4
|
+
import android.os.Build;
|
|
5
|
+
import com.google.ai.edge.litertlm.Backend;
|
|
6
|
+
import com.google.ai.edge.litertlm.Content;
|
|
7
|
+
import com.google.ai.edge.litertlm.Conversation;
|
|
8
|
+
import com.google.ai.edge.litertlm.ConversationConfig;
|
|
9
|
+
import com.google.ai.edge.litertlm.Engine;
|
|
10
|
+
import com.google.ai.edge.litertlm.EngineConfig;
|
|
11
|
+
import com.google.ai.edge.litertlm.Message;
|
|
12
|
+
import com.google.ai.edge.litertlm.SamplerConfig;
|
|
13
|
+
import com.google.common.util.concurrent.ListenableFuture;
|
|
4
14
|
import com.google.mediapipe.tasks.genai.llminference.LlmInference;
|
|
5
15
|
import com.google.mediapipe.tasks.genai.llminference.LlmInference.LlmInferenceOptions;
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
16
|
+
import com.google.mediapipe.tasks.genai.llminference.LlmInferenceSession;
|
|
17
|
+
import com.google.mlkit.genai.common.FeatureStatus;
|
|
18
|
+
import com.google.mlkit.genai.common.StreamingCallback;
|
|
19
|
+
import com.google.mlkit.genai.prompt.Candidate;
|
|
20
|
+
import com.google.mlkit.genai.prompt.GenerateContentRequest;
|
|
21
|
+
import com.google.mlkit.genai.prompt.GenerateContentResponse;
|
|
22
|
+
import com.google.mlkit.genai.prompt.Generation;
|
|
23
|
+
import com.google.mlkit.genai.prompt.TextPart;
|
|
24
|
+
import com.google.mlkit.genai.prompt.java.GenerativeModelFutures;
|
|
25
|
+
import java.io.File;
|
|
26
|
+
import java.io.FileOutputStream;
|
|
27
|
+
import java.io.InputStream;
|
|
28
|
+
import java.util.Collections;
|
|
9
29
|
import java.util.Map;
|
|
10
30
|
import java.util.UUID;
|
|
31
|
+
import java.util.concurrent.CancellationException;
|
|
32
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
33
|
+
import java.util.concurrent.ExecutionException;
|
|
11
34
|
import java.util.concurrent.Executor;
|
|
12
35
|
import java.util.concurrent.Executors;
|
|
13
36
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
14
|
-
import org.pytorch.executorch.extension.llm.LlmCallback;
|
|
15
|
-
import org.pytorch.executorch.extension.llm.LlmModule;
|
|
16
37
|
|
|
17
38
|
public class LLM {
|
|
18
39
|
|
|
19
|
-
private enum
|
|
40
|
+
private enum BackendType {
|
|
41
|
+
LITERT,
|
|
20
42
|
MEDIAPIPE,
|
|
21
|
-
|
|
43
|
+
GEMINI_NANO
|
|
22
44
|
}
|
|
23
45
|
|
|
46
|
+
private static final double DEFAULT_TOP_P = 0.95d;
|
|
24
47
|
private static LLM instance;
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
private
|
|
48
|
+
|
|
49
|
+
private final Context context;
|
|
50
|
+
private final Executor executor;
|
|
51
|
+
private final Map<String, ChatSession> chatSessions;
|
|
52
|
+
|
|
53
|
+
private Engine engine;
|
|
54
|
+
private GenerativeModelFutures geminiNanoModel;
|
|
55
|
+
private LlmInference llmInference;
|
|
28
56
|
private boolean isReady = false;
|
|
29
|
-
private Context context;
|
|
30
|
-
private Executor executor;
|
|
31
57
|
private String modelPath = null;
|
|
32
|
-
private String
|
|
33
|
-
private
|
|
58
|
+
private String modelType = null;
|
|
59
|
+
private BackendType activeBackend = BackendType.LITERT;
|
|
60
|
+
private long currentLoadId = 0L;
|
|
61
|
+
|
|
62
|
+
private Integer maxTokens = 2048;
|
|
63
|
+
private Integer topk = 40;
|
|
64
|
+
private Float temperature = 0.1f;
|
|
65
|
+
private Integer randomSeed = 101;
|
|
34
66
|
|
|
35
67
|
private LLM(Context context) {
|
|
36
68
|
this.context = context;
|
|
37
|
-
this.chatSessions = new
|
|
69
|
+
this.chatSessions = new ConcurrentHashMap<>();
|
|
38
70
|
this.executor = Executors.newSingleThreadExecutor();
|
|
39
71
|
}
|
|
40
72
|
|
|
@@ -45,109 +77,54 @@ public class LLM {
|
|
|
45
77
|
return instance;
|
|
46
78
|
}
|
|
47
79
|
|
|
48
|
-
// Model parameters
|
|
49
|
-
private Integer maxTokens = 2048;
|
|
50
|
-
private Integer sequenceLength = 2048;
|
|
51
|
-
private Integer topk = 40;
|
|
52
|
-
private Float temperature = 0.1f;
|
|
53
|
-
private String modelType = null;
|
|
54
|
-
|
|
55
80
|
public void setModel(
|
|
56
81
|
String path,
|
|
57
|
-
String engine,
|
|
58
82
|
String modelType,
|
|
59
|
-
String tokenizerPath,
|
|
60
|
-
List<String> specialTokens,
|
|
61
83
|
Integer maxTokens,
|
|
62
|
-
Integer sequenceLength,
|
|
63
84
|
Integer topk,
|
|
64
85
|
Float temperature,
|
|
86
|
+
Integer randomSeed,
|
|
87
|
+
String backend,
|
|
65
88
|
ModelLoadCallback callback
|
|
66
89
|
) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.modelType = modelType;
|
|
70
|
-
this.maxTokens = maxTokens;
|
|
71
|
-
this.sequenceLength = sequenceLength != null ? sequenceLength : maxTokens;
|
|
72
|
-
this.topk = topk;
|
|
73
|
-
this.temperature = temperature;
|
|
74
|
-
try {
|
|
75
|
-
this.currentEngine = resolveEngine(engine, path, tokenizerPath);
|
|
76
|
-
} catch (IllegalArgumentException e) {
|
|
77
|
-
isReady = false;
|
|
78
|
-
if (callback != null) {
|
|
79
|
-
callback.onError(e.getMessage());
|
|
80
|
-
}
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
isReady = false;
|
|
90
|
+
ModelLoadRequest request = nextLoadRequest(path, modelType, maxTokens, topk, temperature, randomSeed, backend);
|
|
91
|
+
resetModelState();
|
|
84
92
|
|
|
85
93
|
android.util.Log.d(
|
|
86
94
|
"LLM",
|
|
87
|
-
"setModel called with
|
|
88
|
-
|
|
89
|
-
",
|
|
90
|
-
|
|
91
|
-
", tokenizerPath: " +
|
|
92
|
-
tokenizerPath +
|
|
95
|
+
"setModel called with path: " +
|
|
96
|
+
request.modelPath +
|
|
97
|
+
", modelType: " +
|
|
98
|
+
request.modelType +
|
|
93
99
|
", maxTokens: " +
|
|
94
|
-
maxTokens +
|
|
95
|
-
", sequenceLength: " +
|
|
96
|
-
this.sequenceLength +
|
|
100
|
+
request.maxTokens +
|
|
97
101
|
", topk: " +
|
|
98
|
-
topk +
|
|
102
|
+
request.topk +
|
|
99
103
|
", temperature: " +
|
|
100
|
-
temperature
|
|
101
|
-
", specialTokens: " +
|
|
102
|
-
(specialTokens == null ? 0 : specialTokens.size())
|
|
104
|
+
request.temperature
|
|
103
105
|
);
|
|
104
|
-
initializeModel(callback);
|
|
106
|
+
initializeModel(request, callback);
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
private
|
|
108
|
-
String
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (tokenizerPath != null && !tokenizerPath.isEmpty()) {
|
|
119
|
-
return Engine.EXECUTORCH;
|
|
120
|
-
}
|
|
121
|
-
if (path != null && path.toLowerCase(Locale.US).endsWith(".pte")) {
|
|
122
|
-
return Engine.EXECUTORCH;
|
|
123
|
-
}
|
|
124
|
-
return Engine.MEDIAPIPE;
|
|
109
|
+
private synchronized ModelLoadRequest nextLoadRequest(
|
|
110
|
+
String path,
|
|
111
|
+
String modelType,
|
|
112
|
+
Integer maxTokens,
|
|
113
|
+
Integer topk,
|
|
114
|
+
Float temperature,
|
|
115
|
+
Integer randomSeed,
|
|
116
|
+
String backend
|
|
117
|
+
) {
|
|
118
|
+
currentLoadId += 1;
|
|
119
|
+
return new ModelLoadRequest(path, modelType, maxTokens, topk, temperature, randomSeed, backend, currentLoadId);
|
|
125
120
|
}
|
|
126
121
|
|
|
127
|
-
private
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
mediaPipeInference.close();
|
|
131
|
-
} catch (Exception e) {
|
|
132
|
-
android.util.Log.w("LLM", "Failed to close MediaPipe inference: " + e.getMessage());
|
|
133
|
-
} finally {
|
|
134
|
-
mediaPipeInference = null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (executorchModule != null) {
|
|
139
|
-
try {
|
|
140
|
-
executorchModule.stop();
|
|
141
|
-
} catch (Exception e) {
|
|
142
|
-
android.util.Log.w("LLM", "Failed to stop ExecuTorch module: " + e.getMessage());
|
|
143
|
-
} finally {
|
|
144
|
-
executorchModule = null;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
122
|
+
private synchronized boolean isCurrentLoad(long loadId) {
|
|
123
|
+
return loadId == currentLoadId;
|
|
147
124
|
}
|
|
148
125
|
|
|
149
|
-
private void initializeModel(ModelLoadCallback callback) {
|
|
150
|
-
if (modelPath == null) {
|
|
126
|
+
private void initializeModel(ModelLoadRequest request, ModelLoadCallback callback) {
|
|
127
|
+
if (request.modelPath == null) {
|
|
151
128
|
if (callback != null) {
|
|
152
129
|
callback.onError("Model path not set");
|
|
153
130
|
}
|
|
@@ -156,277 +133,539 @@ public class LLM {
|
|
|
156
133
|
|
|
157
134
|
executor.execute(() -> {
|
|
158
135
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
136
|
+
if (!isCurrentLoad(request.loadId)) {
|
|
137
|
+
notifySupersededLoad(callback);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
BackendType backend = resolveBackend(request.modelPath, request.modelType);
|
|
142
|
+
String actualPath = backend == BackendType.GEMINI_NANO ? request.modelPath : resolveModelPath(request.modelPath, backend);
|
|
143
|
+
|
|
144
|
+
android.util.Log.d("LLM", "Resolved model path: " + actualPath + " using backend: " + backend);
|
|
145
|
+
|
|
146
|
+
if (backend == BackendType.LITERT) {
|
|
147
|
+
Engine loadedEngine = initializeLiteRtModel(actualPath, request);
|
|
148
|
+
if (!isCurrentLoad(request.loadId)) {
|
|
149
|
+
closeEngine(loadedEngine);
|
|
150
|
+
notifySupersededLoad(callback);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
applyLoadedModel(request, backend, loadedEngine, null, null);
|
|
162
154
|
} else {
|
|
163
|
-
|
|
155
|
+
if (backend == BackendType.GEMINI_NANO) {
|
|
156
|
+
initializeGeminiNanoModel(request, callback);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
LlmInference loadedInference = initializeMediaPipeModel(actualPath, request);
|
|
161
|
+
if (!isCurrentLoad(request.loadId)) {
|
|
162
|
+
closeLlmInference(loadedInference);
|
|
163
|
+
notifySupersededLoad(callback);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
applyLoadedModel(request, backend, null, loadedInference, null);
|
|
164
167
|
}
|
|
165
|
-
isReady = true;
|
|
166
168
|
|
|
167
169
|
if (callback != null) {
|
|
168
170
|
callback.onSuccess();
|
|
169
171
|
}
|
|
170
|
-
} catch (Exception
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
} catch (Exception exception) {
|
|
173
|
+
if (!isCurrentLoad(request.loadId)) {
|
|
174
|
+
notifySupersededLoad(callback);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
android.util.Log.e("LLM", "Failed to initialize model", exception);
|
|
179
|
+
resetModelState();
|
|
180
|
+
|
|
173
181
|
if (callback != null) {
|
|
174
|
-
callback.onError(
|
|
182
|
+
callback.onError(exception.getMessage());
|
|
175
183
|
}
|
|
176
184
|
}
|
|
177
185
|
});
|
|
178
186
|
}
|
|
179
187
|
|
|
180
|
-
private void
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
private void notifySupersededLoad(ModelLoadCallback callback) {
|
|
189
|
+
if (callback != null) {
|
|
190
|
+
callback.onError("Model load was superseded by a newer request");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
183
193
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
194
|
+
private synchronized void applyLoadedModel(
|
|
195
|
+
ModelLoadRequest request,
|
|
196
|
+
BackendType backend,
|
|
197
|
+
Engine loadedEngine,
|
|
198
|
+
LlmInference loadedInference,
|
|
199
|
+
GenerativeModelFutures loadedGeminiNanoModel
|
|
200
|
+
) {
|
|
201
|
+
modelPath = request.modelPath;
|
|
202
|
+
modelType = request.modelType;
|
|
203
|
+
maxTokens = request.maxTokens;
|
|
204
|
+
topk = request.topk;
|
|
205
|
+
temperature = request.temperature;
|
|
206
|
+
randomSeed = request.randomSeed;
|
|
207
|
+
activeBackend = backend;
|
|
208
|
+
engine = loadedEngine;
|
|
209
|
+
llmInference = loadedInference;
|
|
210
|
+
geminiNanoModel = loadedGeminiNanoModel;
|
|
211
|
+
isReady = true;
|
|
212
|
+
}
|
|
188
213
|
|
|
189
|
-
|
|
190
|
-
|
|
214
|
+
private Engine initializeLiteRtModel(String actualPath, ModelLoadRequest request) {
|
|
215
|
+
Backend primaryBackend = resolveLiteRtBackend(request.backend);
|
|
216
|
+
EngineConfig config = new EngineConfig(
|
|
217
|
+
actualPath,
|
|
218
|
+
primaryBackend,
|
|
219
|
+
primaryBackend,
|
|
220
|
+
primaryBackend,
|
|
221
|
+
request.maxTokens,
|
|
222
|
+
context.getCacheDir().getAbsolutePath()
|
|
223
|
+
);
|
|
224
|
+
Engine loadedEngine = new Engine(config);
|
|
225
|
+
loadedEngine.initialize();
|
|
226
|
+
return loadedEngine;
|
|
191
227
|
}
|
|
192
228
|
|
|
193
|
-
private
|
|
194
|
-
if (
|
|
195
|
-
|
|
229
|
+
private Backend resolveLiteRtBackend(String backend) {
|
|
230
|
+
if (backend != null && "gpu".equalsIgnoreCase(backend.trim())) {
|
|
231
|
+
return new Backend.GPU();
|
|
196
232
|
}
|
|
197
233
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
int executorModelType = resolveExecutorchModelType(modelType);
|
|
234
|
+
return new Backend.CPU();
|
|
235
|
+
}
|
|
201
236
|
|
|
202
|
-
|
|
203
|
-
|
|
237
|
+
private LlmInference initializeMediaPipeModel(String actualPath, ModelLoadRequest request) {
|
|
238
|
+
LlmInferenceOptions options = LlmInferenceOptions.builder()
|
|
239
|
+
.setModelPath(actualPath)
|
|
240
|
+
.setMaxTokens(request.maxTokens)
|
|
241
|
+
.setMaxTopK(request.topk)
|
|
242
|
+
.build();
|
|
243
|
+
return LlmInference.createFromOptions(context, options);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private void initializeGeminiNanoModel(ModelLoadRequest request, ModelLoadCallback callback) {
|
|
247
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
248
|
+
if (callback != null) {
|
|
249
|
+
callback.onError("Gemini Nano requires Android API 26 or higher");
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
204
253
|
|
|
205
|
-
|
|
206
|
-
|
|
254
|
+
GenerativeModelFutures loadedModel = GenerativeModelFutures.from(Generation.INSTANCE.getClient());
|
|
255
|
+
ListenableFuture<Integer> statusFuture = loadedModel.checkStatus();
|
|
256
|
+
statusFuture.addListener(
|
|
257
|
+
() -> {
|
|
258
|
+
try {
|
|
259
|
+
if (!isCurrentLoad(request.loadId)) {
|
|
260
|
+
notifySupersededLoad(callback);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
Integer status = statusFuture.get();
|
|
265
|
+
if (status == FeatureStatus.AVAILABLE) {
|
|
266
|
+
applyLoadedModel(request, BackendType.GEMINI_NANO, null, null, loadedModel);
|
|
267
|
+
if (callback != null) {
|
|
268
|
+
callback.onSuccess();
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
resetModelState();
|
|
274
|
+
if (callback != null) {
|
|
275
|
+
callback.onError("Gemini Nano is " + geminiNanoStatusName(status) + " on this device");
|
|
276
|
+
}
|
|
277
|
+
} catch (CancellationException | ExecutionException exception) {
|
|
278
|
+
resetModelState();
|
|
279
|
+
if (callback != null) {
|
|
280
|
+
callback.onError(exception.getMessage());
|
|
281
|
+
}
|
|
282
|
+
} catch (InterruptedException exception) {
|
|
283
|
+
Thread.currentThread().interrupt();
|
|
284
|
+
resetModelState();
|
|
285
|
+
if (callback != null) {
|
|
286
|
+
callback.onError(exception.getMessage());
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
executor
|
|
291
|
+
);
|
|
207
292
|
}
|
|
208
293
|
|
|
209
|
-
private
|
|
210
|
-
if (
|
|
211
|
-
return
|
|
294
|
+
private String geminiNanoStatusName(Integer status) {
|
|
295
|
+
if (status == null) {
|
|
296
|
+
return "unknown";
|
|
212
297
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
298
|
+
|
|
299
|
+
switch (status) {
|
|
300
|
+
case FeatureStatus.UNAVAILABLE:
|
|
301
|
+
return "unavailable";
|
|
302
|
+
case FeatureStatus.DOWNLOADABLE:
|
|
303
|
+
return "downloadable but not downloaded";
|
|
304
|
+
case FeatureStatus.DOWNLOADING:
|
|
305
|
+
return "downloading";
|
|
306
|
+
case FeatureStatus.AVAILABLE:
|
|
307
|
+
return "available";
|
|
308
|
+
default:
|
|
309
|
+
return "unknown";
|
|
216
310
|
}
|
|
217
|
-
|
|
218
|
-
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private BackendType resolveBackend(String path, String modelType) {
|
|
314
|
+
if (isGeminiNanoModel(path, modelType)) {
|
|
315
|
+
return BackendType.GEMINI_NANO;
|
|
219
316
|
}
|
|
220
|
-
|
|
317
|
+
|
|
318
|
+
return "litertlm".equals(normalizeModelType(path, modelType)) ? BackendType.LITERT : BackendType.MEDIAPIPE;
|
|
221
319
|
}
|
|
222
320
|
|
|
223
|
-
private
|
|
224
|
-
|
|
321
|
+
private boolean isGeminiNanoModel(String path, String modelType) {
|
|
322
|
+
String requestedModel = modelType != null && !modelType.isBlank() ? modelType : path;
|
|
323
|
+
return "gemininano".equals(normalizeSystemModelName(requestedModel));
|
|
324
|
+
}
|
|
225
325
|
|
|
226
|
-
|
|
227
|
-
|
|
326
|
+
private String normalizeSystemModelName(String value) {
|
|
327
|
+
return stripUrlSuffix(value).toLowerCase().replace("-", "").replace("_", "").replace(" ", "");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private String normalizeModelType(String path, String modelType) {
|
|
331
|
+
if (modelType != null && !modelType.isBlank()) {
|
|
332
|
+
return stripUrlSuffix(modelType).toLowerCase();
|
|
228
333
|
}
|
|
229
334
|
|
|
230
|
-
String
|
|
231
|
-
|
|
335
|
+
String normalizedPath = stripUrlSuffix(path);
|
|
336
|
+
int extensionIndex = normalizedPath.lastIndexOf('.');
|
|
337
|
+
if (extensionIndex == -1 || extensionIndex == normalizedPath.length() - 1) {
|
|
338
|
+
return "";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return normalizedPath.substring(extensionIndex + 1).toLowerCase();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private String stripUrlSuffix(String value) {
|
|
345
|
+
int cutoff = value.length();
|
|
346
|
+
int queryIndex = value.indexOf('?');
|
|
347
|
+
int fragmentIndex = value.indexOf('#');
|
|
232
348
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
349
|
+
if (queryIndex >= 0 && queryIndex < cutoff) {
|
|
350
|
+
cutoff = queryIndex;
|
|
351
|
+
}
|
|
352
|
+
if (fragmentIndex >= 0 && fragmentIndex < cutoff) {
|
|
353
|
+
cutoff = fragmentIndex;
|
|
238
354
|
}
|
|
239
355
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
356
|
+
return value.substring(0, cutoff);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private String resolveModelPath(String path, BackendType backendType) throws Exception {
|
|
360
|
+
if (!path.startsWith("/android_asset/")) {
|
|
361
|
+
return path;
|
|
245
362
|
}
|
|
246
363
|
|
|
364
|
+
String assetPath = path.substring("/android_asset/".length());
|
|
365
|
+
File cacheDir = context.getCacheDir();
|
|
366
|
+
File modelFile = new File(cacheDir, assetPath);
|
|
367
|
+
|
|
247
368
|
copyAssetToFile(assetPath, modelFile);
|
|
248
|
-
android.util.Log.d("LLM", "Copied asset to: " + modelFile.getAbsolutePath());
|
|
249
369
|
|
|
250
|
-
if (
|
|
251
|
-
|
|
370
|
+
if (backendType == BackendType.MEDIAPIPE && assetPath.endsWith(".task")) {
|
|
371
|
+
String companionPath = assetPath.replace(".task", ".litertlm");
|
|
372
|
+
try {
|
|
373
|
+
copyAssetToFile(companionPath, new File(cacheDir, companionPath));
|
|
374
|
+
} catch (Exception exception) {
|
|
375
|
+
android.util.Log.d("LLM", "No MediaPipe companion file found for " + assetPath);
|
|
376
|
+
}
|
|
252
377
|
}
|
|
253
378
|
|
|
254
379
|
return modelFile.getAbsolutePath();
|
|
255
380
|
}
|
|
256
381
|
|
|
257
|
-
private void
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
java.io.File parent = litertlmFile.getParentFile();
|
|
262
|
-
if (parent != null) {
|
|
263
|
-
parent.mkdirs();
|
|
264
|
-
}
|
|
265
|
-
copyAssetToFile(litertlmPath, litertlmFile);
|
|
266
|
-
android.util.Log.d("LLM", "Also copied companion file: " + litertlmFile.getAbsolutePath());
|
|
267
|
-
} catch (Exception e) {
|
|
268
|
-
android.util.Log.d("LLM", "No companion .litertlm file found");
|
|
382
|
+
private void copyAssetToFile(String assetPath, File destination) throws Exception {
|
|
383
|
+
File parent = destination.getParentFile();
|
|
384
|
+
if (parent != null && !parent.exists()) {
|
|
385
|
+
parent.mkdirs();
|
|
269
386
|
}
|
|
270
|
-
}
|
|
271
387
|
|
|
272
|
-
private void copyAssetToFile(String assetPath, java.io.File destFile) throws Exception {
|
|
273
388
|
try (
|
|
274
|
-
|
|
275
|
-
|
|
389
|
+
InputStream inputStream = context.getAssets().open(assetPath);
|
|
390
|
+
FileOutputStream outputStream = new FileOutputStream(destination)
|
|
276
391
|
) {
|
|
277
|
-
byte[] buffer = new byte[
|
|
392
|
+
byte[] buffer = new byte[8192];
|
|
278
393
|
int length;
|
|
279
|
-
while ((length =
|
|
280
|
-
|
|
394
|
+
while ((length = inputStream.read(buffer)) > 0) {
|
|
395
|
+
outputStream.write(buffer, 0, length);
|
|
281
396
|
}
|
|
282
397
|
}
|
|
283
398
|
}
|
|
284
399
|
|
|
285
|
-
public String createChat() {
|
|
400
|
+
public synchronized String createChat() {
|
|
401
|
+
if (!isReady) {
|
|
402
|
+
throw new IllegalStateException("Model not ready");
|
|
403
|
+
}
|
|
404
|
+
|
|
286
405
|
String chatId = UUID.randomUUID().toString();
|
|
287
|
-
ChatSession session
|
|
406
|
+
ChatSession session;
|
|
407
|
+
|
|
408
|
+
if (activeBackend == BackendType.LITERT) {
|
|
409
|
+
if (engine == null) {
|
|
410
|
+
throw new IllegalStateException("LiteRT-LM engine not initialized");
|
|
411
|
+
}
|
|
412
|
+
session = ChatSession.forLiteRt(createLiteRtConversation());
|
|
413
|
+
} else if (activeBackend == BackendType.GEMINI_NANO) {
|
|
414
|
+
if (geminiNanoModel == null) {
|
|
415
|
+
throw new IllegalStateException("Gemini Nano model not initialized");
|
|
416
|
+
}
|
|
417
|
+
session = ChatSession.forGeminiNano();
|
|
418
|
+
} else {
|
|
419
|
+
if (llmInference == null) {
|
|
420
|
+
throw new IllegalStateException("MediaPipe model not initialized");
|
|
421
|
+
}
|
|
422
|
+
session = ChatSession.forMediaPipe();
|
|
423
|
+
}
|
|
424
|
+
|
|
288
425
|
chatSessions.put(chatId, session);
|
|
289
426
|
return chatId;
|
|
290
427
|
}
|
|
291
428
|
|
|
429
|
+
private Conversation createLiteRtConversation() {
|
|
430
|
+
ConversationConfig defaults = new ConversationConfig();
|
|
431
|
+
SamplerConfig samplerConfig = new SamplerConfig(topk, DEFAULT_TOP_P, temperature.doubleValue(), randomSeed);
|
|
432
|
+
ConversationConfig config = defaults.copy(
|
|
433
|
+
defaults.getSystemInstruction(),
|
|
434
|
+
defaults.getInitialMessages(),
|
|
435
|
+
defaults.getTools(),
|
|
436
|
+
samplerConfig,
|
|
437
|
+
defaults.getAutomaticToolCalling(),
|
|
438
|
+
defaults.getChannels()
|
|
439
|
+
);
|
|
440
|
+
return engine.createConversation(config);
|
|
441
|
+
}
|
|
442
|
+
|
|
292
443
|
public void sendMessage(String chatId, String message, MessageCallback callback) {
|
|
293
|
-
ChatSession session
|
|
294
|
-
|
|
295
|
-
|
|
444
|
+
ChatSession session;
|
|
445
|
+
LlmInference inference;
|
|
446
|
+
GenerativeModelFutures nanoModel;
|
|
447
|
+
Integer sessionTopk;
|
|
448
|
+
Float sessionTemperature;
|
|
449
|
+
Integer sessionMaxTokens;
|
|
450
|
+
Integer sessionRandomSeed;
|
|
451
|
+
synchronized (this) {
|
|
452
|
+
session = chatSessions.get(chatId);
|
|
453
|
+
if (session == null) {
|
|
454
|
+
throw new IllegalStateException("Chat session not found");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!isReady) {
|
|
458
|
+
throw new IllegalStateException("Model not ready");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
inference = llmInference;
|
|
462
|
+
nanoModel = geminiNanoModel;
|
|
463
|
+
sessionTopk = topk;
|
|
464
|
+
sessionTemperature = temperature;
|
|
465
|
+
sessionMaxTokens = maxTokens;
|
|
466
|
+
sessionRandomSeed = randomSeed;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (session.backendType == BackendType.LITERT) {
|
|
470
|
+
sendMessageWithLiteRt(chatId, message, session, callback);
|
|
296
471
|
return;
|
|
297
472
|
}
|
|
298
473
|
|
|
299
|
-
if (
|
|
300
|
-
|
|
474
|
+
if (session.backendType == BackendType.GEMINI_NANO) {
|
|
475
|
+
if (nanoModel == null) {
|
|
476
|
+
throw new IllegalStateException("Gemini Nano model not ready");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
sendMessageWithGeminiNano(
|
|
480
|
+
chatId,
|
|
481
|
+
message,
|
|
482
|
+
session,
|
|
483
|
+
nanoModel,
|
|
484
|
+
sessionMaxTokens,
|
|
485
|
+
sessionTopk,
|
|
486
|
+
sessionTemperature,
|
|
487
|
+
sessionRandomSeed,
|
|
488
|
+
callback
|
|
489
|
+
);
|
|
301
490
|
return;
|
|
302
491
|
}
|
|
303
492
|
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
} else {
|
|
307
|
-
sendMediaPipeMessage(chatId, message, session, callback);
|
|
493
|
+
if (inference == null) {
|
|
494
|
+
throw new IllegalStateException("MediaPipe model not ready");
|
|
308
495
|
}
|
|
309
|
-
}
|
|
310
496
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
final int generationSequenceLength = sequenceLength;
|
|
314
|
-
final float generationTemperature = temperature;
|
|
497
|
+
sendMessageWithMediaPipe(chatId, message, session, inference, sessionTopk, sessionTemperature, callback);
|
|
498
|
+
}
|
|
315
499
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
500
|
+
private void sendMessageWithLiteRt(String chatId, String message, ChatSession session, MessageCallback callback) {
|
|
501
|
+
if (session.conversation == null) {
|
|
502
|
+
throw new IllegalStateException("LiteRT-LM conversation not found");
|
|
319
503
|
}
|
|
320
504
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
generationSequenceLength,
|
|
332
|
-
new LlmCallback() {
|
|
333
|
-
@Override
|
|
334
|
-
public void onResult(String result) {
|
|
335
|
-
if (result == null || result.isEmpty()) {
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
callback.onTextReceived(chatId, result, true);
|
|
339
|
-
fullResponse.append(result);
|
|
340
|
-
}
|
|
505
|
+
session.conversation.sendMessageAsync(
|
|
506
|
+
message,
|
|
507
|
+
new com.google.ai.edge.litertlm.MessageCallback() {
|
|
508
|
+
@Override
|
|
509
|
+
public void onMessage(Message responseMessage) {
|
|
510
|
+
String chunk = extractText(responseMessage);
|
|
511
|
+
if (!chunk.isEmpty()) {
|
|
512
|
+
callback.onTextReceived(chatId, chunk, true);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
341
515
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (completed.compareAndSet(false, true)) {
|
|
345
|
-
session.addMessage("assistant", fullResponse.toString());
|
|
346
|
-
callback.onComplete(chatId);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
false,
|
|
351
|
-
generationTemperature,
|
|
352
|
-
0,
|
|
353
|
-
0
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
if (errorCode != 0) {
|
|
357
|
-
completed.set(true);
|
|
358
|
-
callback.onError("ExecuTorch error " + errorCode);
|
|
359
|
-
} else if (completed.compareAndSet(false, true)) {
|
|
360
|
-
session.addMessage("assistant", fullResponse.toString());
|
|
516
|
+
@Override
|
|
517
|
+
public void onDone() {
|
|
361
518
|
callback.onComplete(chatId);
|
|
362
519
|
}
|
|
363
|
-
|
|
364
|
-
|
|
520
|
+
|
|
521
|
+
@Override
|
|
522
|
+
public void onError(Throwable throwable) {
|
|
523
|
+
callback.onError(throwable.getMessage());
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
Collections.emptyMap()
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private String extractText(Message responseMessage) {
|
|
531
|
+
StringBuilder builder = new StringBuilder();
|
|
532
|
+
|
|
533
|
+
for (Content content : responseMessage.getContents().getContents()) {
|
|
534
|
+
if (content instanceof Content.Text) {
|
|
535
|
+
builder.append(((Content.Text) content).getText());
|
|
365
536
|
}
|
|
366
|
-
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return builder.toString();
|
|
367
540
|
}
|
|
368
541
|
|
|
369
|
-
private void
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
542
|
+
private void sendMessageWithGeminiNano(
|
|
543
|
+
String chatId,
|
|
544
|
+
String message,
|
|
545
|
+
ChatSession session,
|
|
546
|
+
GenerativeModelFutures nanoModel,
|
|
547
|
+
Integer sessionMaxTokens,
|
|
548
|
+
Integer sessionTopk,
|
|
549
|
+
Float sessionTemperature,
|
|
550
|
+
Integer sessionRandomSeed,
|
|
551
|
+
MessageCallback callback
|
|
552
|
+
) {
|
|
553
|
+
synchronized (session) {
|
|
554
|
+
session.addMessage("user", message);
|
|
555
|
+
}
|
|
373
556
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
557
|
+
GenerateContentRequest.Builder requestBuilder;
|
|
558
|
+
synchronized (session) {
|
|
559
|
+
requestBuilder = new GenerateContentRequest.Builder(new TextPart(session.buildPrompt()));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
requestBuilder.setMaxOutputTokens(Math.min(sessionMaxTokens, 256));
|
|
563
|
+
requestBuilder.setTopK(sessionTopk);
|
|
564
|
+
requestBuilder.setTemperature(sessionTemperature);
|
|
565
|
+
requestBuilder.setSeed(sessionRandomSeed);
|
|
566
|
+
|
|
567
|
+
StringBuffer fullResponse = new StringBuffer();
|
|
568
|
+
AtomicBoolean streamed = new AtomicBoolean(false);
|
|
569
|
+
ListenableFuture<GenerateContentResponse> responseFuture = nanoModel.generateContent(
|
|
570
|
+
requestBuilder.build(),
|
|
571
|
+
new StreamingCallback() {
|
|
572
|
+
@Override
|
|
573
|
+
public void onNewText(String additionalText) {
|
|
574
|
+
if (additionalText == null || additionalText.isEmpty()) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
streamed.set(true);
|
|
579
|
+
fullResponse.append(additionalText);
|
|
580
|
+
callback.onTextReceived(chatId, additionalText, true);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
responseFuture.addListener(
|
|
586
|
+
() -> {
|
|
587
|
+
try {
|
|
588
|
+
GenerateContentResponse response = responseFuture.get();
|
|
589
|
+
if (!streamed.get()) {
|
|
590
|
+
String text = extractText(response);
|
|
591
|
+
if (!text.isEmpty()) {
|
|
592
|
+
fullResponse.append(text);
|
|
593
|
+
callback.onTextReceived(chatId, text, true);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
synchronized (session) {
|
|
598
|
+
session.addMessage("assistant", fullResponse.toString());
|
|
599
|
+
}
|
|
600
|
+
callback.onComplete(chatId);
|
|
601
|
+
} catch (CancellationException | ExecutionException exception) {
|
|
602
|
+
callback.onError(exception.getMessage());
|
|
603
|
+
} catch (InterruptedException exception) {
|
|
604
|
+
Thread.currentThread().interrupt();
|
|
605
|
+
callback.onError(exception.getMessage());
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
executor
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private String extractText(GenerateContentResponse response) {
|
|
613
|
+
if (response.getCandidates().isEmpty()) {
|
|
614
|
+
return "";
|
|
377
615
|
}
|
|
378
616
|
|
|
617
|
+
Candidate candidate = response.getCandidates().get(0);
|
|
618
|
+
return candidate.getText();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private void sendMessageWithMediaPipe(
|
|
622
|
+
String chatId,
|
|
623
|
+
String message,
|
|
624
|
+
ChatSession session,
|
|
625
|
+
LlmInference inference,
|
|
626
|
+
Integer sessionTopk,
|
|
627
|
+
Float sessionTemperature,
|
|
628
|
+
MessageCallback callback
|
|
629
|
+
) {
|
|
379
630
|
executor.execute(() -> {
|
|
631
|
+
LlmInferenceSession inferenceSession = null;
|
|
380
632
|
try {
|
|
381
|
-
// Add user message to history
|
|
382
633
|
session.addMessage("user", message);
|
|
383
634
|
|
|
384
|
-
|
|
385
|
-
String fullPrompt = session.buildPrompt(message);
|
|
635
|
+
String fullPrompt = session.buildPrompt();
|
|
386
636
|
android.util.Log.d("LLM", "Full prompt: " + fullPrompt);
|
|
387
637
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
.setTemperature(sessionTemperature)
|
|
393
|
-
.build();
|
|
638
|
+
LlmInferenceSession.LlmInferenceSessionOptions sessionOptions = LlmInferenceSession.LlmInferenceSessionOptions.builder()
|
|
639
|
+
.setTopK(sessionTopk)
|
|
640
|
+
.setTemperature(sessionTemperature)
|
|
641
|
+
.build();
|
|
394
642
|
|
|
395
|
-
|
|
396
|
-
|
|
643
|
+
inferenceSession = LlmInferenceSession.createFromOptions(inference, sessionOptions);
|
|
644
|
+
LlmInferenceSession activeSession = inferenceSession;
|
|
397
645
|
|
|
398
|
-
// Add the query
|
|
399
646
|
inferenceSession.addQueryChunk(fullPrompt);
|
|
400
647
|
|
|
401
|
-
// Use streaming API
|
|
402
648
|
final StringBuilder fullResponse = new StringBuilder();
|
|
403
649
|
|
|
404
650
|
com.google.mediapipe.tasks.genai.llminference.ProgressListener<String> resultListener =
|
|
405
651
|
new com.google.mediapipe.tasks.genai.llminference.ProgressListener<String>() {
|
|
406
652
|
private StringBuilder buffer = new StringBuilder();
|
|
407
653
|
private boolean hasStarted = false;
|
|
408
|
-
private final AtomicBoolean completed = new AtomicBoolean(false);
|
|
409
654
|
|
|
410
655
|
@Override
|
|
411
656
|
public void run(String partialResult, boolean done) {
|
|
412
657
|
android.util.Log.d("LLM", "Partial result: " + partialResult + ", done: " + done);
|
|
413
658
|
|
|
414
|
-
// Accumulate in buffer
|
|
415
659
|
buffer.append(partialResult);
|
|
416
660
|
|
|
417
|
-
// Process buffer when we have enough content or when done
|
|
418
661
|
String content = buffer.toString();
|
|
419
|
-
|
|
420
|
-
// Check if we can process some content
|
|
421
662
|
StringBuilder toSend = new StringBuilder();
|
|
422
663
|
int i = 0;
|
|
423
664
|
while (i < content.length()) {
|
|
424
|
-
// Check for escape sequence
|
|
425
665
|
if (i < content.length() - 1 && content.charAt(i) == '\\' && content.charAt(i + 1) == 'n') {
|
|
426
666
|
toSend.append('\n');
|
|
427
667
|
i += 2;
|
|
428
668
|
} else if (i == content.length() - 1 && content.charAt(i) == '\\' && !done) {
|
|
429
|
-
// We have a backslash at the end and more chunks coming - wait for next chunk
|
|
430
669
|
break;
|
|
431
670
|
} else {
|
|
432
671
|
toSend.append(content.charAt(i));
|
|
@@ -434,14 +673,11 @@ public class LLM {
|
|
|
434
673
|
}
|
|
435
674
|
}
|
|
436
675
|
|
|
437
|
-
// Update buffer to contain only unprocessed content
|
|
438
676
|
buffer = new StringBuilder(content.substring(i));
|
|
439
677
|
|
|
440
|
-
// Send processed content if any
|
|
441
678
|
String chunk = toSend.toString();
|
|
442
679
|
|
|
443
|
-
|
|
444
|
-
if (!hasStarted && chunk.length() > 0) {
|
|
680
|
+
if (!hasStarted && !chunk.isEmpty()) {
|
|
445
681
|
chunk = chunk.replaceFirst("^\\n", "");
|
|
446
682
|
hasStarted = true;
|
|
447
683
|
}
|
|
@@ -451,37 +687,91 @@ public class LLM {
|
|
|
451
687
|
fullResponse.append(chunk);
|
|
452
688
|
}
|
|
453
689
|
|
|
454
|
-
if (done &&
|
|
455
|
-
|
|
456
|
-
|
|
690
|
+
if (done && buffer.length() > 0) {
|
|
691
|
+
String remaining = buffer.toString();
|
|
692
|
+
if (!remaining.isEmpty()) {
|
|
457
693
|
callback.onTextReceived(chatId, remaining, true);
|
|
458
694
|
fullResponse.append(remaining);
|
|
459
|
-
buffer = new StringBuilder();
|
|
460
695
|
}
|
|
696
|
+
}
|
|
461
697
|
|
|
698
|
+
if (done) {
|
|
462
699
|
session.addMessage("assistant", fullResponse.toString());
|
|
463
700
|
callback.onComplete(chatId);
|
|
464
|
-
|
|
465
|
-
try {
|
|
466
|
-
inferenceSession.close();
|
|
467
|
-
} catch (Exception e) {
|
|
468
|
-
android.util.Log.e("LLM", "Failed to close session: " + e.getMessage());
|
|
469
|
-
}
|
|
701
|
+
closeMediaPipeSession(activeSession);
|
|
470
702
|
}
|
|
471
703
|
}
|
|
472
704
|
};
|
|
473
705
|
|
|
474
706
|
inferenceSession.generateResponseAsync(resultListener);
|
|
475
|
-
} catch (Exception
|
|
476
|
-
|
|
707
|
+
} catch (Exception exception) {
|
|
708
|
+
closeMediaPipeSession(inferenceSession);
|
|
709
|
+
callback.onError(exception.getMessage());
|
|
477
710
|
}
|
|
478
711
|
});
|
|
479
712
|
}
|
|
480
713
|
|
|
481
|
-
public String getReadiness() {
|
|
714
|
+
public synchronized String getReadiness() {
|
|
482
715
|
return isReady ? "ready" : "not_ready";
|
|
483
716
|
}
|
|
484
717
|
|
|
718
|
+
private synchronized void resetModelState() {
|
|
719
|
+
clearChatSessions();
|
|
720
|
+
|
|
721
|
+
closeEngine(engine);
|
|
722
|
+
closeLlmInference(llmInference);
|
|
723
|
+
engine = null;
|
|
724
|
+
llmInference = null;
|
|
725
|
+
geminiNanoModel = null;
|
|
726
|
+
modelPath = null;
|
|
727
|
+
modelType = null;
|
|
728
|
+
isReady = false;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private void closeEngine(Engine loadedEngine) {
|
|
732
|
+
if (loadedEngine != null) {
|
|
733
|
+
try {
|
|
734
|
+
loadedEngine.close();
|
|
735
|
+
} catch (Exception exception) {
|
|
736
|
+
android.util.Log.w("LLM", "Failed to close LiteRT-LM engine", exception);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private void closeMediaPipeSession(LlmInferenceSession inferenceSession) {
|
|
742
|
+
if (inferenceSession != null) {
|
|
743
|
+
try {
|
|
744
|
+
inferenceSession.close();
|
|
745
|
+
} catch (Exception exception) {
|
|
746
|
+
android.util.Log.e("LLM", "Failed to close MediaPipe session", exception);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private void closeLlmInference(LlmInference inference) {
|
|
752
|
+
if (inference != null) {
|
|
753
|
+
try {
|
|
754
|
+
inference.close();
|
|
755
|
+
} catch (Exception exception) {
|
|
756
|
+
android.util.Log.w("LLM", "Failed to close MediaPipe model", exception);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private void clearChatSessions() {
|
|
762
|
+
for (ChatSession session : chatSessions.values()) {
|
|
763
|
+
if (session.conversation != null) {
|
|
764
|
+
try {
|
|
765
|
+
session.conversation.close();
|
|
766
|
+
} catch (Exception exception) {
|
|
767
|
+
android.util.Log.w("LLM", "Failed to close LiteRT-LM conversation", exception);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
chatSessions.clear();
|
|
773
|
+
}
|
|
774
|
+
|
|
485
775
|
public interface MessageCallback {
|
|
486
776
|
void onTextReceived(String chatId, String text, boolean isChunk);
|
|
487
777
|
void onComplete(String chatId);
|
|
@@ -493,17 +783,31 @@ public class LLM {
|
|
|
493
783
|
void onError(String error);
|
|
494
784
|
}
|
|
495
785
|
|
|
496
|
-
// Inner class to manage chat sessions
|
|
497
786
|
private static class ChatSession {
|
|
498
787
|
|
|
499
|
-
private
|
|
788
|
+
private final BackendType backendType;
|
|
789
|
+
private final Conversation conversation;
|
|
790
|
+
private final StringBuilder history;
|
|
500
791
|
|
|
501
|
-
ChatSession() {
|
|
792
|
+
private ChatSession(BackendType backendType, Conversation conversation) {
|
|
793
|
+
this.backendType = backendType;
|
|
794
|
+
this.conversation = conversation;
|
|
502
795
|
this.history = new StringBuilder();
|
|
503
796
|
}
|
|
504
797
|
|
|
798
|
+
static ChatSession forLiteRt(Conversation conversation) {
|
|
799
|
+
return new ChatSession(BackendType.LITERT, conversation);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
static ChatSession forMediaPipe() {
|
|
803
|
+
return new ChatSession(BackendType.MEDIAPIPE, null);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
static ChatSession forGeminiNano() {
|
|
807
|
+
return new ChatSession(BackendType.GEMINI_NANO, null);
|
|
808
|
+
}
|
|
809
|
+
|
|
505
810
|
void addMessage(String role, String content) {
|
|
506
|
-
// Store messages in a cleaner format for history
|
|
507
811
|
if (history.length() > 0) {
|
|
508
812
|
history.append("\n");
|
|
509
813
|
}
|
|
@@ -514,8 +818,40 @@ public class LLM {
|
|
|
514
818
|
}
|
|
515
819
|
}
|
|
516
820
|
|
|
517
|
-
String buildPrompt(
|
|
821
|
+
String buildPrompt() {
|
|
518
822
|
return history.toString();
|
|
519
823
|
}
|
|
520
824
|
}
|
|
825
|
+
|
|
826
|
+
private static class ModelLoadRequest {
|
|
827
|
+
|
|
828
|
+
private final String modelPath;
|
|
829
|
+
private final String modelType;
|
|
830
|
+
private final Integer maxTokens;
|
|
831
|
+
private final Integer topk;
|
|
832
|
+
private final Float temperature;
|
|
833
|
+
private final Integer randomSeed;
|
|
834
|
+
private final String backend;
|
|
835
|
+
private final long loadId;
|
|
836
|
+
|
|
837
|
+
private ModelLoadRequest(
|
|
838
|
+
String modelPath,
|
|
839
|
+
String modelType,
|
|
840
|
+
Integer maxTokens,
|
|
841
|
+
Integer topk,
|
|
842
|
+
Float temperature,
|
|
843
|
+
Integer randomSeed,
|
|
844
|
+
String backend,
|
|
845
|
+
long loadId
|
|
846
|
+
) {
|
|
847
|
+
this.modelPath = modelPath;
|
|
848
|
+
this.modelType = modelType;
|
|
849
|
+
this.maxTokens = maxTokens;
|
|
850
|
+
this.topk = topk;
|
|
851
|
+
this.temperature = temperature;
|
|
852
|
+
this.randomSeed = randomSeed;
|
|
853
|
+
this.backend = backend;
|
|
854
|
+
this.loadId = loadId;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
521
857
|
}
|