7527cf8848
- LlmClient интерфейс; RemoteLlmClient (OkHttp, llm.binom.pw) и LocalLlmClient (LiteRT-LM, Gemma 4 E2B) - LocalLlm: Gemma4E2B (SHA-256, GPU/CPU-бэкенды), buildLocalLlmPrompt (SYSTEM→systemInstruction, TOOL→user), GemmaModelDownloader (стриминг, прогресс, фолбэк источников), спекулятивное декодирование - LlmPrefs (персистентность выбора), PhoneApp: фабрика по llmChoice, пересоздание ассистента, скачивание/отмена - UI: GlassesInfoScreen — выбор сервер/локальная + прогресс; ChatScreen — индикатор активной модели - Тесты: +12 (LlmLocalTest) — phone 154 зелёных
9.0 KiB
9.0 KiB
Задача: интерфейс LLM + две реализации (Qwen удалённо / Gemma E2B локально) + переключатель
Дизайн — TASK-local-llm.md. Заметка с разбором litertlm (скопирована в репо, читай её): docs/litertlm-openassistant-notes.md. Полная архитектура агента (тулсеты/скиллы) — TASK-agent-tools.md. Реализуй в app-phone.
Контекст
- Сейчас
LlmClient— класс с прямым OkHttp кhttps://llm.binom.pw/v1/chat/completions(Qwen3.8-27B-NVFP4). Файл:app-phone/src/main/kotlin/pw/binom/viewmate/phone/agent/LlmClient.kt(НЕ app-phone/agent/ — путь с src/main/kotlin!). Вызывается изPhoneApp.ensureAssistant()(app-phone/src/main/kotlin/pw/binom/viewmate/phone/PhoneApp.kt) — создаётся один раз и передаётся вAssistant(chat = { history, system -> llm.chat(history, system) }). - Нужно: интерфейс + две реализации + ручной глобальный переключатель в UI телефона. В будущем всё должно работать оффлайн.
- Модель для локальной: Gemma 4 E2B (2B), формат
.litertlm(LiteRT LM, Google). Разбор — вdocs/litertlm-openassistant-notes.md(скопирован в репо — читай его, не /root/notes/).
Что сделать
1. Интерфейс LlmClient
Превратить LlmClient в интерфейс (или добавить интерфейс-обёртку):
interface LlmClient {
suspend fun chat(messages: List<ChatMessage>, system: String? = null): String
}
- Существующий класс →
RemoteLlmClient(реализация интерфейса, код не менять по сути — только implements). - Все использования (PhoneApp.ensureAssistant) — через интерфейс.
2. Локальная реализация — LocalLlmClient (Gemma 4 E2B, LiteRT LM)
По заметке /root/notes/openassistant-lite-llm.md (секция «Две нейронки + токенайзер», «Как повторить», «Скорость»):
- Зависимость:
com.google.ai.edge.litertlm:litertlm-android:0.14.0в app-phone/build.gradle.kts (репозиторий — google(), у нас в settings должен быть; проверить). ВАЖНО: litertlm 0.14.0 требует kotlinx-coroutines 1.11.0 (иначе NoSuchMethodError close$default на SendChannel — подтверждено автором Modern-Apps, см. его build.gradle.kts:47-48). Поднять версию kotlinx-coroutines в gradle/libs.versions.toml с 1.10.2 до 1.11.0 — проверить, что ничего не ломается (тесты после этого обязаны быть зелёными). - Модель: файл
gemma-4-E2B-it.litertlm(SHA256181938105e0eefd105961417e8da75903eacda102c4fce9ce90f50b97139a63c), ~0.8 ГБ. Хранить вgetExternalFilesDir(null)(как у автора). НЕ класть в APK (большой) — загружается отдельно. - Загрузка: если файла нет — скачать с зеркала (автор:
https://data.vayunmathur.com/models/, но у нас доступнее HF:https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/<файл>), SHA256-проверка. Сделай через OkHttp (уже есть в проекте) + поток в файл, с прогрессом (лог). - Запуск (из заметки):
Engine(EngineConfig(modelPath, backend = GPU(), visionBackend = GPU(), audioBackend = CPU(), cacheDir))ExperimentalFlags.enableSpeculativeDecoding = trueПЕРЕДengine.initialize()(спекуляция у Gemma 4 встроена в архитектуру — MTP, флаг включает рантайм-путь)ConversationConfig(systemInstruction, initialMessages, tools?, automaticToolCalling?)+sendMessageAsyncстриминг
- Chat: перевести
List<ChatMessage>+ system в формат litertlm (initialMessages — история, systemInstruction — system). Вернуть накопленный текст ответа. Стриминг можно агрегировать (не обязательно гнать по частям наружу — контрактchat(): String). - Обработка ошибок: модель не загружена → понятное сообщение («локальная модель не скачана, скачайте в настройках»), инициализация упала → текст ошибки. НЕ падать.
- Threading: Engine litertlm требует аккуратности с потоками — смотри пример автора (InferenceService.kt). Скорее всего вызовы идут из корутины — оберни в withContext(Dispatchers.IO) и соблюдай требования движка.
- Время жизни: Engine создаётся один раз (как ассистент), переиспользуется; инициализация — лениво при первом вызове (модель может весить ~1-2 ГБ в RAM).
3. Переключатель (ручной, глобальный)
- Состояние:
LlmmodelChoice=REMOTE|LOCAL(или enum с именами). Хранить в SharedPreferences (переживает рестарт). Global: влияет на весь ассистент, не на сессию. - Фабрика:
PhoneApp.ensureAssistant()выбирает реализацию по сохранённому выбору: REMOTE →RemoteLlmClient, LOCAL →LocalLlmClient. Переключение — пересоздать ассистента (или хотя бы llm-ссылку) при изменении выбора. - UI: на экране «Информация об очках» (GlassesInfoScreen, там уже есть InfoRow) — или лучше отдельный блок в GlassesScreen — добавить переключатель «Модель ассистента: Qwen (сервер) / Gemma (локально)». Компонент: два варианта (радио-кнопки или сегментированная кнопка), плюс статус: для LOCAL — «модель скачана/не скачана», кнопка «Скачать модель» (с прогрессом), для REMOTE — «сервер».
- Индикатор активной модели — в чате (ChatScreen) мелкой подписью «модель: qwen/gemma» (не обязательно, но желательно — пользователь должен видеть, кто отвечает).
4. Проверка/тесты
- Юнит-тесты (JVM, без Android — что можно):
- Выбор реализации по настройке (фабрика/логика выбора — чистая часть).
- Формирование промпта для litertlm (маппинг ChatMessage → initialMessages) — если вынести в чистую функцию.
- SHA256-проверка файла (чистая функция).
- Сборка:
./gradlew :app-phone:testDebugUnitTestи:app-phone:assembleDebug— зелёные (все старые 142 теста остаются зелёными).
Ограничения
- Код только Kotlin
- НЕ трогать механики тулсетов/скиллов (AgentToolkit/Skills/Toolsets), Assistant.kt — менять только точку создания LlmClient
- НЕ ломать существующие экраны
- НЕ коммитить
Проверка на железе (после твоей части)
- Сборка + установка на телефон (142, adb).
- Загрузка модели (нужен доступ к HF — проверь, скачай на машину, залей через adb, если HF с телефона недоступен).
- Переключение REMOTE ↔ LOCAL, общение с ассистентом на каждой.
- Оффлайн-тест:
svc wifi disable→ локальная модель отвечает, удалённая — понятная ошибка. - Замер скорости локальной (ток/с из логов).
Ответ
Опиши: какие файлы создал/изменил, как устроен интерфейс, как инициализируется Engine, где переключатель, что осталось проверить на железе (особенно: работает ли litertlm на нашем устройстве, скачивается ли модель).