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