- 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 зелёных
6.0 KiB
Задача: медиа-тулы для тулсета media (агент «Порфирий»)
Механика тулсетов/скиллов УЖЕ готова (AgentToolkit, Skills, Toolsets — см. TASK-agent-tools.md, закоммичено c5cbc89). Твоя задача — реализовать сам тулсет media с исполнителем на базе Jellyfin. Чистую механику НЕ трогать.
Контекст (что уже есть)
AgentToolkit(app-phone/.../agent/AgentToolkit.kt): ядро 5 тул + диспач + подмена. Регистрация тулсета:ToolsetRegistry.register(Toolset(...), executor).Toolset/ToolSignature/ToolsetExecutor(Toolsets.kt): сигнатуры с префиксомmedia_, аргументы строками (query,id,seek_ms?).PhoneApp.ktуже создаётAgentToolkit(skills=..., toolsets=ToolsetRegistry(), showText=...)— тулсеты пустые, тулы не зарегистрированы.JellyfinClient(lib-core/src/commonMain/kotlin/pw/binom/viewmate/core/media/JellyfinClient.kt):suspend fun search(userId: String, query: String): List<JellyfinItem>suspend fun item(userId: String, itemId: String): JellyfinItem?suspend fun allItems(userId: String, types: List<String>): List<JellyfinItem>fun posterUrl(itemId: String, maxWidth: Int): String
JellyfinItem(JellyfinModels.kt): Id, Name, Type, Overview, ImageTags, SeriesName, SeasonId, IndexNumber, ParentIndexNumber, RunTimeTicks.- В PhoneApp:
jellyfin: JellyfinClient,chatDao,server.hub(broadcast),actions(воспроизведение),state.
Что сделать
1. Тулсет media (зарегистрировать в PhoneApp)
Тулы (имена БЕЗ префикса — префикс media_ добавляется механизмом):
search— поиск фильмов/сериалов по названию. Аргументы:query(обязательный),limit?(опциональный, дефолт 5).- Возврат: список строк «id: Название (Год) — Тип» (до limit), либо «ничего не найдено по запросу ...».
info— детали элемента по id. Аргументы:id(обязательный).- Возврат: Название, Тип, Год, Обзор (Overview, обрезать до ~300 символов), длительность (RunTimeTicks/10_000_000 сек), есть ли постер.
play— запуск воспроизведения на очках. Аргументы:id(обязательный),seek_ms?(опциональный).- Проверки: очки подключены (hub.connected > 0), видео скачано на очки (локальный путь file:////video.mkv существует на очках — через hub/каталог), звук готов на телефоне (audioReady).
- Воспроизведение — через существующий механизм DetailsScreen (actions.watch(item.Id, audioIndex, localVideoPath, phoneAudioReady)) — вынеси/переиспользуй логику, не дублируй криво.
- Возврат: «запущено: <Название>» или понятная ошибка с причиной («очки не подключены», «видео не скачано на очки — скачай вручную», «звук не готов»).
2. Юзер-ID для Jellyfin
Jellyfin требует userId. Посмотри, как DetailsScreen/CatalogScreen получают userId (вероятно app.jellyfin.users() первый или конфиг). Используй тот же механизм, закешируй.
3. Скилл media (файл skills/media.md)
Формат (как в Skills.kt):
---
name: media
description: поиск фильмов, детали, запуск/управление просмотром.
---
Текст скилла: объясни модели, что для работы с медиа вызвать enable_toolset|media, и опиши тулы search/info/play с аргументами.
4. Тесты (JVM, без Android — по возможности)
Механику тестировать НЕ надо (уже покрыта). Но исполнитель media_search можно покрыть, если JellyfinClient абстрагируется (интерфейс/fake). Если JellyfinClient — конкретный класс с Ktor, сделай минимальный интерфейс MediaSource (search/item) и обёртку — тогда executor тестируется на фейке. Не усложняй сверх меры: если это требует большого рефакторинга, опиши, что осталось непокрытым.
Ограничения
- Код только Kotlin
- НЕ трогать AgentToolkit/Skills/Toolsets (механика) — только регистрация тулсета и его исполнитель
- НЕ ломать существующие экраны (DetailsScreen/CatalogScreen)
- Все новые файлы — в app-phone (агент живёт на телефоне)
- Сборка:
./gradlew :app-phone:assembleDebug— зелёная;./gradlew :app-phone:testDebugUnitTest— зелёный (все старые тесты остаются зелёными) - НЕ коммитить
Проверка
./gradlew :app-phone:testDebugUnitTest— зелёные (старые + новые)./gradlew :app-phone:assembleDebug— собирается- Опиши в ответе: какие файлы создал/изменил, как зарегистрирован тулсет, как получается userId, что осталось непокрыто тестами.