Files

33 KiB
Raw Permalink Blame History

Требования к десктопному клиенту agentik

Статус: накидываем. Ни один пункт не обязателен к исполнению в том виде, как записан. Позже пересматриваем.

Основа дизайна: вариант 1 (approved/001-sidebar-utility.html) — выбран, лежит в approved/.

Сейчас описываем только поведение. Кода не пишем, пока требования не устоятся.


1. Общее

  • R1. Стиль — тёмная тема, уже принятая в клиентах пользователя (фон #121218, панели #17212B, текст #EBEBEB). Это текущая тема, а не единственная — см. R1.1.
  • R1.1. Цвета берём из цветовой схемы, а не пишем в коде числом. Предусматриваем, что тем будет несколько (тёмная, светлая и другие). В коде не должно быть цвета вида #121218 — вместо этого берём цвет из текущей схемы по смыслу: «фон», «панель», «текст», «акцент», «ошибка», «успех». Смена темы тогда ничего не ломает — меняется схема, код остаётся.
  • R1.2. Имена берём по смыслу, а не по виду. Не «тёмно-серый» и не «синий», а «фон», «панель», «акцент». Смысл не меняется от смены темы, а название цвета — меняется.
  • R1.3. В макетах (sketches/, approved/) цвета стоят числами — это нормально, макет должен выглядеть как задумано. На этом основан и способ проверки темы: имена в коде — из R1.1, числа — только в макетах.
  • R1.4. Отдельно проверяем, что при смене темы ничего не осталось вписанным числом: ни текст, ни фон, ни границы, ни тени, ни цвет «наведения» курсора. Если цвет не из схемы — он не переключится и будет выбиваться.
  • R1.5. Особый случай — цвета для различения ассистентов (цвет агента из настроек). Они не из схемы: это пользовательские цвета, они заданы сознательно и в другой теме остаются собой. Их надо отличать от цветов темы, чтобы не перепутать при смене.
  • R2. Что видно на экране в любой момент: список диалогов, выбранный диалог и поле ввода. Больше ничего обязательного нет.

2. Раскладка и адаптивность

  • R3. Широкое окно — две части: список диалогов слева, открытый диалог справа.
  • R4. Узкое окно — на экране что-то одно: либо список диалогов, либо конкретный диалог. Переключение — как в Телеграме.
  • R5. В узком окне у открытого диалога сверху слева — кнопка «Назад». Нажатие возвращает к списку диалогов на всю ширину.
  • R6. Порог «широкое/узкое» — по ширине окна, одна величина. Какой именно размер считать порогом — решим позже.

3. Список диалогов

  • R7. Группировка диалогов — «Работа», «Дом» и подобное. Управляемая: группы можно добавлять, переименовывать и удалять.
  • R8. В строке диалога: название, последнее сообщение, время, счётчик непрочитанных.
  • R9. Есть поиск по диалогам.
  • R9.1. Над списком — лента групп (круглые кнопки): «Все чаты», «Работа», «Дом» и другие. Это часть одобренного варианта 1, не забывать при доработках.
  • R9.2. Справа в этой ленте — кнопка «Группы» с карандашом (✎), прижата к правому краю, отдельно от чипов. Прежний «+» там больше не стоит. Причина: рядом чипы групп, и всё похожее на «+» читается как «добавить группу», хотя диалог создаётся кнопкой в шапке списка. Макет — approved/006-groups-manage.
  • R9.3. Кнопка открывает окно «Группы»: список групп, у каждой «изменить» и «удалить», внизу «+ Добавить группу».
  • R9.4. «Все чаты» — встроенная группа, не удаляется и не переименовывается (в макете помечена «встроенная», пунктирная рамка). Нужна как надёжное место, где виден весь список: иначе при удалении группы диалоги пропадут из виду.
  • R9.5. У группы только название. Цвет, значок, порядок — не делаем, пока не попросили.
  • R9.6. Пустая группа допустима — можно создать заранее и разложить диалоги потом.
  • R9.7. Удаление группы диалоги НЕ удаляет. В окне подтверждения это сказано первым делом: диалоги останутся и будут видны в «Все чаты». Диалоги обязаны быть где-то видны — «корзины» у них нет.
  • R9.8. Удаление группы — отдельным блоком «опасное» в окне изменения и красной залитой кнопкой в подтверждении. Не в одном ряду с «Сохранить».
  • R9.9. Макеты групп — МАКЕТЫ ОДОБРЕНЫ, лежат в approved/006-groups-manage/ (1-list, 2-add, 3-edit, 4-delete). Отдельные файлы по состояниям, без интерактивности — так и делаем.
  • R9.2. В строке диалога — круглый значок непрочитанных, если есть.

4. Открытый диалог

  • R10. Показ сообщений: мои и ассистента, с временем.
  • R10.1. В шапке открытого диалога — кнопка ⋯ (три вертикальные точки), без визуальных границ, как в Telegram. Открывает окно «Параметры диалога».
  • R10.2. Окно «Параметры диалога»: название (с редактированием по ✎), карточка обслуживающего агента (имя + URL + статус «на связи»/«не отвечает»), признак «Временный диалог» (только просмотр — задаётся только при создании), «Создан» и «Обновлён» (dd.MM.yyyy HH:mm, системная тайм-зона), «Опасное» с кнопкой «Удалить диалог». Создание идёт через модалку выбора агента (R14.x), а не через это окно.
  • R11. Ответ ассистента можно прервать — на видном месте кнопка «Стоп».
  • R12. Сообщения приходят вживую, по мере генерации: текст дотекает куском за куском, а не появляется целиком в конце. Отображать именно так.
  • R13. Разметка (markdown). Если я или агент пишем текст, похожий на разметку, показывать его разметкой, а не сырыми символами: списки, выделение, код, таблицы, ссылки. Код для этого писать не надо — готовый рендер уже есть в репозитории ai/assistent, брать оттуда.
  • R13.1. Откуда брать — точно. См. MARKDOWN-SOURCE.md: репозиторий ai/assistent, ветка main, коммит 2c5b3f9. Копировать 4 файла: модель разметки, парсер, отрисовку и десктопную обёртку открытия ссылок. Одна внешняя зависимость — ru.otpbank.ai:markdown:0.51.0 (есть в Nexus).

5. Панель ввода

  • R14. Поле ввода, кнопка «Отправить» и отдельная кнопка записи микрофона.
  • R15. Подсказка о горячих клавишах под полем убирается — её не должно быть.
  • R16. Пока идёт запись, должно быть понятно, что сейчас пишется голос.
  • R17. Отмена записи — явная.
  • R18. Голос: кнопка включает микрофон и останавливает его. Точная механика (по нажатию или на удержание) — решим позже.
  • R19. Распознавание речи — отдельная тема, здесь не решается. Важно только: где кнопка, что видно во время записи, что происходит после.
  • R19.1. Как читать микрофон — решено, пишем не мы. Подключаем свою готовую библиотеку mic-kmp (pw.binom.mic:mic-api-jvm, версия 1.0.0, есть в Nexus). Отдаёт звук потоком кусочков, а не файлом: можно распознавать сразу, не дожидаясь конца записи. Микрофон освобождается сам при остановке. Формат — 16 кГц, моно. Код из ai/assistent для этого не берём. Подробности — MIC-ASR-SEARCH.md.

6. Несколько агентов

  • R20. Предусмотреть возможность подключать несколько агентов и видеть их в одном клиенте. Не «много людей», а один клиент — несколько агентов.
  • R21. Возможный способ различения — по аватару: у каждого агента свой аватар, по нему и отличаем. Тема аватаров не продумана, это накидка.
  • R22. Другие возможные способы (на выбор, обсудить):
    • Цветовой акцент — у каждого агента свой цвет: полоса у пузыря ответа, цвет имени в шапке;
    • Группировка в списке — диалоги сгруппированы по агентам, с заголовком группы (как секции каналов);
    • Переключатель агента — как выбор аккаунта в Телеграме: все диалоги одного агента разом;
    • Метка в строке диалога — маленький значок/буква агента рядом с названием.
  • R23. У библиотеки уже есть всё для этого: агент создаётся вызовом с двумя значениями — имя и адрес. Имя клиент знает сам, сервер его не хранит. Каждому агенту — своё имя и адрес.
  • R23.1. Экран настроек агентов — МАКЕТ ОДОБРЕН, лежит в approved/004-settings-agents.html (черновик — sketches/004-settings-agents). Окно поверх чата, не отдельная страница. Два состояния: список агентов и форма нового.
  • R23.2. В строке агента видно: имя, адрес, состояние («на связи» / «не отвечает»), кнопки изменить и удалить.
  • R23.3. Форма нового агента: название, адрес сервера, и чем отличать в списке — цвет или картинка. Под каждым полем — пояснение простым языком.
  • R23.3.1. Переключатель «Цвет / Картинка»: выбирается что-то одно. Цвет — простой выбор из готовых, им помечаются диалоги агента в списке (полоска у строки, точка у группы). Картинка — файл с диска; показывается круглым превью, рядом «Выбрать файл…» и «Убрать».
  • R23.3.2. Картинку копировать себе, а не брать по исходному адресу. Выбранный файл сохраняется внутрь клиента (в свою папку), и дальше используется только эта копия. Ссылку на исходный путь не хранить: файл могут удалить, переименовать, флешку вынуть — и значок у агента пропадёт. Работает и для картинки на диске, и для картинки по ссылке: скачать и положить себе.
  • R23.3.3. Выбранная картинка показывается кружком там же, где был бы цвет: в списке агентов, в списке диалогов, в шапке чата. Убрать её можно в любой момент — тогда снова становится цвет.
  • R23.4. Проверка связи до сохранения — в отдельном окне поверх настроек, не строкой внутри формы. Нажал «Проверить связь» — открылось второе окно, в нём идёт проверка. Пока идёт: «спрашиваю агента…» с крутящимся значком. Результат — зелёный значок «отвечает» или красный «не отвечает», и под ним подробности: ответ на запрос, адрес, число диалогов, отклик в миллисекундах. Кнопка «Добавить агента» становится доступной только при успехе. Есть «Проверить снова». Закрытие окна возвращает к настройкам. Основа: агент отвечает на запрос состояния и отдаёт список диалогов; оба запроса ничего не меняют.
  • R23.5. Цвет агента работает не только в настройках: тем же цветом помечаются его диалоги в списке (полоска у строки, точка у группы).
  • R23.6. Список агентов сохраняется между запусками. Где именно — не решено.
  • R23.7. Доступа по паролю/ключу у библиотеки нет — поля для него не делать.
  • R23.8. Новый диалог: выбор ассистента — МАКЕТ ОДОБРЕН, лежит в approved/005-new-chat-picker.html. Черновик со всеми состояниями — sketches/005-new-chat-picker. По «+» спрашиваем, в каком ассистенте создать диалог — потому что диалог живёт внутри агента, отдельно его не создать.
  • R23.9. Окно показывается только если ассистентов больше одного. Один — диалог создаётся сразу в нём, спрашивать не о чем, окна нет и в помине.
  • R23.10. При нуле ассистентов кнопки «+» нет вообще. Создавать диалог не в ком. Вместо списка — объяснение, что нужно сначала добавить ассистента.
  • R23.11. В списке выбора видно: значок (цвет или картинка), имя, адрес сервера, состояние связи. Отмечен первый по умолчанию — создать диалог можно одним нажатием.
  • R23.12. Ассистента без связи выбирать можно — окно предупреждает словами, но не мешает. Связь может пропасть на секунду, а диалог человек создаёт осознанно. Ассистентов без связи из списка не выкидываем.

7. Состояние агента: что показывать

Сверка с исходниками здесь важнее всего. Ниже — по факту из кода, не по догадкам.

7.1. Что есть в самой библиотеке client (что клиент реально получает)

  • У каждого события и каждого сообщения есть дата. Значит доступно: длительность хода, сколько времени агент думал, когда пришёл ответ.
  • Есть события: начало ответа, рассуждение, текст по мере генерации, картинка, вызов инструмента (имя, аргументы, заголовок), результат инструмента, конец хода, прерывание, ошибка.
  • Есть события о диалогах: создан, удалён, переименован.
  • По диалогу: название, время последнего изменения, умеет ли принимать и отдавать картинки, временный он или постоянный.
  • Токенов, модели, цены, предела контекста — в библиотеке НЕТ. Ни одного числа. Проверено сплошным поиском: числовых полей в протоколе не существует вообще.

7.2. Решение: счётчиков токенов и кольца в интерфейсе НЕ будет

  • R24. Показывать израсходованные токены, долю от предела и кольцо заполнения не нужно. Данных для этого в библиотеке нет — значит и рисовать нечего. Кольцо из макета удалено.
  • R25. Никаких полей наугад: нет данных — нет цифры. Не подставлять правдоподобные числа вместо отсутствующих.
  • R26. Если когда-нибудь агент начнёт отдавать такие числа — вернёмся к вопросу. До тех пор тема закрыта.

7.3. Ход работы агента

  • R27. Показывать не только ответы ассистента, но и происходящее вокруг них: вызовы инструментов, рассуждения, ошибки.
  • R28. Свёрнутый вид — одна строка: кратко, что происходит (например, «вызов инструмента: такой-то»).
  • R29. Развёрнутый вид — подробности шага: аргументы, результат, время.
  • R30. Сворачивание и разворачивание — по нажатию на строку.

8. Простота кода и кэш сообщений

8.1. Цель: клиент должен читаться

  • R31. Главная цель — чтобы клиент был максимально прост по коду. Я должен открыть исходники и понять, что там происходит. Это важнее, чем сэкономить строки или вынести лишнее.
  • R31.1. Сначала десктоп, затем Android по тем же лекалам. Десктопный клиент — образец. Когда он получится удобным и понятным, Android делается по его решениям. Поэтому простота кода здесь — не пожелание, а условие.
  • R32. Распознавание голоса, микрофон и детекция речи — готовые библиотеки, свои (mic-kmp, asr-kmp, vad-kmp). Писать заново ничего не надо, и это уже не «вынос в отдельное место» — библиотеки и есть отдельное место.
  • R33. Материал найден, разобран: MIC-ASR-SEARCH.md. Там же сказано, почему код микрофона из ai/assistent брать не надо.
  • R34. Из ai/assistent берём только приёмы отрисовки, плагины и версии — не архитектуру, не экраны, не дизайн. Разбор: BORROW-FROM-ASSISTENT.md.

8.2. Кэш сообщений на клиенте — решение принято

Задача: не тянуть всю историю диалога заново при каждом открытии окна.

Схема:

  1. Открыли диалог — смотрим свой кэш.
  2. Берём из кэша последнее сообщение и его дату.
  3. Спрашиваем библиотеку client: есть ли сообщения новее этой даты.
  4. Были — забираем только новые, добавляем в кэш, рисуем.
  5. Не были — ничего не делаем.
  • R35. Это работает: в библиотеке client всё нужное есть. Функция Conversation.getMessages(after, offset, limit) отдаёт сообщения строго новее указанной даты (проверено: в отборе created_at > ?). Пустой ответ = после нашего сообщения ничего не появилось. Есть и версия потоком, страницами по 100 (PAGE_SIZE = 100). Отдельно создавать диалог для проверки не нужно — только если открываем новый.
  • R36. Дубли отсекаются по id сообщения. Идентификатор один и тот же и в живом потоке, и в истории (Message.id). То есть при догрузке не нужно угадывать, что уже нарисовано — сверяем по идентификатору.
  • R37. Самого кэша в библиотеке нет — это пишем мы. Библиотека даёт только «спроси, что новее».

Три места, где схема может порваться (найдено в коде, не предположения):

  • R38. Одинаковые даты. Отбор строго «новее» (created_at > ?), поэтому если два сообщения получили ровно одну и ту же дату — второе в ответ не попадёт. Порядок внутри одной даты сервер задаёт сам (сортировка по времени, затем по id). Реальный риск невелик (дата с точностью до миллисекунд), но при сверке по id (R36) он не страшен вовсе.

  • R39. Прерванный ответ в историю не попадает. Если ответ прервали кнопкой «Стоп», кусок ответа остаётся только в живом потоке, в историю он не пишется (Event.Interrupted). В кэше окажется то, чего на сервере нет.

  • R40. Живой поток тоже надо просить «с этого момента». У подписки на события есть тот же параметр «после»; без него после переподключения пропустим события.

  • R41. Решение: кэш делаем. Схема — из R35.

  • R41.1. Хранилище — абстракция. Клиент просит «дай сообщения диалога», «сохрани настройки» — и не знает, где это лежит. Реализацию можно заменить (понадобится на Android) — экраны не тронутся. Подробно: STORAGE.md.

  • R41.2. Ни одного упоминания базы вне реализации. Если в экране или в логике клиента встретилось sqlite / SQL / query — абстракция прохудилась. Тот же признак, что и с цветами числом в коде.

  • R41.3. Реализация — SQLite, тот же подход, что в agentik: SQLDelight 2.3.2 (умеет и JVM, и Android). Не файлы.

  • R41.4. Три отдельные абстракции, не одна на всё: сообщения диалога; настройки; снимок списка диалогов. Разная жизнь — разными интерфейсами.

  • R41.5. Настройки — JSON-файл, не в базе. Их правит человек руками: сломался адрес агента — открыл, увидел, исправил. С базой для этого нужен инструмент. Там же тема и выбранная группа.

  • R41.6. Секреты в JSON не пишем. Пароль/ключ — в системное хранилище паролей, в настройках только ссылка.

  • R41.7. Картинки — файлами, не в базе. В сообщении картинка едет массивом байт; держать её в базе нельзя — база распухнет и станет тяжёлой для копирования. В базе — ссылка на файл.

8.3. Что не выносим

  • R42. Экраны, пузырь сообщения, хранение списка агентов — остаются в клиенте. Это и есть клиент, выносить их некуда.
  • R43. Полезный признак, что что-то не вынесено: в экране накопилась логика длиннее пары десятков строк. Значит, это место жить в экране не должно.

8.4. Кэш наполняется фоново, а не по факту открытия

Задача: превью и история должны быть у всех диалогов, а не только у тех, что открывали в этом окне. Раньше «…» в списке стояло именно потому, что запись появлялась лишь при открытии диалога.

Схема (на каждого агента):

  1. Одна подписка на все события-сообщения (outbox.conversationEvents(after=null, conversationId=null)).
  2. На End/Interrupted любого диалога — догон журнала по watermark'у и пересчёт превью; данные кладутся в локальный кэш.
  3. На старте — догон диалогов, чей updatedAt с сервера новее нашего watermark'а, включая никогда не открытые.
  • R44. Источник истины — журнал, не SSE. Outbox — короткий bounded tail: события за время, пока клиент был выключен, из него выпадают. Поэтому догон идёт по журналу (updatedAt / synced_at), а SSE лишь триггерит его.
  • R45. Открытый диалог фоновый ingest не трогает. Пока диалог открыт, его сообщения ведёт сессия; engine пропускает этот диалог, чтобы не дублировать работу. Общий мьютекс на диалог сериализует запись на транзишене «открыли / закрыли».
  • R46. API менять не нужно (снимает открытый вопрос из §9): превью считается локально из журнала, число непрочитанных — journal.count(after=lastSeen).

9. Решения, которые ещё не приняты

  • Запись: по нажатию или на удержание.
  • Нужны ли вложения (картинки) в этом клиенте.
  • Как именно различать агентов (см. R21–R22): цвета или картинки достаточно?
  • Нужен ли картинке размер/форма — показывать кружком без изменений или обрезать.
  • Где хранится список агентов — в контроле настроек (JSON) или спрашивать сервер.
  • Как выглядит показ хода работы агента в свёрнутом виде.
  • Сколько держать в кэше и когда чистить (старые диалоги).
  • Что делать с куском прерванного ответа в кэше (R39): хранить или выбрасывать.
  • Группы — только на устройстве или на сервере? Сервер про них не знает (STORAGE.md, §7). Если только у нас — раскладка не поедет между десктопом и телефоном.
  • Бейдж — число или точка. Точка сервер не трогает вообще (STORAGE.md, §6.2).
  • Где каталог клиента — ~/.agentik/ или системный («Документы»). На Android понятие «домашний каталог» своё.

10. Как проверяем

Проверка — живой прогон, а не «собралось». Правило пользователя: перед показом гонять сценарий руками; на экран смотреть, а не верить описанию.