Files

156 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# agentik-desktop — десктопный клиент агента agentik
Клиент агента **agentik** на Kotlin Multiplatform + **Compose Desktop** (цель сборки —
только JVM). UI общается с агентом через готовую библиотеку `pw.binom.agentik:client`:
она даёт `Agent` / `Conversation`, живой поток событий (SSE), авто-reconnect,
кэш списка диалогов и `interrupt()` для кнопки «Стоп».
Проект уже **собирается и запускается** (`./gradlew run`), а не только спроектирован.
## Что где лежит
```
sketches/index.html # ВСЕ экраны одним документом: список диалогов, чат, запись,
# группы, агенты, проверка связи, пустые состояния.
# Это и черновик, и «одобренное» — отдельного approved/ больше нет.
sketches/style.css # палитра и стили макетов (цвета числами — так и надо, это макет)
src/jvmMain/kotlin/... # код приложения
src/jvmTest/kotlin/... # тесты
```
Документы рядом:
- `REQUIREMENTS.md` — требования (накиданы, ни один пункт не обязателен в текущей форме).
- `STORAGE.md` — что и где хранится на клиенте (SQLite + JSON).
- `CACHE.md` — кэш сообщений: что есть в библиотеке, схема «только новое».
- `THEMES.md` — цвета не пишем в коде, только из семантической схемы.
- `MARKDOWN-SOURCE.md` — откуда взята отрисовка Markdown.
- `BORROW-FROM-ASSISTENT.md` — что берём из `ai/assistent`, а что нет.
- `MIC-ASR-SEARCH.md` — библиотеки микрофона/распознавания (`mic-kmp`, `asr-kmp`, `vad-kmp`).
## Структура кода
```
pw.binom.agentik.desktop
├── Main.kt # точка входа: окно, реестр, состояние
├── settings/ # AppPaths, AppSettings/AgentConfig, SettingsRepository
├── persistence/ # ConversationMetaRepository (группы/lastSeen/preview/watermark)
├── session/ # LiveTurn, ChatSession, AgentConnection, AgentRegistry
├── model/ # UiModels + Mappers (журнал/стрим → UI)
├── media/ # AvatarStorage (копия картинки агента)
├── net/ # ConnectionChecker (проверка связи)
├── markdown/ # вендоренный парсер Markdown (из assistent)
└── ui/ # App, AppState, theme/, components/, screens/
```
Ключевые решения по слоям — в `STORAGE.md`; кратко:
- **Транспорт целиком в `:client`.** HTTP/JSON/SSE/Bearer, reconnect, кэш списка
диалогов — не переписываем.
- **Сообщения** — `KsqliteJournalStore` из `:journal-ksqlite` (offline-история).
- **Группы, `lastSeen`, превью, watermark синхронизации** — наш
`ConversationMetaRepository` (сервер про это не знает).
- **Отдельная SQLite-база на агента**: id диалогов серверные, у двух агентов
могут совпасть; плюс группы локальны для агента.
## Текущее состояние реализации
Готово и покрыто тестами:
- **Настройки** — `settings.json` (агенты, тема, выбранная группа, последний диалог),
`clientId` генерируется один раз. Атомарная запись.
- **Живой список диалогов** — `AgentConnection.conversationsFlow` опрашивает
встроенный кэш либы (`conversationStore.listFlow`, warmup-цикл на старте) и
дополнительно подписан на SSE-поток агента: диалог, созданный с другого
клиента (или самим сервером), появляется в списке сам; свои создания и
удаления отражаются сразу. Ошибки сервера не считаются «пустым списком» —
последний известный список сохраняется.
- **Реестр агентов** — добавление/правка/удаление, подключение, деградация в
«не отвечает» без падения приложения.
- **Стриминг** — `LiveTurn` применяет события `Working → StartReasoning →
AppendText → ToolCall/ToolResult → End/Interrupted/Error`; `ChatSession`
догоняет историю (`after = synced_at`), пишет в локальный кэш, склеивает
live + history, обновляет превью и `lastSeen`.
- **История/UI-модели** — мапперы `MessageRecord → UiMessage` со склейкой
`ToolCall`+`ToolResult` в одну карточку.
- **Группы** — CRUD групп + раскладка диалогов (каскад: удаление группы не
удаляет диалоги).
- **Тема** — семантические цвета (`AgentikTheme.colors`), тёмная + светлая
схемы; отдельно пользовательские цвета агентов.
- **Markdown** — вендоренный рендер из `ai/assistent` (см. `MARKDOWN-SOURCE.md`).
- **Аватары** — копирование выбранного файла в `~/.agentik-desktop/avatars/`.
- **Проверка связи** — «ответ сервера» + создание/удаление временного диалога,
с таймингами.
- **UI (Compose)** — рейка агентов, список диалогов (поиск, папки, бейджи),
чат с живым стримингом, композер, окна «Агенты», «Группы», «Новый диалог»
(при нескольких агентах — выбор, при одном — сразу) и «Параметры диалога»
(кнопка `⋯` в шапке чата: имя, обслуживающий агент, признак «Временный»,
даты создания/обновления, удаление), пустые состояния, адаптивная раскладка
(узкое окно — список или чат).
- **Голос** — микрофон → VAD → распознавание (`voice/`): кнопка микрофона
или `Ctrl+M` из чата запускает запись (`mic-api`), Silero VAD (`vad-kmp`) отмечает наличие речи,
Vosk (`asr-vosk`, модель внутри артефакта) отдаёт промежуточный текст над
полем ввода, финал дописывается в черновик. Тишина текстом не становится.
Нативные модели считаются на одном выделенном потоке. На старте `main()`
делает [`VoiceNative.warmUp()`] — обязательный прогрев Vosk/ONNX ДО
инициализации skia/Compose (иначе создание модели после отрисовки окна
роняет JVM в C++-логах; подробности в `MIC-ASR-SEARCH.md`).
- **Реальная интеграция** — интеграционные тесты гоняются против живого
`:standalone`-агента (Qwen через vLLM), а не только in-memory-фейков:
`RealServerIT` (диалог → отправка → живой ответ LLM → локальный кэш) и
`RealAppStateIT` (весь `AppState`: реестр → подключение → живой список →
создание диалога → стриминг → превью/lastSeen), `RealSpeechIT` (речь:
прогрев нативных ASR → инициализация Compose → распознавание реального wav
→ «раз два три четыре пять»). См. `AGENTIK_E2E_URL`.
Проверено вживую (окно приложения + живой агент): список диалогов подтягивается
с сервера, кнопка «+» создаёт диалог, сообщение уходит и ответ приходит в UI,
`Ctrl+M` переводит микрофон в запись и обратно без падения JVM.
Не начато / заглушки:
- **Свёрнутый вид хода работы агента** — сейчас живой ход показывается
развёрнуто (reasoning, карточки ToolCall/ToolResult, спиннер). Как его
складывать — вопрос дизайна, не решён (REQUIREMENTS §9).
- **Кусок прерванного ответа в кэше** — R39 выполнен (прерванный ответ в
историю не пишется), а вот хранить ли его обрывок в кэше — вопрос не решён
(REQUIREMENTS §9).
- **Светлая тема визуально не проверялась глазами** (`THEMES.md`); в
`ui/ThemeRenderTest.kt` проверено программно: фон код-блоков берётся из
слота темы, порядок яркостей в светлой палитре корректный.
- **Голос: движок зашит на Vosk** (`VoskVoiceRecognizer`) — Qwen3/Whisper из
`asr-kmp` подключаются заменой `VoiceRecognizerFactory`; выбор движка/модели
в настройках не сделан.
## Как запустить
```bash
./gradlew run # окно приложения
./gradlew jvmTest # тесты (реальная SQLite, фейковый транспорт)
# Интеграционные тесты против живого агента (по умолчанию пропускаются):
AGENTIK_E2E_URL=http://127.0.0.1:8080/agentik ./gradlew jvmTest \
--tests '*RealServerIT' --tests '*RealAppStateIT'
./gradlew jvmJar # собрать jar
```
## Что решено
- **Сначала десктоп, затем Android — по тем же лекалам.** Десктоп делаем
образцом: понятный и читаемый по коду. Потом Android повторяет решения.
- **Несколько агентов** — различение цветом или картинкой (выбирает пользователь).
Картинку клиент **копирует себе**, а не берёт по адресу.
- **Markdown** — показывать разметкой. Готовый рендер взят из `ai/assistent`.
- **Папки над списком диалогов** — сохранены.
- **Проверка связи** — отдельным окном, с таймингами шагов.
- **Ориентир по размеру:** ~9 небольших кусков логики поверх библиотеки, а не
собственная инфраструктура (см. `STORAGE.md`, `CACHE.md`).
## Что предстоит решить
1. **Запись** — по нажатию или на удержание.
2. Форма показа хода работы агента в свёрнутом виде.
3. Различение агентов: одного цвета может оказаться мало.
4. **Что делать с куском прерванного ответа** в кэше (`CACHE.md`, п. 3.2).
5. Где показывать несколько агентов в одном списке диалогов (сейчас список —
у активного агента).