Files
magistr/AI_ASSISTANT_IMPLEMENTATION_PLAN.md
2026-08-05 15:37:34 +03:00

1579 lines
107 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.
# План реализации отдельного AI-ассистента для Magistr
> Статус: исходная техническая спецификация для создания отдельного проекта
>
> Версия документа: 1.0
>
> Дата: 5 августа 2026 года
>
> Основной интегрируемый продукт: Magistr
>
> Рабочее название нового репозитория: `magistr-assistant`
## 1. Назначение документа
Документ описывает реализацию отдельного коммерческого микросервиса с персонажем-проводником для Magistr. Ассистент помогает пользователям находить страницы и элементы интерфейса, объясняет последовательность действий, подсвечивает нужные элементы и принимает запросы текстом или голосом.
Документ должен использоваться как:
- исходная архитектурная спецификация нового репозитория;
- основание для декомпозиции задач в Gitea;
- контракт между новым сервисом и Magistr;
- перечень обязательных проверок перед пилотом;
- фиксация продуктовых ограничений первого релиза;
- точка продолжения для последующего режима выполнения действий после подтверждения пользователя.
Этот документ не является разрешением на создание нового репозитория, изменение production-инфраструктуры или подключение платных AI API. Такие действия выполняются отдельными этапами после явного начала реализации.
## 2. Зафиксированные продуктовые решения
| Вопрос | Решение |
|---|---|
| Пользователи первого релиза | `ADMIN`, `EDUCATION_OFFICE`, `DEPARTMENT` |
| Возможности первого релиза | Подсказки, переходы по разрешённым страницам и подсветка элементов |
| Автоматическое выполнение действий | Не входит в первый релиз; возможно позже, только после явного подтверждения |
| Языки | Русский и английский |
| Персонаж | Один для всех университетов |
| Образ | Животное или абстрактный проводник; окончательный образ выбирается позже |
| Текстовый AI | Внешний российский сервис по API через заменяемый адаптер |
| Голос | Push-to-talk: запись только пока пользователь удерживает кнопку |
| Управление подпиской | Только оператор платформы |
| Оплата | Вручную по договору, без платёжной системы |
| Репозиторий | Отдельный репозиторий и независимый релизный цикл |
| Данные диалогов | Полный текст и аудио не сохраняются |
| Аналитика | Только обезличенная статистика и явная оценка ответа |
| Retention пользовательской аналитики | Автоматическое удаление данных старше 30 дней |
## 3. Цель первого релиза
Пользователь должен иметь возможность открыть персонажа, задать вопрос текстом или голосом и получить короткую инструкцию. Если пользователь имеет право на соответствующий раздел, интерфейс Magistr должен перейти на нужную страницу и подсветить следующий элемент. Если права отсутствуют, ассистент должен прямо сообщить об ограничении и указать, к какой роли следует обратиться.
Пример ожидаемого поведения:
1. Пользователь спрашивает: «Где менять время пар в субботу?».
2. Ассистент проверяет роль и доступные возможности.
3. Для `ADMIN` или `EDUCATION_OFFICE` открывает настройки временных слотов.
4. Подсвечивает выбор области действия и объясняет, что нужна субботняя сетка.
5. Для `DEPARTMENT` не пытается открыть запрещённую страницу и сообщает, что изменение выполняет администратор или учебный отдел.
## 4. Что не входит в первый релиз
- автоматическое нажатие кнопок;
- заполнение или отправка форм;
- создание, изменение и удаление данных Magistr;
- доступ ассистента к расписаниям, пользователям и другим бизнес-данным напрямую через API Magistr;
- чтение всего DOM и отправка содержимого страницы AI-провайдеру;
- долговременная память о пользователе;
- сохранение истории диалога на сервере;
- фоновой постоянно включённый микрофон;
- распознавание пользователя по голосу;
- 3D-персонаж, VRM, Live2D или сложный игровой движок;
- пользовательская настройка персонажа университетом;
- личный кабинет оплаты;
- онлайн-эквайринг, автопродление и возвраты;
- внешняя веб-навигация и поиск в интернете;
- поддержка ролей `SCHEDULE_VIEWER`, `TEACHER` и `STUDENT` в первом пилоте;
- использование существующих tenant-БД Magistr для данных подписки и аналитики ассистента.
## 5. Подтверждённые ограничения текущего Magistr
### 5.1. Frontend
Основная административная панель — Vanilla JavaScript SPA. Вкладка выбирается через hash и `data-tab`, после чего HTML загружается динамически в `#app-content`. Инициализация конкретного экрана завершается только после асинхронного импорта его JS-модуля.
Страница настроек — отдельный SPA со своим набором маршрутов. Переход из основной панели в настройки приводит к загрузке нового документа.
Кабинет кафедры находится внутри основной административной панели. Для роли `DEPARTMENT` целевой режим просмотра расписаний принудительно ограничен собственной кафедрой.
Следствия для ассистента:
- подсветка возможна только после завершения загрузки и инициализации представления;
- при переходе между основной панелью и настройками необходимо безопасно переносить незавершённое действие;
- нельзя рассчитывать на случайные CSS-классы и текст кнопки как на стабильный контракт;
- динамические элементы требуют resolver-функций, а не статического селектора;
- ассистент обязан учитывать роль до перехода, а клиент обязан повторно проверить действие.
### 5.2. Ролевая модель
| Возможность | ADMIN | EDUCATION_OFFICE | DEPARTMENT |
|---|---:|---:|---:|
| Открыть календарный учебный график | Да | Да | Нет |
| Изменить код дня на `*` | Да | Да | Нет |
| Открыть настройки временных слотов | Да | Да | Нет |
| Изменить субботнюю сетку времени | Да | Да | Нет |
| Просматривать расписания | Да | Да | Только своя кафедра |
| Искать конкретную группу прямым фильтром | Да | Да | Нет, режим зафиксирован на кафедре |
| Создать точечный перенос занятия | Да | Да | Нет |
Ролевая недоступность является ожидаемым результатом workflow. Она не должна оформляться как техническая ошибка или компенсироваться попыткой открыть скрытый раздел.
### 5.3. Мультитенантность
Tenant Magistr определяется по домену запроса. Access JWT связан с tenant и не принимается на другом домене. Новый сервис не должен получать основной симметричный `JWT_SECRET` Magistr.
Подписки ассистента хранятся централизованно в собственной БД, где ключом является стабильный `tenantKey`, совпадающий с нормализованным tenant Magistr. Выключение подписки должно блокировать API на сервере, а не только скрывать виджет.
### 5.4. Browser security
Frontend Magistr использует строгую Content Security Policy:
- скрипты и соединения только same-origin;
- inline-скрипты запрещены;
- произвольные inline-стили запрещены;
- `unsafe-eval` отсутствует;
- доступ к микрофону сейчас запрещён через `Permissions-Policy`.
Следствия:
- виджет распространяется как проверенный ESM-артефакт и включается в frontend-сборку Magistr;
- API публикуется через same-origin маршрут `/assistant/*`;
- подсветка реализуется CSS-классом на целевом элементе, а не динамическим inline-стилем;
- текст ответа вставляется через `textContent`, без исполнения HTML;
- для голоса разрешается только `microphone=(self)`;
- `media-src` и `worker-src` расширяются только при доказанной необходимости конкретной реализации;
- сбой ассистента не должен блокировать загрузку Magistr.
## 6. Стартовые пользовательские сценарии
### 6.1. Добавить выходной в четверг текущей недели
Идентификатор намерения: `calendar.set_non_working_day`.
Доступ: `ADMIN`, `EDUCATION_OFFICE`.
Требуемый пользовательский путь:
1. Открыть «Календарный учебный график».
2. Открыть внутреннюю вкладку «Сетки».
3. Выбрать нужный календарный график.
4. Нажать «Загрузить сетку».
5. Найти ячейку четверга нужной недели и курса.
6. Выбрать код `* — Нерабочий праздничный день`.
7. Нажать «Сохранить сетку».
Ассистент не знает, какой учебный график и курс имеет в виду пользователь, поэтому не выбирает их самостоятельно. Он подсвечивает выбор графика и объясняет оставшиеся шаги. Если в будущем появится безопасный read-only контекст выбранного графика, workflow можно сделать точнее.
Поведение для `DEPARTMENT`:
- не выполнять переход;
- ответить, что календарный график изменяют администратор или учебный отдел;
- предложить передать им дату, курс и название группы/графика.
### 6.2. Изменить время пар в субботу
Идентификатор намерения: `time_slots.edit_saturday`.
Доступ: `ADMIN`, `EDUCATION_OFFICE`.
Требуемый пользовательский путь:
1. Открыть «Настройки».
2. Перейти во вкладку «Временные слоты».
3. В поле «Область действия» выбрать системную субботнюю сетку.
4. Найти нужную пару в таблице.
5. Нажать «Изменить».
6. Указать начало и окончание.
7. Нажать «Сохранить слот».
Поведение для `DEPARTMENT` аналогично предыдущему сценарию: объяснить отсутствие прав и указать ответственную роль.
### 6.3. Посмотреть расписание группы ИБ-01б
Идентификатор намерения: `schedule.view_group`.
Вариант для `ADMIN` и `EDUCATION_OFFICE`:
1. Открыть «Просмотр расписаний».
2. В поле «Что смотреть» выбрать «Группа».
3. В поле «Группа» найти `ИБ-01б`.
4. При необходимости выбрать семестр.
5. Нажать «Показать».
Ассистент не должен читать или передавать AI-провайдеру полный список групп. Имя группы извлекается из пользовательской фразы и остаётся частью подсказки. На первом этапе ассистент подсвечивает поле группы, но не меняет его значение.
Вариант для `DEPARTMENT`:
1. Открыть «Просмотр расписаний».
2. Объяснить, что кафедра видит только расписания своей кафедры.
3. Подсветить кнопку «Показать».
4. После загрузки предложить выбрать группу среди найденных результатов.
Если группа не относится к кафедре пользователя, ассистент должен сообщить об ограничении, а не предлагать обход.
### 6.4. Перенести пару с одной даты на другую
Идентификатор намерения: `schedule.move_lesson`.
Доступ: `ADMIN`, `EDUCATION_OFFICE`.
Требуемый пользовательский путь:
1. Открыть «Просмотр расписаний».
2. Найти нужное расписание и период.
3. Нажать «Показать».
4. На карточке конкретной пары нажать «Изменить».
5. Выбрать доступную дату переноса.
6. Выбрать время, действующее для новой даты.
7. При необходимости уточнить преподавателя, аудиторию, формат и комментарий.
8. Проверить блок «Было / Стало».
9. Нажать «Сохранить».
Ассистент не выбирает конкретное занятие сам и не сохраняет перенос. Для `DEPARTMENT` отвечает, что точечные изменения выполняют администратор или учебный отдел.
## 7. Целевая архитектура
```mermaid
flowchart LR
User[Пользователь Magistr]
Host[Frontend Magistr]
Loader[Assistant loader]
Widget[Assistant Web Component]
Bridge[Magistr UI bridge]
Caddy[Caddy / same-origin route]
API[magistr-assistant API]
Entitlement[Entitlement service]
Workflow[Workflow engine]
Provider[Российский AI provider]
DB[(Assistant PostgreSQL)]
OTel[OpenTelemetry Collector]
SigNoz[SigNoz]
User --> Host
Host --> Loader
Loader --> Widget
Widget --> Bridge
Bridge --> Host
Widget -->|/assistant/v1/*| Caddy
Caddy --> API
API --> Entitlement
API --> Workflow
Workflow --> Provider
Entitlement --> DB
API --> DB
API --> OTel
OTel --> SigNoz
```
### 7.1. Границы ответственности
#### Magistr
- аутентифицирует пользователя;
- определяет tenant;
- выдаёт короткоживущий assistant-токен;
- сообщает роль и `departmentId` в ограниченном наборе claims;
- предоставляет host bridge для навигации и подсветки;
- содержит стабильные `data-assistant-target` на элементах;
- не передаёт новому сервису основной JWT-секрет;
- продолжает обеспечивать backend-авторизацию всех бизнес-операций.
#### Новый сервис
- проверяет assistant-токен;
- проверяет подписку tenant и разрешённую роль;
- распознаёт намерение;
- выбирает проверенный workflow;
- формирует двуязычную подсказку;
- возвращает только разрешённые семантические UI-действия;
- выполняет STT и TTS через провайдерские адаптеры;
- учитывает квоты;
- сохраняет только обезличенную статистику и оценку;
- удаляет пользовательскую аналитику старше 30 дней.
#### AI-провайдер
- получает только минимальный текст запроса, разрешённые workflow и локаль;
- не получает JWT, tenant-домен, userId, DOM, бизнес-данные или аудио после завершения STT;
- не является источником прав доступа;
- не генерирует исполняемый код или селекторы.
## 8. Выбор технологического стека
### 8.1. API-сервис
Рекомендуемый стек:
- Python 3.13 с последним patch-релизом как консервативная baseline-версия; переход на 3.14 — после проверки всей матрицы AI/audio-зависимостей;
- FastAPI;
- Pydantic v2 для строгих DTO и JSON Schema;
- Uvicorn как ASGI server;
- SQLAlchemy 2.x;
- Alembic для миграций отдельной БД ассистента;
- PostgreSQL 17 с последним доступным minor-релизом для новой отдельной инсталляции; если инфраструктурно сервис размещается в существующем PostgreSQL 16, использовать последний minor 16 и заранее проверить миграции;
- `httpx` для вызова внешних AI API;
- OpenTelemetry Python SDK;
- `structlog` или стандартный `logging` с JSON formatter;
- `pytest`, `pytest-asyncio`, `respx` и Testcontainers;
- `ruff` для lint/format;
- `mypy` или `pyright` в строгом режиме;
- `uv` и `uv.lock` для воспроизводимых зависимостей.
Причины выбора:
- зрелая экосистема AI, аудио и HTTP-интеграций;
- быстрый выпуск адаптеров к российским API;
- автоматическая OpenAPI-схема для генерации TypeScript-контрактов;
- удобная строгая валидация ответов модели;
- асинхронная обработка внешних API без блокировки worker;
- простое тестирование провайдеров через fake-адаптеры;
- независимость от Java backend Magistr.
В Kubernetes каждый pod запускает один ASGI-процесс. Масштабирование выполняется репликами Deployment, а миграции запускаются отдельным одноразовым Job до rollout. Несколько worker внутри одного контейнера не являются базовой production-схемой.
### 8.2. Виджет
Рекомендуемый стек:
- TypeScript со строгими настройками;
- Web Component с Shadow DOM;
- без React, Vue и других runtime-фреймворков;
- ESM-сборка через esbuild;
- CSS отдельным same-origin файлом;
- Web Audio API и MediaRecorder только для явной голосовой сессии;
- Vitest или `node:test` для unit-тестов;
- Playwright для браузерной интеграции;
- OpenAPI-generated типы для API;
- exact dependency versions и lock-файл.
Причины выбора:
- Magistr уже использует ES modules и Vanilla JavaScript;
- Web Component изолирует стили персонажа от страниц;
- небольшой размер bundle;
- отсутствие второго frontend-фреймворка в Magistr;
- возможность опубликовать виджет отдельным версионированным пакетом;
- Shadow DOM не мешает host bridge подсвечивать элементы основного документа.
### 8.3. Почему не следует делать fork AIRI
AIRI решает значительно более широкую задачу: самостоятельный виртуальный персонаж, локальные модели, память, игры, Live2D/VRM и множество runtime-окружений. Для Magistr полезны только идеи разделения UI, диалога, STT и TTS. Fork создаст большой объём лишних зависимостей и усложнит безопасность, CSP, обновления и поддержку.
## 9. Предлагаемая структура отдельного репозитория
```text
magistr-assistant/
├── AGENTS.md
├── README.md
├── pyproject.toml
├── uv.lock
├── package.json
├── package-lock.json
├── Makefile
├── compose.yaml
├── .env.example
├── .gitea/
│ └── workflows/
│ ├── checks.yaml
│ ├── build-image.yaml
│ ├── publish-widget.yaml
│ └── deploy.yaml
├── service/
│ └── assistant_api/
│ ├── main.py
│ ├── api/
│ │ ├── bootstrap.py
│ │ ├── chat.py
│ │ ├── voice.py
│ │ ├── feedback.py
│ │ ├── operator.py
│ │ └── health.py
│ ├── auth/
│ │ ├── assistant_token.py
│ │ └── operator_auth.py
│ ├── domain/
│ │ ├── entitlement.py
│ │ ├── interaction.py
│ │ ├── workflow.py
│ │ └── ui_action.py
│ ├── orchestration/
│ │ ├── intent_resolver.py
│ │ ├── workflow_engine.py
│ │ ├── response_validator.py
│ │ └── language.py
│ ├── providers/
│ │ ├── base.py
│ │ ├── fake.py
│ │ ├── llm.py
│ │ ├── stt.py
│ │ └── tts.py
│ ├── persistence/
│ │ ├── models.py
│ │ ├── repositories.py
│ │ └── retention.py
│ ├── observability/
│ │ ├── logging.py
│ │ ├── metrics.py
│ │ └── tracing.py
│ └── settings.py
├── migrations/
│ ├── env.py
│ └── versions/
├── widget/
│ ├── src/
│ │ ├── assistant-element.ts
│ │ ├── api-client.ts
│ │ ├── conversation-store.ts
│ │ ├── hold-to-talk.ts
│ │ ├── host-bridge.ts
│ │ ├── i18n.ts
│ │ ├── pending-action.ts
│ │ ├── renderer.ts
│ │ └── styles.css
│ ├── assets/
│ │ └── placeholder-character/
│ ├── tests/
│ ├── tsconfig.json
│ └── esbuild.mjs
├── contracts/
│ ├── site-map.schema.json
│ ├── workflow.schema.json
│ ├── ui-action.schema.json
│ ├── assistant-response.schema.json
│ └── generated/
├── content/
│ ├── site-map/
│ │ └── magistr.v1.yaml
│ └── workflows/
│ ├── calendar.set_non_working_day.yaml
│ ├── time_slots.edit_saturday.yaml
│ ├── schedule.view_group.yaml
│ └── schedule.move_lesson.yaml
├── deploy/
│ ├── docker/
│ └── kubernetes/
├── tests/
│ ├── contract/
│ ├── integration/
│ ├── retention/
│ ├── security/
│ └── provider/
└── docs/
├── ARCHITECTURE.md
├── API.md
├── CONTENT_AUTHORING.md
├── DEPLOYMENT.md
├── PRIVACY.md
├── RUNBOOK.md
└── THREAT_MODEL.md
```
`deploy/kubernetes` может содержать шаблоны нового проекта, но production source of truth должен быть согласован с текущим внешним инфраструктурным контуром. Kubernetes-файлы ассистента не следует добавлять в репозиторий Magistr.
## 10. Модель интеграции релизов
Новый репозиторий публикует два независимых артефакта:
1. Docker image API-сервиса.
2. Версионированный ESM-пакет виджета.
API развивается независимо при соблюдении обратной совместимости `/v1`. Magistr фиксирует точную версию widget-пакета и обновляет её отдельным pull request. Это исключает незаметную подмену исполняемого кода на странице без пересборки Magistr.
Правила совместимости:
- semantic versioning для API и widget;
- `apiVersion` в bootstrap-ответе;
- `siteMapVersion` в widget, API и контексте страницы;
- сервис поддерживает текущую и предыдущую minor-версию site map;
- несовместимая версия приводит к безопасному текстовому ответу без UI-действий;
- удаление action или target выполняется только в major-версии контракта;
- widget никогда не загружает исполняемый JavaScript с AI-провайдера.
## 11. Контракт навигации и подсветки
### 11.1. Site map
Ассистент не должен угадывать устройство интерфейса по DOM. В отдельном репозитории хранится версионированная карта разрешённых маршрутов и целей. В Magistr ей соответствуют стабильные атрибуты `data-assistant-target` и функции host bridge.
Пример элемента карты:
```yaml
siteMapVersion: 1.0.0
application: admin
routes:
- id: academic-calendar.grid
document: admin
tab: academic-calendar
roles: [ADMIN, EDUCATION_OFFICE]
readyEvent: magistr:view-mounted
targets:
- id: academic-calendar.inner-tab.grid
kind: static
- id: academic-calendar.graph-select
kind: static
- id: academic-calendar.load-grid
kind: static
- id: academic-calendar.day-cell
kind: resolver
acceptedParameters: [isoDate, course]
```
Site map содержит только интерфейсный контракт: маршруты, допустимые роли, цели, типы параметров и локализованные названия. В нём нет токенов, tenant-настроек, персональных данных и бизнес-содержимого страницы.
### 11.2. Стабильные цели первого релиза
При реализации в Magistr необходимо добавить стабильные цели минимум для следующих элементов:
| Target ID | Назначение |
|---|---|
| `navigation.academic-calendar` | Переход к календарному учебному графику |
| `academic-calendar.inner-tab.grid` | Внутренняя вкладка «Сетки» |
| `academic-calendar.graph-select` | Выбор календарного графика |
| `academic-calendar.load-grid` | Загрузка сетки |
| `academic-calendar.day-cell` | Ячейка конкретной даты и курса через resolver |
| `academic-calendar.activity-select` | Выбор кода дня |
| `academic-calendar.save-grid` | Кнопка сохранения сетки |
| `navigation.settings` | Переход в отдельный SPA настроек |
| `settings.time-slots` | Вкладка временных слотов |
| `time-slots.scope-select` | Область действия сетки |
| `time-slots.slot-row` | Строка конкретной пары через resolver |
| `time-slots.edit-slot` | Кнопка изменения слота |
| `time-slots.save-slot` | Сохранение слота |
| `navigation.schedule-view` | Просмотр расписаний |
| `schedule-view.target-select` | Поле «Что смотреть» |
| `schedule-view.group-select` | Выбор группы |
| `schedule-view.semester-select` | Выбор семестра |
| `schedule-view.load` | Кнопка «Показать» |
| `schedule-view.lesson-card` | Карточка занятия через resolver |
| `schedule-view.edit-override` | Кнопка изменения занятия |
| `schedule-override.date` | Дата переноса |
| `schedule-override.time-slot` | Время занятия |
| `schedule-override.save` | Сохранение точечного изменения |
Target ID является стабильным публичным контрактом. Он не должен совпадать с CSS-селектором и не должен раскрывать внутренние числовые идентификаторы. Если нужен динамический элемент, host bridge получает строго типизированные параметры и сам находит его в текущем представлении.
### 11.3. Разрешённые UI-действия
API может вернуть только следующие семантические действия:
| Действие | Параметры | Эффект |
|---|---|---|
| `navigate` | `routeId` | Открыть разрешённый маршрут |
| `highlight` | `targetId`, необязательные типизированные параметры | Прокрутить к элементу и подсветить его |
| `showWorkflow` | `workflowId`, `currentStep` | Показать локализованные шаги в виджете |
| `clearHighlight` | нет | Снять предыдущую подсветку |
| `announce` | локализованный ключ | Озвучить короткое состояние через ARIA live region |
Запрещены действия `click`, `type`, `submit`, `eval`, произвольный URL, произвольный CSS-селектор и произвольный JavaScript. Параметры, не предусмотренные JSON Schema, отклоняются.
Пример безопасного ответа:
```json
{
"requestId": "019...",
"locale": "ru",
"answer": "Открою просмотр расписаний и покажу поле выбора группы.",
"workflow": {
"id": "schedule.view_group",
"version": "1.0.0",
"currentStep": 2
},
"actions": [
{"type": "navigate", "routeId": "schedule-view"},
{"type": "highlight", "targetId": "schedule-view.group-select"}
],
"feedbackToken": "opaque-short-lived-token"
}
```
### 11.4. Выполнение действия на клиенте
Host bridge выполняет последовательность:
1. Проверяет JSON Schema, версию контракта, текущую роль и allowlist маршрута.
2. Выполняет разрешённую навигацию штатным роутером Magistr.
3. Ждёт событие `magistr:view-mounted` с ожидаемым `routeId` и `siteMapVersion`.
4. Находит target через зарегистрированный resolver.
5. Прокручивает элемент через `scrollIntoView` с учётом `prefers-reduced-motion`.
6. Добавляет CSS-класс подсветки и доступное пояснение.
7. Снимает подсветку по `Escape`, клику пользователя, смене маршрута или таймауту.
Если цель не найдена за настраиваемый таймаут, клиент показывает шаги текстом, регистрирует обезличенное событие `target_not_found` и не пытается найти похожий элемент эвристикой.
Переход между основной панелью и отдельным SPA настроек выполняется через `sessionStorage`. Разрешено сохранить только `actionId`, `routeId`, `targetId`, типизированные несекретные параметры, версию и срок жизни не более двух минут. Свободный текст запроса, токены и пользовательские данные там не сохраняются.
## 12. Формат проверенных workflow
Каждый поддерживаемый сценарий хранится в Git и проходит code review. Модель не придумывает инструкцию с нуля, а выбирает и адаптирует утверждённый workflow.
```yaml
id: time_slots.edit_saturday
version: 1.0.0
status: published
roles: [ADMIN, EDUCATION_OFFICE]
locales:
ru:
title: Изменить время пар в субботу
examples:
- Где менять время пар в субботу?
- Как изменить субботние звонки?
denied: Изменять временные слоты могут администратор и учебный отдел.
en:
title: Change Saturday lesson times
examples:
- Where can I change Saturday lesson times?
steps:
- id: open-settings
action: {type: navigate, routeId: settings.time-slots}
- id: choose-scope
action: {type: highlight, targetId: time-slots.scope-select}
requiresUserChoice: true
- id: choose-slot
action: {type: highlight, targetId: time-slots.slot-row}
requiresUserChoice: true
- id: save
action: {type: highlight, targetId: time-slots.save-slot}
irreversible: false
```
Обязательные поля workflow:
- уникальный стабильный `id` и semver-версия;
- разрешённые роли;
- русские и английские примеры намерения;
- локализованный отказ по правам;
- последовательность шагов;
- явные точки, где требуется выбор пользователя;
- допустимые параметры и их типы;
- требуемая версия site map;
- дата проверки и ответственный редактор;
- тестовые фразы и ожидаемые действия;
- признак снятия с публикации без физического удаления старой версии.
Контент публикуется вместе с API image или как подписанный неизменяемый bundle. Редактирование workflow через production-БД в первом релизе не требуется: Git обеспечивает review, историю и откат.
## 13. Обработка текстового запроса и роль AI
### 13.1. Конвейер
1. Проверить assistant-токен и активную подписку tenant.
2. Нормализовать локаль, длину и Unicode запроса.
3. Удалить очевидные секреты по маскам только как дополнительную защиту; не обещать абсолютную деидентификацию произвольного текста.
4. Сопоставить запрос с проверенными примерами детерминированным поиском.
5. Если совпадение уверенное, выбрать workflow без вызова LLM.
6. Если запрос неоднозначен, отправить провайдеру минимальный текст и каталог разрешённых намерений для роли.
7. Принять от провайдера только структурированный результат по JSON Schema: `intentId`, извлечённые безопасные параметры, локаль и confidence.
8. Повторно проверить роль, workflow, параметры и site map на своей стороне.
9. Сформировать ответ из утверждённых локализованных шаблонов.
10. Вернуть действия клиенту и записать обезличенный outcome.
LLM используется как классификатор и языковой слой, но не как источник прав, маршрутов или произвольных действий. Низкая уверенность приводит к уточняющему вопросу без навигации. Если вопрос не относится к поддерживаемым возможностям, ассистент честно сообщает границы первого релиза.
### 13.2. Пороговые решения
Пороги задаются конфигурацией и калибруются на тестовом наборе:
- высокая уверенность: вернуть workflow;
- средняя уверенность: попросить выбрать из двух наиболее близких сценариев;
- низкая уверенность: ответить, что сценарий пока не поддерживается;
- конфликт роли и workflow: вернуть ролевой отказ независимо от confidence модели;
- конфликт версии: только текстовая инструкция без UI-действия.
Нельзя использовать ответ провайдера, если в нём неизвестный `intentId`, target, лишнее поле, неправильный тип или параметр вне allowlist.
### 13.3. Адаптеры провайдеров
Для LLM, STT и TTS определяются отдельные интерфейсы. Один поставщик может реализовать все три функции, но код не должен это предполагать. Адаптер обязан поддерживать:
- общий нормализованный DTO;
- таймауты подключения и ответа;
- повтор только безопасных идемпотентных запросов;
- circuit breaker;
- ограничение конкурентных запросов;
- унифицированные коды ошибок;
- учёт длительности, токенов/символов и стоимости без текста;
- fake-реализацию для локальной разработки и CI.
Автоматический failover между разными AI-провайдерами не включается до юридической и качественной проверки: он способен незаметно изменить место обработки данных и поведение модели.
## 14. Голосовой сценарий push-to-talk
### 14.1. Состояния интерфейса
```text
idle -> requesting_permission -> recording -> uploading -> transcribing
-> thinking -> answer_ready -> optional_speaking -> idle
```
Из любого промежуточного состояния возможен переход в `cancelled` или `error`, после которого пользователь может повторить попытку текстом.
### 14.2. Правила записи
- запись начинается только при `pointerdown` или доступном клавиатурном эквиваленте на кнопке;
- запись завершается при `pointerup`, `pointercancel`, потере capture, отпускании клавиши, скрытии вкладки или достижении лимита;
- рекомендуемый начальный лимит — 30 секунд и 10 МБ до серверной проверки;
- короткая запись ниже технического порога не отправляется;
- пользователь видит индикатор записи и длительность;
- повторное нажатие во время отправки не запускает параллельную запись;
- исходный аудиобуфер удаляется из памяти клиента после завершения или ошибки;
- сервер не пишет аудио во временный постоянный файл без необходимости; если конвертация требует файла, используется изолированный temp-каталог с гарантированным удалением;
- аудио никогда не попадает в логи, трассировки, аналитику или резервные копии.
На сервере проверяются реальный контейнер/кодек, длительность, размер и частота. При необходимости отдельный sandboxed-процесс приводит запись к поддерживаемому моноформату с жёсткими лимитами CPU, памяти и времени.
### 14.3. Распознавание и озвучивание
STT возвращает транскрипт только в память текущего запроса. После классификации он удаляется. Пользователь видит распознанный текст и может отменить запрос до навигации.
TTS по умолчанию озвучивает короткий ответ, только если пользователь включил голосовой ответ в текущей сессии. Для длинной пошаговой инструкции озвучивается резюме, полный текст остаётся на экране. Полученное аудио не кешируется межпользовательски, если лицензионные условия провайдера и модель угроз не разрешают безопасный кеш.
Если браузер, политика безопасности или провайдер не поддерживает голос, текстовый режим остаётся полностью работоспособным.
## 15. Локализация
- локаль по умолчанию берётся из пользовательского интерфейса Magistr, а не определяется по имени пользователя;
- пользователь может переключить `Русский / English` внутри виджета на время текущей browser-сессии;
- ответ следует языку последнего запроса, если язык определён уверенно;
- UI, workflow, ролевые отказы, ошибки, ARIA-метки и голосовые подсказки существуют на обоих языках;
- отсутствие перевода является ошибкой сборки контента;
- fallback — русский для русской инсталляции, но технический ключ никогда не показывается пользователю;
- названия реальных элементов Magistr берутся из утверждённых переводов site map, а не переводятся моделью на лету;
- тестовый корпус содержит русские, английские и смешанные формулировки, опечатки и варианты транслитерации названий групп.
## 16. Персонаж и UX виджета
До выбора образа используется нейтральный абстрактный placeholder без человекоподобной мимики. Архитектура UI должна позволять заменить набор изображений и токенов темы без изменения диалоговой логики.
Обязательные визуальные состояния:
- ожидание;
- пользователь удерживает кнопку и говорит;
- распознавание речи;
- подготовка ответа;
- показ следующего шага;
- успешное нахождение элемента;
- элемент или функция недоступны;
- техническая ошибка с переходом на текстовый режим.
Виджет располагается поверх интерфейса, но не перекрывает основные кнопки и системные уведомления. На небольшом экране он раскрывается в нижнюю панель. Диалог закрывается с клавиатуры; фокус не теряется; используется ARIA dialog/live region; контраст соответствует WCAG 2.2 AA; анимация отключается при `prefers-reduced-motion`.
Образ не должен обещать полномочия, которых нет у ассистента. Формулировки первого релиза: «покажу», «подскажу», «открою раздел», но не «изменю» и не «сохраню».
## 17. API-контракты
Все публичные эндпоинты versioned, принимают и возвращают JSON, кроме аудио и SSE. Ошибки имеют машинный `code`, локализованный `message` и `requestId`. Внешняя документация не раскрывает operator API.
### 17.1. Обмен токена в Magistr
```http
POST /api/assistant/session
Authorization: Bearer <основной access token Magistr>
Content-Type: application/json
{"locale":"ru","widgetVersion":"1.0.0","siteMapVersion":"1.0.0"}
```
Magistr проверяет обычную аутентификацию и роль, затем подписывает отдельный короткоживущий JWT. Пример ответа:
```json
{
"enabled": true,
"assistantBasePath": "/assistant/v1",
"token": "eyJ...",
"expiresAt": "2026-08-05T12:05:00Z"
}
```
Если подписка отключена, основной backend может вернуть `enabled: false` без токена. Это улучшает UX, но не заменяет обязательную серверную проверку подписки в новом сервисе.
### 17.2. Bootstrap
```http
GET /assistant/v1/bootstrap
Authorization: Bearer <assistant token>
```
Возвращает совместимые версии, доступные функции (`text`, `voiceInput`, `voiceOutput`, `feedback`), лимиты текущей сессии, локали и публичную конфигурацию персонажа. В ответе нет коммерческих сумм и секретной информации о tenant.
### 17.3. Текстовый диалог
```http
POST /assistant/v1/chat/stream
Authorization: Bearer <assistant token>
Content-Type: application/json
Accept: text/event-stream
{
"requestId": "019...",
"locale": "ru",
"message": "Как добавить выходной на этой неделе в четверг?",
"pageContext": {
"application": "admin",
"routeId": "schedule-view",
"siteMapVersion": "1.0.0"
}
}
```
SSE-события ограничены типами `status`, `answer`, `workflow`, `actions`, `done`, `error`. UI-действия отправляются только после полной серверной валидации, одним завершённым событием. Если реальный streaming не поддерживается выбранным провайдером, контракт сохраняется, но сервер отправляет один готовый ответ.
Первая версия не принимает историю целиком. Виджет хранит ограниченный контекст текущей вкладки в памяти, а сервер получает только последние необходимые сообщения с жёстким лимитом и не сохраняет их. Для четырёх стартовых workflow предпочтительно обрабатывать каждый вопрос независимо.
### 17.4. Голос
```http
POST /assistant/v1/voice/transcriptions
Authorization: Bearer <assistant token>
Content-Type: multipart/form-data
```
Ответ содержит транскрипт, определённую локаль и request ID. Транскрипт возвращается пользователю, но не сохраняется сервисом.
```http
POST /assistant/v1/voice/speech
Authorization: Bearer <assistant token>
Content-Type: application/json
{"requestId":"019...","locale":"ru","textKey":"workflow.calendar.summary"}
```
Предпочтителен `textKey` с разрешёнными параметрами, а не произвольный длинный текст. Ответ — поток аудио с правильным `Content-Type`, `Cache-Control: no-store` и ограничением размера.
### 17.5. Оценка
```http
POST /assistant/v1/feedback
Authorization: Bearer <assistant token>
Content-Type: application/json
{"feedbackToken":"opaque...","rating":"positive","reasonCode":"found_target"}
```
`rating`: `positive` или `negative`. Свободный комментарий в первом релизе отсутствует, чтобы случайно не сохранять текст диалога или персональные данные. Допустимые причины выбираются из локализованного списка.
### 17.6. Operator API
Минимальный закрытый набор:
- `PUT /internal/v1/tenants/{tenantKey}/entitlement` — включить или выключить доступ, задать дату окончания и квоты;
- `GET /internal/v1/tenants/{tenantKey}/entitlement` — проверить состояние;
- `GET /internal/v1/tenants/{tenantKey}/usage` — получить агрегаты без текстов;
- `POST /internal/v1/tenants/{tenantKey}/rotate-feedback-key` — аварийная ротация;
- `POST /internal/v1/retention/run` — только для диагностического ручного запуска защищённой job.
На первом этапе вместо отдельного operator UI допустима CLI-команда, вызывающая тот же application service. Ручное изменение строк SQL считается аварийной операцией, а не штатным управлением.
### 17.7. Служебные эндпоинты
- `GET /health/live` — процесс работает, без проверки внешнего AI;
- `GET /health/ready` — доступна БД, загружены workflow и ключи проверки JWT;
- `GET /metrics` — только во внутренней сети;
- OpenAPI в production закрыта или доступна только оператору.
Недоступность внешнего AI-провайдера не должна постоянно выводить pod из readiness и провоцировать рестарт. Она отражается отдельной метрикой, circuit breaker и понятной ошибкой пользователю.
## 18. Аутентификация и границы доверия
Assistant JWT подписывается асимметричным ключом Magistr. Новый сервис получает только публичный ключ или JWKS.
Рекомендуемые claims:
```json
{
"iss": "magistr",
"aud": "magistr-assistant",
"sub": "opaque-session-subject",
"tenant": "university-key",
"role": "EDUCATION_OFFICE",
"departmentId": null,
"locale": "ru",
"jti": "019...",
"iat": 1785931200,
"exp": 1785931500
}
```
Правила:
- срок жизни — около пяти минут;
- токен не продлевается сервисом ассистента; новый получает только Magistr;
- `aud`, `iss`, подпись, `exp`, допустимая роль и tenant обязательны;
- email, ФИО, логин и основной access token не передаются;
- `departmentId` нужен только для ролевого варианта кафедры;
- публичные ключи ротируются с перекрывающим периодом по `kid`;
- токен хранится только в памяти виджета, не в `localStorage`;
- ответы API имеют `Cache-Control: no-store`;
- CORS не открывается глобально, основной путь same-origin;
- rate limit применяется по tenant, сессии и типу операции;
- operator-доступ использует отдельную аутентификацию, сетевое ограничение и аудит.
Новый сервис принимает решение о доступности подсказки, но не заменяет авторизацию Magistr. Даже будущие исполняемые действия должны повторно проверяться backend Magistr.
## 19. Модель данных отдельной БД
### 19.1. `tenant_entitlements`
| Поле | Назначение |
|---|---|
| `tenant_key` | Первичный ключ, нормализованный tenant |
| `status` | `enabled`, `suspended`, `expired` |
| `contract_reference` | Внутренняя ссылка на договор, без текста договора |
| `starts_at`, `ends_at` | Период доступа |
| `features` | Строго валидируемые флаги text/STT/TTS |
| `quota_policy` | Ссылка на версионированную политику квот |
| `created_at`, `updated_at` | Служебные даты |
Эта коммерческая конфигурация не является пользовательской аналитикой и хранится до окончания договорных и бухгалтерских обязательств. Она не удаляется 30-дневной job.
### 19.2. `usage_events`
Разрешённые поля:
- UUID события и время;
- tenant key;
- роль из ограниченного enum;
- тип канала: text/STT/TTS;
- `intentId` или `unsupported`, но не текст запроса;
- версия workflow, site map, widget и API;
- outcome: `success`, `clarification`, `denied`, `target_not_found`, `provider_error`;
- нормализованная латентность;
- количество входных/выходных токенов, символов или секунд аудио;
- код провайдера из внутреннего enum;
- технический request ID;
- `expires_at = created_at + 30 days`.
Запрещённые поля: prompt, транскрипт, ответ, аудио, имя группы, дата занятия, ФИО, email, логин, основной user ID, IP, User-Agent, DOM и URL с query-параметрами.
### 19.3. `feedback_events`
Содержит tenant, `intentId`, workflow version, `positive/negative`, выбранный reason code, channel, created/expiry timestamps и одноразовый псевдослучайный идентификатор ответа. Свободного комментария нет. Нельзя восстановить текст вопроса по feedback token.
### 19.4. `operator_audit_events`
Фиксирует включение/выключение подписки, изменение срока и квот: оператор, действие, tenant, время, old/new state без секретов. Для аудита управления срок хранения задаётся отдельно согласно договорным требованиям; 30-дневное удаление пользовательской аналитики на эту таблицу не распространяется. Это исключение должно быть явно отражено в политике конфиденциальности.
### 19.5. Агрегаты и квоты
Для быстрых лимитов допустима таблица дневных/месячных счётчиков без пользователя. Агрегаты после удаления событий можно хранить дольше только если они необратимо укрупнены минимум до tenant + месяц + тип операции и не содержат request ID, роли малой группы или редких `intentId`. До юридического решения безопасный вариант — удалять и события, и производные пользовательские агрегаты через 30 дней.
Векторная БД в MVP не нужна: поддерживаемых workflow мало, их версии лежат в Git, а детерминированный поиск проще проверять.
### 19.6. Retention job
- ежедневный Kubernetes CronJob или внутренний scheduled worker с distributed lock;
- удаление пакетами по индексированному `expires_at`, чтобы не блокировать таблицу;
- отдельные индексы на `expires_at`, `(tenant_key, created_at)` и поля агрегирования;
- метрики времени последнего успешного запуска, количества удалённых строк и oldest remaining event;
- alert, если старейшая запись превысила 31 день;
- интеграционный тест с искусственно устаревшими строками;
- резервные копии пользовательской аналитики имеют согласованный срок жизни и не превращают удаление из основной БД в фикцию;
- восстановление backup сопровождается немедленным повторным retention до открытия трафика.
## 20. Безопасность и приватность
### 20.1. Основные угрозы
| Угроза | Контроль |
|---|---|
| Межтенантный доступ | Tenant только из проверенного JWT; entitlement и квоты запрашиваются с ним; тесты на несовпадающие tenant |
| Повышение роли через prompt | Роль только из JWT; модель не влияет на policy engine |
| Prompt injection | Провайдер выбирает только intent из allowlist; DOM и произвольные инструкции сайта ему не отправляются |
| XSS в ответе | Строгая схема, `textContent`, запрет HTML/Markdown с raw HTML, CSP |
| Произвольное управление UI | Только semantic actions и target allowlist; нет `click`, selector и script |
| Кража токена | Короткий TTL, хранение в памяти, same-origin, `no-store`, строгая CSP |
| Обход отключённой подписки | Проверка entitlement на каждом billable запросе и при bootstrap |
| Перерасход API | Квоты, rate limits, maximum concurrency, circuit breaker и бюджеты |
| Аудиобомба/вредный контейнер | Проверка magic bytes, лимиты размера/длительности, sandboxed transcoding |
| Утечка через логи/трейсы | Централизованный allowlist полей и тест, запрещающий content keys |
| Устаревшая карта UI | Версионирование, contract test и безопасный текстовый fallback |
| Компрометация operator API | Отдельная identity, private network, least privilege, audit, ротация ключей |
| Сбой AI-провайдера | Таймаут, circuit breaker, текстовая ошибка, Magistr продолжает работать |
### 20.2. Минимизация данных
Перед подключением реального провайдера документируются регион обработки, срок хранения входных данных провайдером, использование запросов для обучения, порядок удаления и перечень субподрядчиков. В настройках провайдера выбирается режим без обучения и с минимальным техническим хранением, если он доступен.
Пользователю перед первой голосовой записью кратко сообщается, что звук отправляется внешнему сервису для распознавания и не сохраняется Magistr Assistant. Разрешение браузера запрашивается только после нажатия на кнопку микрофона.
Нельзя обещать полную анонимность текста, который пользователь сам ввёл: он может написать ФИО или другую персональную информацию. Поэтому интерфейс предупреждает не вводить персональные данные, сервис не хранит текст, а провайдер выбирается с подходящими договорными условиями.
### 20.3. Секреты и сеть
- ключи AI API, приватный ключ operator auth и DSN БД хранятся в Kubernetes Secret или внешнем secret store;
- секреты не попадают в image, репозиторий, frontend bootstrap и логи;
- egress разрешён только к БД, telemetry collector и выбранным provider endpoints;
- ingress к публичному API идёт только через reverse proxy; operator и metrics routes закрыты NetworkPolicy;
- TLS используется для внешних вызовов и соединения с БД в production;
- зависимости и container image регулярно сканируются;
- административная ротация секретов описывается в runbook и проверяется до пилота.
## 21. Подписка, квоты и управление оператором
### 21.1. Жизненный цикл подписки
1. Оператор получает подтверждение договора вне системы.
2. Проверяет точный `tenantKey` университета.
3. Через защищённую CLI или operator API задаёт период, функции и квоты.
4. Система показывает оператору итоговое состояние до применения.
5. После подтверждения создаётся audit event.
6. Widget становится доступен при следующем bootstrap без релиза Magistr.
7. При `suspended` или истечении срока новые запросы блокируются сервером; Magistr продолжает работать.
8. Возобновление подписки не восстанавливает удалённую аналитику.
Статусы должны иметь точные причины: договор завершён, ручная приостановка, квота исчерпана, конфигурация неполна. Конечному пользователю не показываются сведения о договоре; он получает нейтральное сообщение и предложение обратиться к ответственному лицу университета.
### 21.2. Квоты
Квоты конфигурируются отдельно для:
- текстовых запросов;
- секунд STT;
- символов или секунд TTS;
- одновременных запросов tenant;
- burst rate на одну browser-сессию.
Числовые значения нельзя фиксировать до получения тарифов и лимитов выбранных российских API. До пилота необходимо посчитать стоимость на тестовом корпусе и задать мягкий warning-порог и жёсткий hard limit. Проверка квоты и резервирование использования должны быть атомарными. При ошибке внешнего провайдера списывается только фактически тарифицированное использование, насколько это позволяет его API.
### 21.3. Operator CLI первого релиза
Предлагаемые команды:
```text
assistantctl tenant show <tenant-key>
assistantctl tenant enable <tenant-key> --until YYYY-MM-DD --policy pilot-v1
assistantctl tenant suspend <tenant-key> --reason contract-ended
assistantctl tenant set-features <tenant-key> --text on --stt on --tts on
assistantctl tenant usage <tenant-key> --period 30d
assistantctl retention status
```
CLI обязана иметь `--dry-run`, подтверждение потенциально блокирующих изменений и машиночитаемый JSON-режим. Секрет оператора не передаётся аргументом командной строки.
## 22. Наблюдаемость и эксплуатационные цели
### 22.1. Логи
Production-логи структурированы, а человекочитаемые сообщения написаны по-русски. Разрешённые поля: timestamp, level, service/version, environment, requestId, traceId, tenant key, роль-enum, endpoint, intent ID, outcome, latency bucket, provider code и error code.
Запрещённые поля централизованно фильтруются: `message`, `prompt`, `transcript`, `answer`, `audio`, Authorization, cookie, JWT, ФИО, email, IP, User-Agent и multipart body. Логирование request/response body в reverse proxy и APM выключается для `/assistant/*`.
### 22.2. Метрики
Минимальный набор:
- `assistant_requests_total{channel,outcome,role}`;
- `assistant_request_duration_seconds{channel,stage}`;
- `assistant_provider_requests_total{provider,capability,outcome}`;
- `assistant_provider_duration_seconds{provider,capability}`;
- `assistant_usage_units_total{tenant,capability}` с контролем cardinality;
- `assistant_workflow_matches_total{intent,outcome}`;
- `assistant_ui_actions_total{action,outcome}`;
- `assistant_target_not_found_total{site_map_version,target}`;
- `assistant_feedback_total{rating,reason,intent}`;
- `assistant_entitlement_denied_total{reason}`;
- `assistant_retention_last_success_timestamp`;
- `assistant_retention_oldest_event_age_seconds`;
- состояние circuit breaker и очереди запросов.
Для большого числа tenant допускается убирать tenant label из Prometheus и строить коммерческие отчёты запросом к БД, чтобы не создавать высокую cardinality.
### 22.3. Трассировки
Span-цепочка: reverse proxy → auth/entitlement → intent resolution → provider → response validation. Текстовые и аудиоданные никогда не записываются в span attributes или events. Трассировка интегрируется с существующим OpenTelemetry/SigNoz контуром, но сервис имеет собственное имя и dashboards.
### 22.4. Начальные SLO для пилота
| Показатель | Цель |
|---|---:|
| Доступность API подсказок | 99,5% за месяц, исключая согласованные работы |
| Bootstrap p95 внутри инфраструктуры | до 300 мс |
| Готовый текстовый ответ p95 | до 8 с |
| STT + ответ без TTS p95 для записи до 15 с | до 15 с |
| Выполнение локальной навигации и подсветки p95 после ответа | до 1 с после готовности view |
| Успешное нахождение target на поддерживаемой версии UI | не менее 98% |
| Пользовательская аналитика старше 30 дней | 0 строк, допустимое окно job до 24 часов |
| Влияние аварии ассистента на Magistr | 0 отказов основных функций |
Это стартовые ориентиры, а не договорной SLA. После пилота они уточняются по фактическим latency российских провайдеров и инфраструктуры.
### 22.5. Оповещения и runbook
Alerts:
- резкий рост 5xx или provider errors;
- p95 выше цели в течение согласованного окна;
- circuit breaker открыт длительное время;
- retention не выполнялся более 25 часов;
- есть запись старше 31 дня;
- истекает ключ JWT/JWKS или provider secret;
- квота tenant близка к hard limit;
- несовместимость site map/widget;
- массовый `target_not_found` после релиза Magistr.
Runbook должен описывать диагностику, безопасное отключение только ассистента, переключение на текстовый режим, ротацию секретов, повтор retention, откат image и widget, а также правила обращения с потенциальной утечкой.
## 23. Развёртывание и CI/CD отдельного проекта
### 23.1. Контейнер
- multi-stage build;
- непривилегированный пользователь и read-only root filesystem;
- минимальный базовый образ с фиксированным digest;
- `no-new-privileges`, drop Linux capabilities;
- отдельный writable temp volume только если нужен аудиоконвертер;
- health endpoints без секретов;
- graceful shutdown длиннее максимального обычного запроса;
- SBOM и vulnerability scan;
- OCI labels с git SHA и версией;
- image публикуется по immutable digest.
### 23.2. Kubernetes
Production-набор:
- Deployment API минимум с двумя репликами для пилотной production-среды;
- Service;
- Ingress/reverse-proxy route `/assistant/*` на доменах Magistr;
- одноразовый migration Job перед rollout;
- CronJob retention с distributed lock;
- ConfigMap только для несекретных параметров;
- Secret/external secret;
- NetworkPolicy ingress/egress;
- PodDisruptionBudget;
- resource requests/limits;
- topology spread или anti-affinity при наличии нескольких узлов;
- HPA только после появления достоверных метрик CPU/concurrency;
- ServiceMonitor/OTel configuration;
- отдельный service account без доступа к Kubernetes API, если он не нужен.
Внешний инфраструктурный репозиторий остаётся source of truth для production-манифестов. Изменение текущего `../k8s` выполняется отдельной задачей и не является частью создания этого документа.
### 23.3. Reverse proxy и CSP
Рекомендуемый маршрут same-origin:
```text
https://<tenant-domain>/assistant/v1/* -> magistr-assistant-service
```
Путь обмена токена остаётся в Magistr: `/api/assistant/session`. Виджет и CSS отдаются Magistr как локальные versioned assets. Для микрофона политика меняется с `microphone=()` на `microphone=(self)`. Новые внешние домены в browser CSP не добавляются, потому что виджет обращается только к same-origin proxy.
### 23.4. Gitea Actions
Pipeline для pull request:
1. Проверка format/lint Python и TypeScript.
2. Строгая проверка типов.
3. Unit-тесты.
4. Валидация JSON Schema и всех YAML workflow.
5. Проверка полноты русских/английских переводов.
6. Contract-тест с зафиксированной версией Magistr bridge.
7. Интеграционные тесты PostgreSQL и fake providers.
8. Playwright для виджета.
9. Security tests, dependency audit, secret scan.
10. Сборка API image и widget без публикации.
Pipeline `main` дополнительно публикует артефакты, SBOM и immutable digest. Production deploy запускается только после успешных проверок, миграции и принятой стратегии окружений. `workflow_dispatch` остаётся дополнительным способом повторить безопасный deploy, а не единственным способом доставки.
### 23.5. Локальная разработка
`compose.yaml` нового репозитория поднимает API, PostgreSQL, fake LLM/STT/TTS и при необходимости OTel collector. Magistr подключается через локальный proxy или dev override. По умолчанию никакой запрос не уходит во внешний AI API; реальный provider включается явной переменной и отдельным профилем.
## 24. Стратегия тестирования
### 24.1. Unit-тесты
- role policy для каждого workflow;
- confidence thresholds;
- валидация и отклонение неизвестных actions/targets;
- вычисление квот;
- entitlement status и даты;
- локализация и fallback;
- очистка лог-полей;
- TTL pending action;
- state machine push-to-talk;
- provider timeout/retry/circuit breaker;
- формирование `expires_at`.
### 24.2. Contract-тесты
- OpenAPI ↔ generated TypeScript types;
- workflow ↔ workflow schema;
- target из workflow обязательно существует в site map;
- role workflow не шире role target/route;
- Magistr bridge поддерживает все action types;
- widget/API/site map version matrix;
- все опубликованные строки имеют RU и EN;
- удалённый target требует major change или deprecation window.
Contract bundle полезно публиковать отдельным package, который подключают оба репозитория. CI Magistr должен падать, если новый UI удалил требуемую цель без согласованного обновления контракта.
### 24.3. Интеграционные тесты API
- валидный/истёкший/неверно подписанный JWT;
- неверные `iss`, `aud`, tenant и роль;
- включённая, выключенная и истёкшая подписка;
- атомарное исчерпание квоты конкурентными запросами;
- отсутствие текста и аудио в БД, логах и traces;
- ошибки/таймауты/невалидный JSON каждого provider;
- очистка временного аудио после успеха, отмены и исключения;
- retention на границе 30 дней;
- восстановление из backup с повторной очисткой;
- недоступность provider не ломает health/live и Magistr.
### 24.4. Browser E2E
Для каждой из трёх ролей и двух локалей проверяются:
- загрузка/скрытие виджета по entitlement;
- клавиатурное открытие и закрытие;
- переход в SPA и между документами;
- ожидание асинхронной готовности представления;
- подсветка, прокрутка, снятие по Escape и таймауту;
- безопасный fallback, когда target отсутствует;
- отсутствие исполнения HTML в вредоносном ответе;
- запрет action, недоступного роли;
- сохранение основного функционала Magistr при 5xx/timeout API;
- permission denied для микрофона;
- отпускание кнопки за пределами элемента и скрытие вкладки;
- `prefers-reduced-motion`, масштаб 200%, мобильная ширина и screen reader labels.
### 24.5. Приёмочные сценарии MVP
| Фраза | Роль | Ожидаемый результат |
|---|---|---|
| «Как добавить выходной на этой неделе в четверг?» | ADMIN/EDUCATION_OFFICE | Открыта сетка календаря, подсвечен выбор графика, показаны дальнейшие шаги |
| Та же фраза | DEPARTMENT | Понятный ролевой отказ, запрещённый экран не открыт |
| «Где менять время пар в субботу?» | ADMIN/EDUCATION_OFFICE | Открыты временные слоты, подсвечена область действия |
| Та же фраза | DEPARTMENT | Указана ответственная роль |
| «Какое расписание у группы ИБ-01б?» | ADMIN/EDUCATION_OFFICE | Открыт просмотр расписания, подсвечено поле группы |
| Та же фраза | DEPARTMENT | Открыт собственный режим кафедры, объяснено ограничение |
| «Как перенести пару с одной даты на другую?» | ADMIN/EDUCATION_OFFICE | Открыт просмотр, объяснён выбор занятия и override |
| Та же фраза | DEPARTMENT | Указана ответственная роль, редактирование не предлагается |
Для каждой строки создаются английские варианты, голосовая версия, вариант с опечатками и негативный тест, в котором модель пытается вернуть запрещённое действие.
### 24.6. Нагрузочные и отказные тесты
- bootstrap storm после начала рабочего дня;
- параллельные короткие text-запросы;
- максимальные разрешённые аудиозаписи;
- медленный provider и разрыв SSE;
- временная недоступность БД;
- истощение connection pool;
- остановка pod во время запроса;
- одновременный retention и чтение агрегатов;
- поведение при исчерпании tenant quota;
- доказательство, что деградация ассистента не увеличивает latency основных API Magistr.
## 25. Поэтапный план реализации
Оценки ниже даны в человеко-днях для одного разработчика, включают код и основные тесты, но не ожидание договора с AI-провайдером, юридическое согласование, закупку и создание финальной графики. Диапазон нужен для планирования, а не является обещанием срока.
### Этап 0. Проработка и ADR — 3–5 дней
Задачи:
- создать отдельный репозиторий и его `AGENTS.md`;
- утвердить рабочее имя и владельца сервиса;
- сравнить российские LLM/STT/TTS API по SLA, регионам, retention, цене и качеству русского/английского;
- утвердить data flow и договорные ограничения;
- зафиксировать ADR по стеку, same-origin proxy, token exchange, workflow engine и отсутствию server-side history;
- снять базовые замеры четырёх сценариев вручную;
- выбрать начальные SLO и бюджет пилота.
Критерий выхода: выбран provider или согласован fake-first режим, утверждены границы данных и архитектурные решения.
### Этап 1. Контракты интерфейса Magistr — 5–8 дней
Задачи:
- оформить JSON Schema actions/site map/workflow;
- внедрить в Magistr host bridge и `data-assistant-target` только для четырёх сценариев;
- добавить событие готовности асинхронных views;
- реализовать безопасную навигацию и pending action между SPA;
- добавить CSS подсветки и accessibility;
- создать fake widget для проверки без AI;
- добавить contract и browser tests для ролей.
Критерий выхода: статический тестовый ответ безопасно открывает и подсвечивает каждый разрешённый target, а запрещённые actions отклоняются.
### Этап 2. Основа сервиса, auth и entitlement — 8–12 дней
Задачи:
- создать FastAPI skeleton, конфигурацию и русские логи;
- PostgreSQL/Alembic schema;
- assistant token endpoint в Magistr и JWKS/rotation contract;
- проверка JWT и tenant entitlement;
- operator CLI с audit;
- квоты и rate limits;
- bootstrap, health, metrics;
- retention job и тесты;
- fake provider.
Критерий выхода: включённый tenant получает bootstrap, выключенный блокируется сервером, межтенантные тесты и retention проходят.
### Этап 3. Виджет и текстовый UX — 10–15 дней
Задачи:
- Web Component, адаптивная панель и placeholder персонажа;
- текстовый ввод, ответы, шаги и оценки;
- browser-only conversation state;
- SSE client, timeout/cancel/retry;
- bridge actions и безопасный fallback;
- RU/EN UI;
- CSP integration;
- unit и Playwright tests.
Критерий выхода: все четыре workflow полностью проходят с fake provider текстом на двух языках.
### Этап 4. AI-классификация и контент — 8–12 дней
Задачи:
- детерминированный resolver;
- адаптер выбранного LLM;
- structured output и строгая повторная валидация;
- confidence/clarification policy;
- все четыре versioned workflow;
- тестовый корпус фраз и regression evaluation;
- бюджеты, timeout и circuit breaker;
- защита логов и аналитика outcome.
Критерий выхода: целевые метрики качества достигнуты на зафиксированном тестовом корпусе, запрещённые действия не проходят validator.
### Этап 5. Голос и завершение локализации — 8–12 дней
Задачи:
- hold-to-talk state machine;
- браузерные разрешения и CSP/Permissions-Policy;
- STT adapter, проверка и ограничение аудио;
- просмотр транскрипта и отмена;
- TTS adapter и управление воспроизведением;
- двуязычные голоса и fallback;
- accessibility и мобильные сценарии;
- тесты удаления буферов и временных файлов.
Критерий выхода: голосовой вопрос можно безопасно задать удержанием кнопки, а отказ любого voice-компонента оставляет рабочий текстовый режим.
### Этап 6. Production hardening — 8–12 дней
Задачи:
- threat-model review;
- dependency/container scan и SBOM;
- Kubernetes policies/resources/migration/retention jobs;
- dashboards, alerts и runbooks;
- нагрузочные и failure tests;
- backup/restore/retention rehearsal;
- секреты и ротация;
- проверка отсутствия content в logs/traces/DB;
- rollback rehearsal API и widget.
Критерий выхода: закрыты критические угрозы, проверены аварийные процедуры, сервис можно отключить без влияния на Magistr.
### Этап 7. Пилот одного университета — 5–10 дней активной работы
Задачи:
- включить подписку одному tenant;
- провести короткое обучение ответственных пользователей;
- следить за latency, target failures, квотами и оценками;
- вручную разобрать негативные оценки только по обезличенным кодам и воспроизвести сценарии на тестовом стенде;
- исправить контент/target contracts;
- получить решение о расширении.
Критерий выхода: все четыре сценария устойчивы, retention подтверждён, нет влияния на основные функции, согласованы следующие workflow.
### Итоговая ориентировочная трудоёмкость
Полный путь до завершённого пилота: примерно **55–86 человеко-дней**. При одном разработчике это обычно 3–5 календарных месяцев с учётом согласований и исправлений; при параллельной работе backend/frontend/infra календарный срок можно уменьшить, но контракты этапа 1 и privacy-решения этапа 0 остаются последовательными зависимостями.
## 26. Декомпозиция начального бэклога
### Контракты и Magistr
- `ASST-001` — ADR архитектуры и границ данных.
- `ASST-002` — schema semantic UI actions.
- `ASST-003` — schema site map и workflow.
- `ASST-004` — inventory target для четырёх сценариев.
- `ASST-005` — stable `data-assistant-target` в основной admin SPA.
- `ASST-006` — targets в settings SPA.
- `ASST-007` — host bridge role/route allowlist.
- `ASST-008` — `magistr:view-mounted` и ожидание view.
- `ASST-009` — pending action между документами.
- `ASST-010` — CSS highlight и accessibility.
- `ASST-011` — assistant session endpoint и асимметричные ключи.
- `ASST-012` — CSP и `Permissions-Policy` для same-origin voice.
### Service core
- `ASST-013` — FastAPI skeleton, settings и error model.
- `ASST-014` — PostgreSQL/Alembic и repository layer.
- `ASST-015` — JWT/JWKS validator.
- `ASST-016` — tenant entitlement application service.
- `ASST-017` — quota/rate-limit service.
- `ASST-018` — operator CLI и audit.
- `ASST-019` — bootstrap API.
- `ASST-020` — feedback tokens и endpoint.
- `ASST-021` — usage events без content.
- `ASST-022` — 30-day retention job.
- `ASST-023` — health, metrics, traces и log allowlist.
### Workflow и AI
- `ASST-024` — provider abstractions и fake provider.
- `ASST-025` — deterministic intent resolver.
- `ASST-026` — LLM adapter и structured output.
- `ASST-027` — response/action validator.
- `ASST-028` — confidence/clarification policy.
- `ASST-029` — workflow календарного выходного.
- `ASST-030` — workflow субботних временных слотов.
- `ASST-031` — workflow просмотра группы.
- `ASST-032` — workflow переноса занятия.
- `ASST-033` — RU/EN regression corpus.
### Widget и voice
- `ASST-034` — Web Component shell и placeholder.
- `ASST-035` — text chat и browser state.
- `ASST-036` — SSE client/cancel/retry.
- `ASST-037` — workflow step renderer.
- `ASST-038` — bridge client и action executor.
- `ASST-039` — feedback UI.
- `ASST-040` — i18n completeness gate.
- `ASST-041` — hold-to-talk state machine.
- `ASST-042` — STT upload/validation/adapter.
- `ASST-043` — TTS playback/adapter.
- `ASST-044` — voice privacy notice и permissions UX.
### Delivery и эксплуатация
- `ASST-045` — Gitea checks pipeline.
- `ASST-046` — immutable API image и SBOM.
- `ASST-047` — versioned widget publication.
- `ASST-048` — Kubernetes templates/infrastructure integration.
- `ASST-049` — dashboards и alerts.
- `ASST-050` — privacy/security/runbook docs.
- `ASST-051` — load/failure/security test suite.
- `ASST-052` — pilot checklist и rollback rehearsal.
## 27. Definition of Done первого релиза
Релиз считается готовым, только если одновременно выполнено следующее:
- отдельный репозиторий, pipeline и независимые версии API/widget существуют;
- один tenant можно включить и выключить без нового релиза;
- сервер проверяет tenant, роль, entitlement и квоту на каждом запросе;
- роли `ADMIN`, `EDUCATION_OFFICE`, `DEPARTMENT` проходят согласованную матрицу;
- четыре стартовых вопроса работают текстом и голосом на русском и английском;
- ассистент выполняет только навигацию и подсветку;
- ни модель, ни клиент не могут вернуть/выполнить неизвестный selector, URL или action;
- ответ при отсутствии прав понятен и не открывает запрещённый раздел;
- полный текст, транскрипт и аудио отсутствуют в БД, логах, traces и backups;
- оценка ответа хранится без свободного текста и удаляется с аналитикой через 30 дней;
- retention проверен автоматическим тестом и наблюдаемой production job;
- voice permission запрашивается только после осознанного действия;
- сбой API/LLM/STT/TTS не мешает пользоваться Magistr;
- CSP, XSS, межтенантные и quota-тесты проходят;
- browser E2E подтверждает targets на текущей версии Magistr;
- есть dashboard, alerts, runbook, rollback и операторский аудит;
- документация API, контента, privacy, deployment и эксплуатации актуальна;
- пилотный университет и оператор приняли пользовательские сценарии.
## 28. Основные риски и способы снижения
| Риск | Вероятность/влияние | Снижение |
|---|---|---|
| UI Magistr меняется и подсветка ломается | Высокая/среднее | Stable target contract, CI двух репозиториев, version fallback |
| Модель выбирает неверный сценарий | Средняя/высокое | Детерминированный слой, ограниченный каталог, confidence и clarification |
| Российский API имеет высокую задержку | Средняя/среднее | Замеры этапа 0, короткие prompts, timeout, UX states, provider abstraction |
| Стоимость голоса выше ожиданий | Средняя/высокое | Hold-to-talk, лимит длины, квоты, TTS opt-in, замер unit economics |
| Пользователь произносит персональные данные | Высокая/среднее | Не хранить content, предупреждение, договорные настройки provider, минимальная передача |
| Отсутствуют права на нужный экран | Высокая/низкое | Ролевой workflow как штатный результат, без обходов |
| Межтенантная утечка | Низкая/критическое | Подписанный tenant claim, central policy, isolation tests, no business API access |
| Operator случайно отключает не тот tenant | Средняя/высокое | Exact tenant preview, dry-run, confirmation и audit |
| Два независимых релиза несовместимы | Средняя/высокое | Semver, compatibility window, contract package и безопасный fallback |
| Retention не охватывает backup | Средняя/высокое | Короткий backup retention, restore procedure с cleanup, регулярная проверка |
| Виджет ухудшает доступность интерфейса | Средняя/среднее | Keyboard/screen-reader/mobile tests, reduced motion, неперекрывающая компоновка |
| Fork большого avatar-проекта усложняет поддержку | Высокая/среднее | Малый Web Component, заимствовать идеи, не кодовую базу AIRI |
## 29. Подготовка к будущему выполнению действий
Возможность «сделать после подтверждения» не следует реализовывать расширением текущего `highlight` до `click`. Для неё нужен отдельный command-контур:
```text
пользовательский запрос
-> draft command
-> серверная проверка прав и актуального состояния
-> человекочитаемый preview «что изменится»
-> явное подтверждение пользователя
-> короткоживущий confirmation token
-> идемпотентная команда Magistr API
-> серверный audit/result
```
Обязательные будущие свойства:
- отдельные backend endpoints Magistr, не DOM-автоматизация;
- allowlist команд, строгие DTO и повторная обычная RBAC-проверка;
- preview без побочных эффектов;
- подтверждение привязано к exact command hash, tenant, пользователю и короткому TTL;
- изменение параметра аннулирует старое подтверждение;
- idempotency key и защита от повторной отправки;
- optimistic locking/version для изменяемого объекта;
- явный показ «было / станет»;
- усиленное подтверждение для массовых и необратимых действий;
- полный служебный audit по требованиям безопасности, отдельно от диалогового текста;
- компенсация или понятная ручная процедура, если команда выполнена частично;
- отдельный threat model и юридическое согласование.
До появления этого контура UI action schema первого релиза не содержит `click`, `fill` и `submit` даже в скрытом виде.
## 30. Открытые решения до начала разработки
Эти вопросы не блокируют создание плана, но должны быть закрыты на этапе 0:
1. Какой российский provider выбран отдельно для LLM, STT и TTS; можно ли юридически использовать одного поставщика для всех трёх функций?
2. Какие у него фактические retention, region, SLA, rate limits, streaming и режим запрета обучения?
3. Какие квоты и себестоимость допустимы для пилотного договора?
4. Нужен ли голосовой ответ по умолчанию или только после отдельного включения пользователем?
5. Какой tenant станет пилотным и сколько одновременно активных пользователей ожидается?
6. Каким способом operator CLI аутентифицируется в production: mTLS, OIDC service identity или иной внутренний механизм?
7. Где будет source of truth для production manifest нового сервиса и как он связан с текущим внешним `k8s`?
8. Каков точный срок хранения operator audit и договорных записей?
9. Нужно ли хранить укрупнённые месячные коммерческие агрегаты дольше 30 дней, и можно ли доказать их необратимую обезличенность?
10. Какой окончательный образ, имя, голос, цветовая тема и допустимый уровень анимации у единого персонажа?
11. Нужен ли пользователю переключатель полного выключения озвучивания и удаления browser-session контекста одной кнопкой?
12. Кто принимает и обновляет двуязычный workflow-контент после пилота?
## 31. Рекомендуемый порядок следующих действий
1. Согласовать этот документ как продуктовую и архитектурную основу.
2. Выбрать пилотный tenant и получить обезличенное число потенциальных пользователей.
3. Провести короткий provider spike на четырёх фразах и их вариантах, не передавая production-данные.
4. Зафиксировать ADR и JSON Schema контрактов.
5. Создать отдельный репозиторий `magistr-assistant`.
6. Первым техническим инкрементом реализовать fake assistant + Magistr bridge без внешнего AI.
7. Только после стабильной навигации добавить token exchange, entitlement и API.
8. Затем подключить LLM; после текстовой приёмки — STT/TTS.
9. Выполнить security/privacy review и production hardening.
10. Включить один tenant, собрать 30 дней обезличенных исходов и решить, какие workflow добавлять дальше.
## 32. Источники и точки сверки
### Исходники Magistr
- `frontend/admin/js/main.js` — маршрутизация и асинхронная загрузка views;
- `frontend/admin/js/role-capabilities.js` — доступные вкладки по ролям;
- `frontend/admin/settings/js/main.js` — отдельный settings SPA;
- `frontend/admin/js/views/academic-calendar.js` — сетка календаря и выбор активности дня;
- `frontend/admin/settings/js/views/time-slots.js` — временные слоты и области действия;
- `frontend/admin/js/views/schedule-view.js` — режим группы, кафедры и override;
- `backend/src/main/java/com/magistr/app/controller/AcademicCalendarController.java` — роли календаря;
- `backend/src/main/java/com/magistr/app/controller/ScheduleOverrideController.java` — роли точечных изменений;
- `frontend/security.conf` — текущие CSP и Permissions-Policy;
- `docs/ARCHITECTURE.md`, `docs/FRONTEND.md`, `docs/INFRASTRUCTURE.md` — действующие проектные соглашения.
### Внешняя техническая документация
- [FastAPI: общие принципы deployment](https://fastapi.tiangolo.com/deployment/concepts/) — репликация, перезапуски, память и предварительные шаги;
- [FastAPI: server workers](https://fastapi.tiangolo.com/deployment/server-workers/) — модель worker и контейнерные варианты;
- [Статус версий Python](https://devguide.python.org/versions/) — сроки bugfix/security-поддержки 3.13 и 3.14;
- [PostgreSQL: политика версий](https://www.postgresql.org/support/versioning/) — поддерживаемые major/minor и сроки окончания поддержки;
- [SQLAlchemy 2.0 Documentation](https://docs.sqlalchemy.org/en/20/) — выбранная стабильная линия ORM.
Перед непосредственной реализацией версии языка, библиотек, PostgreSQL, browser APIs и требования конкретного AI-провайдера необходимо повторно проверить по актуальной официальной документации и зафиксировать lock-файлами.