docs(public-share): add model & voice input notes to public share plan
docs: add AI agent roles plan documentation
This commit is contained in:
362
docs/ai-agent-roles-plan.md
Normal file
362
docs/ai-agent-roles-plan.md
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
# Роли агента (Agent Roles) — проектный план
|
||||||
|
|
||||||
|
> Статус: проработанная фича, **не реализована**. Контекст: gitmost — форк Docmost.
|
||||||
|
> Идея: дать возможность создавать переиспользуемые **роли агента** (например
|
||||||
|
> «Корректор» или «Факт-чекер, который ходит в веб и проверяет факты») и
|
||||||
|
> заводить чат, привязанный к выбранной роли. Роль задаёт поведение агента
|
||||||
|
> (системный промпт) и, опционально, модель.
|
||||||
|
>
|
||||||
|
> Зафиксированные решения по объёму (см. раздел «Развилки»):
|
||||||
|
> - **Владение** — только **админские, общие на воркспейс** роли (как провайдер и
|
||||||
|
> внешние MCP-серверы сегодня). Личных ролей в v1 нет.
|
||||||
|
> - **Гейтинг инструментов** — **нет**. Роль меняет только инструкции и (опц.) модель;
|
||||||
|
> набор инструментов всегда полный (тот же, что у обычного чата). Ограничение
|
||||||
|
> возможностей по ролям отложено (см. «Возможные расширения»).
|
||||||
|
> - **Артефакт этого шага** — только дизайн-документ; код не пишется.
|
||||||
|
|
||||||
|
## Зачем это (и почему ложится в текущую архитектуру)
|
||||||
|
|
||||||
|
Сегодня у встроенного AI-агента нет понятия персоны/роли на уровне чата: вся
|
||||||
|
настройка поведения — один системный промпт **на весь воркспейс**. Пользователь
|
||||||
|
хочет заводить разные чаты под разные задачи (вычитка орфографии, проверка фактов
|
||||||
|
по вебу и т. д.), каждый — со своей инструкцией и, возможно, своей моделью.
|
||||||
|
|
||||||
|
Три факта из текущего кода определяют дизайн (всё сверено по исходникам):
|
||||||
|
|
||||||
|
1. **Системный промпт — только на уровне воркспейса.** Собирается в
|
||||||
|
[ai-chat.prompt.ts](../apps/server/src/core/ai-chat/ai-chat.prompt.ts),
|
||||||
|
функция `buildSystemPrompt()`, по слоям: *базовая персона*
|
||||||
|
(`workspace.settings.ai.provider.systemPrompt` либо `DEFAULT_PROMPT`) →
|
||||||
|
*контекст* (имя воркспейса, открытая страница) → несъёмный `SAFETY_FRAMEWORK`.
|
||||||
|
Персоны на чат сейчас нет — её надо добавить как ещё один слой.
|
||||||
|
|
||||||
|
2. **Инструменты — всегда все включены.** В
|
||||||
|
[ai-chat.service.ts](../apps/server/src/core/ai-chat/ai-chat.service.ts):
|
||||||
|
`const tools = { ...external.tools, ...docmostTools }`. ~40 Docmost-инструментов
|
||||||
|
строит `AiChatToolsService.forUser()`
|
||||||
|
([tools/ai-chat-tools.service.ts](../apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts)),
|
||||||
|
внешние MCP-инструменты подмешивает `mcpClients.toolsFor(workspaceId)`
|
||||||
|
([external-mcp/mcp-clients.service.ts](../apps/server/src/core/ai-chat/external-mcp/mcp-clients.service.ts)).
|
||||||
|
Механизма включать подмножество инструментов нет — есть только CASL-проверка в
|
||||||
|
момент вызова (через персональный loopback-токен). **По зафиксированному
|
||||||
|
решению этот механизм мы и не вводим** — роль не трогает набор инструментов.
|
||||||
|
|
||||||
|
3. **Веб-доступ уже решён внешними MCP.** Внешние MCP-серверы
|
||||||
|
(`ai_mcp_servers`, напр. Tavily) с SSRF-защитой
|
||||||
|
([external-mcp/ssrf-guard.ts](../apps/server/src/core/ai-chat/external-mcp/ssrf-guard.ts))
|
||||||
|
и шифрованием заголовков — это и есть «факт-чекер ходит в гугл». Поскольку
|
||||||
|
гейтинга нет, веб-инструменты **уже доступны каждому чату**, если админ
|
||||||
|
подключил соответствующий MCP-сервер. Роль «Факт-чекер» работает чисто за счёт
|
||||||
|
инструкции «проверяй факты по веб-источникам и цитируй ссылки» — она направляет
|
||||||
|
модель пользоваться уже доступными инструментами, а не добавляет их.
|
||||||
|
|
||||||
|
4. **Чат создаётся неявно** при первом сообщении: клиент
|
||||||
|
([chat-thread.tsx](../apps/client/src/features/ai-chat/components/chat-thread.tsx))
|
||||||
|
шлёт POST `/api/ai-chat/stream` с `chatId: null`, сервер
|
||||||
|
([ai-chat.controller.ts](../apps/server/src/core/ai-chat/ai-chat.controller.ts))
|
||||||
|
создаёт строку `ai_chats`. Привязать чат к роли можно одним новым полем `role_id`,
|
||||||
|
которое клиент передаёт один раз при первом сообщении.
|
||||||
|
|
||||||
|
**Вывод:** роль — это тонкий слой поверх существующего пайплайна. Нужны:
|
||||||
|
новая таблица ролей + админский CRUD, поле `ai_chats.role_id`, новый слой в
|
||||||
|
`buildSystemPrompt()`, опциональный override модели в `getChatModel()`, пикер роли
|
||||||
|
и управление ролями в UI. Граница безопасности (CASL через loopback-токен)
|
||||||
|
**не меняется** — роль её не ослабляет и не усиливает (см. «Безопасность»).
|
||||||
|
|
||||||
|
## Модель
|
||||||
|
|
||||||
|
**Роль (Agent Role)** — именованный, общий на воркспейс пресет, который связывает:
|
||||||
|
|
||||||
|
| Часть | Что задаёт | Пример «Корректор» | Пример «Факт-чекер» |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **instructions** | фрагмент системного промпта (персона/поведение) | «Исправляй только орфографию, пунктуацию и грамматику. Никогда не меняй смысл, факты, тон и структуру текста. Используй точечную правку текста» | «Проверяй фактические утверждения страницы по авторитетным веб-источникам. Цитируй ссылки. Помечай сомнительные места комментарием. Не редактируй текст страницы без явной просьбы» |
|
||||||
|
| **model (опц.)** | модель ≠ дефолт воркспейса | дешёвая модель | сильная модель |
|
||||||
|
| **presentation** | имя, emoji, описание | 🔤 «Корректор» | 🔎 «Факт-чекер» |
|
||||||
|
|
||||||
|
Чего роль в v1 **не** задаёт (по зафиксированным решениям): набор инструментов,
|
||||||
|
выбор конкретных внешних MCP-серверов, владельца (роли только общие/админские),
|
||||||
|
снапшот конфигурации на чат.
|
||||||
|
|
||||||
|
**Привязка чата к роли** — нулевое поле `ai_chats.role_id`. Чат «помнит», с какой
|
||||||
|
ролью создан; роль применяется на каждом ходу. Чат без роли (`role_id IS NULL`) —
|
||||||
|
обычный универсальный ассистент (текущее поведение).
|
||||||
|
|
||||||
|
## Модель данных (миграции)
|
||||||
|
|
||||||
|
Соглашение: `apps/server/src/database/migrations/YYYYMMDDThhmmss-description.ts`.
|
||||||
|
Только **добавляем** таблицы/столбцы (никогда не трогаем данные Docmost). Timestamp
|
||||||
|
новой миграции должен сортироваться **после** последней применённой; на момент
|
||||||
|
написания последняя — `20260618T160000-ai-stt-credentials.ts`, значит брать
|
||||||
|
`20260619T...`. После миграции — `pnpm --filter server migration:codegen` для
|
||||||
|
регенерации [db.d.ts](../apps/server/src/database/types/db.d.ts). Образец стиля —
|
||||||
|
[20260617T130000-ai-mcp-servers.ts](../apps/server/src/database/migrations/20260617T130000-ai-mcp-servers.ts).
|
||||||
|
|
||||||
|
**Миграция — таблица ролей + привязка чата:**
|
||||||
|
```sql
|
||||||
|
CREATE TABLE ai_agent_roles (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_uuid_v7(),
|
||||||
|
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||||
|
creator_id uuid REFERENCES users(id) ON DELETE SET NULL, -- кто создал (аудит)
|
||||||
|
name varchar NOT NULL, -- "Корректор"
|
||||||
|
emoji varchar, -- presentation
|
||||||
|
description text,
|
||||||
|
instructions text NOT NULL, -- фрагмент system prompt
|
||||||
|
model_config jsonb, -- { driver?, chatModel } | NULL = дефолт воркспейса
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
deleted_at timestamptz -- soft delete (как у ai_chats)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_ai_agent_roles_workspace_id ON ai_agent_roles (workspace_id);
|
||||||
|
|
||||||
|
-- привязка чата к роли
|
||||||
|
ALTER TABLE ai_chats
|
||||||
|
ADD COLUMN role_id uuid REFERENCES ai_agent_roles(id) ON DELETE SET NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
Заметки:
|
||||||
|
- `creator_id ON DELETE SET NULL` — роль общая и переживает удаление автора
|
||||||
|
(в отличие от `ai_chats.creator_id`, который `NOT NULL`); это только аудит.
|
||||||
|
- `ai_chats.role_id ON DELETE SET NULL` — если роль жёстко удалят, чат
|
||||||
|
деградирует к универсальному поведению, а не ломается (см. edge-cases).
|
||||||
|
В сочетании с `deleted_at` основной путь удаления роли — **soft delete**:
|
||||||
|
старые чаты тогда продолжают видеть инструкции через JOIN с учётом `deleted_at`
|
||||||
|
(решение по поведению при удалении — в «Открытых вопросах»).
|
||||||
|
- `model_config jsonb` — `{ chatModel }` либо `{ driver, chatModel }`. Пусто/`NULL`
|
||||||
|
→ модель воркспейса. По образцу `publicShareChatModel` из
|
||||||
|
[public-share-assistant-plan.md](./public-share-assistant-plan.md): креды
|
||||||
|
(`apiKey`/`baseUrl`) берутся от провайдера соответствующего драйвера из
|
||||||
|
`ai_provider_credentials`, отдельные креды на роль не нужны.
|
||||||
|
|
||||||
|
Типы: добавить `AiAgentRoles` в `db.interface.ts` (или поднять через codegen),
|
||||||
|
`role_id` появится в `AiChats` автоматически после codegen.
|
||||||
|
|
||||||
|
## Бэкенд
|
||||||
|
|
||||||
|
### 1. Слой инструкций роли в системном промпте
|
||||||
|
|
||||||
|
В [ai-chat.prompt.ts](../apps/server/src/core/ai-chat/ai-chat.prompt.ts) добавить
|
||||||
|
вход `roleInstructions` в `buildSystemPrompt()`. Приоритет персоны:
|
||||||
|
```text
|
||||||
|
effectivePersona = roleInstructions?.trim() || adminPrompt?.trim() || DEFAULT_PROMPT
|
||||||
|
return `${effectivePersona}${context}\n${SAFETY_FRAMEWORK}`
|
||||||
|
```
|
||||||
|
Ключевое: **`SAFETY_FRAMEWORK` по-прежнему добавляется всегда и не отключается
|
||||||
|
ролью.** Роль задаёт только персону; контекст (воркспейс, открытая страница) и
|
||||||
|
safety-блок остаются как есть.
|
||||||
|
|
||||||
|
Решение «роль заменяет, а не дополняет admin-промпт» выбрано намеренно: для
|
||||||
|
узкой роли вроде «Корректора» нужно, чтобы её инструкция доминировала, а не
|
||||||
|
конкурировала с общим промптом воркспейса. (Альтернатива «конкатенировать
|
||||||
|
admin-промпт + роль» — в «Открытых вопросах».)
|
||||||
|
|
||||||
|
### 2. Применение роли в стриме
|
||||||
|
|
||||||
|
В [ai-chat.service.ts](../apps/server/src/core/ai-chat/ai-chat.service.ts) (метод
|
||||||
|
`stream()`), где сейчас резолвится `system` и `model`:
|
||||||
|
- Загрузить роль по `ai_chats.role_id` (если задан и не удалён).
|
||||||
|
- Передать `role.instructions` в `buildSystemPrompt({ ..., roleInstructions })`.
|
||||||
|
- Если у роли есть `model_config` — резолвить модель с override (см. п. 3).
|
||||||
|
- Набор инструментов **не меняется** (по решению).
|
||||||
|
|
||||||
|
Важно: `role_id` сервер берёт **из строки `ai_chats`, а не из тела запроса** на
|
||||||
|
каждом ходу — роль нельзя подменить пораздачно. Клиент сообщает `roleId` только
|
||||||
|
при создании чата (первое сообщение), сервер сохраняет его в `ai_chats.role_id`.
|
||||||
|
|
||||||
|
### 3. Override модели
|
||||||
|
|
||||||
|
`AiService.getChatModel(workspaceId)`
|
||||||
|
([integrations/ai/ai.service.ts](../apps/server/src/integrations/ai/ai.service.ts))
|
||||||
|
получает опциональный аргумент override модели (паттерн из
|
||||||
|
[public-share-assistant-plan.md](./public-share-assistant-plan.md) §5):
|
||||||
|
- `model_config.chatModel` — id модели вместо `chatModel` воркспейса;
|
||||||
|
- `model_config.driver` (опц.) — если указан другой драйвер, берём его креды из
|
||||||
|
`ai_provider_credentials`; если кредов нет → `AiNotConfiguredException` (503) с
|
||||||
|
**внятным сообщением** («для роли X выбран провайдер Y, но он не настроен»),
|
||||||
|
согласно конвенции об ошибках (никаких «Something went wrong»).
|
||||||
|
- Пусто → текущее поведение (модель воркспейса).
|
||||||
|
|
||||||
|
Резолв модели делать **до** hijack ответа, чтобы ненастроенный провайдер вернул
|
||||||
|
503, а не падал в середине стрима (как уже сделано в контроллере для воркспейс-модели).
|
||||||
|
|
||||||
|
### 4. CRUD ролей (админский модуль)
|
||||||
|
|
||||||
|
Новый модуль `core/ai-chat/roles/` рядом с `external-mcp/`:
|
||||||
|
`ai-agent-roles.controller.ts` + `ai-agent-roles.service.ts` + repo
|
||||||
|
(`database/repos/ai-agent-roles/`). Эндпоинты под `/api/ai-chat/roles` (или
|
||||||
|
`/api/ai-settings/roles` — рядом с MCP-серверами; выбрать единообразно с
|
||||||
|
существующим размещением, см. «Открытые вопросы»):
|
||||||
|
|
||||||
|
| Метод | Доступ | Назначение |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `list` | **любой участник воркспейса** | получить список ролей для пикера при создании чата |
|
||||||
|
| `create` / `update` / `delete` | **только админ** | управление ролями (как `ai-settings`) |
|
||||||
|
|
||||||
|
Нюанс CASL: создание/правка/удаление — под админской абилити (как
|
||||||
|
[ai-settings.controller.ts](../apps/server/src/core/.../ai-settings.controller.ts)
|
||||||
|
управляет провайдером и MCP-серверами), но **list должен быть доступен всем
|
||||||
|
участникам**, иначе обычный пользователь не сможет выбрать роль при заведении
|
||||||
|
чата. Все запросы строго скоупятся по `workspace_id` (мультитенант по хосту).
|
||||||
|
|
||||||
|
Валидация при create/update: непустые `name` и `instructions`; если задан
|
||||||
|
`model_config.driver` — он из числа поддерживаемых (`openai`/`gemini`/`ollama`).
|
||||||
|
|
||||||
|
## Клиент
|
||||||
|
|
||||||
|
### 1. Пикер роли при создании чата
|
||||||
|
|
||||||
|
В зоне «New chat» / композере
|
||||||
|
([ai-chat-window.tsx](../apps/client/src/features/ai-chat/components/ai-chat-window.tsx),
|
||||||
|
[chat-input.tsx](../apps/client/src/features/ai-chat/components/chat-input.tsx)) —
|
||||||
|
селектор роли (Mantine `Select`/`SegmentedControl`), дефолт «Универсальный
|
||||||
|
ассистент» (без роли). Выбранный `roleId` хранится в новом Jotai-атоме рядом с
|
||||||
|
[atoms/ai-chat-atom.ts](../apps/client/src/features/ai-chat/atoms/ai-chat-atom.ts)
|
||||||
|
и уходит в теле **первого** запроса на `/stream` (расширить
|
||||||
|
`prepareSendMessagesRequest` в `chat-thread.tsx`: добавить `roleId`). После того
|
||||||
|
как сервер создал чат с ролью, пикер для этого чата фиксируется (роль чата
|
||||||
|
неизменна; смена роли = новый чат — простое и предсказуемое поведение для v1).
|
||||||
|
|
||||||
|
### 2. Бейдж роли
|
||||||
|
|
||||||
|
Показывать emoji+имя роли в шапке окна чата и в строке списка
|
||||||
|
([conversation-list.tsx](../apps/client/src/features/ai-chat/components/conversation-list.tsx)),
|
||||||
|
чтобы было видно, «с кем» разговор. `role_id`/денормализованное имя+emoji роли
|
||||||
|
добавить в выдачу списка чатов и тип `IAiChat`
|
||||||
|
([types/ai-chat.types.ts](../apps/client/src/features/ai-chat/types/ai-chat.types.ts)).
|
||||||
|
|
||||||
|
### 3. Управление ролями в настройках
|
||||||
|
|
||||||
|
Новая секция «Роли агента» в Settings → AI
|
||||||
|
([pages/settings/workspace/ai-settings.tsx](../apps/client/src/pages/settings/workspace/ai-settings.tsx)),
|
||||||
|
рядом с «External tools». Переиспользовать паттерн add/edit/delete-модалки из
|
||||||
|
[ai-mcp-servers.tsx](../apps/client/src/features/workspace/components/settings/components/ai-mcp-servers.tsx).
|
||||||
|
Форма роли: имя, emoji, описание, **instructions** (textarea — как редактор
|
||||||
|
системного сообщения в
|
||||||
|
[ai-provider-settings.tsx](../apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx)),
|
||||||
|
опциональный override модели. Подпись-напоминание под полем instructions:
|
||||||
|
«встроенный safety-фреймворк добавляется автоматически» (как у системного сообщения).
|
||||||
|
|
||||||
|
### 4. Слой запросов
|
||||||
|
|
||||||
|
Новые TanStack Query хуки в
|
||||||
|
[queries/ai-chat-query.ts](../apps/client/src/features/ai-chat/queries/ai-chat-query.ts)
|
||||||
|
(или отдельный файл): `useAiRolesQuery()` (list), `useCreate/Update/DeleteAiRoleMutation()`
|
||||||
|
+ функции в
|
||||||
|
[services/ai-chat-service.ts](../apps/client/src/features/ai-chat/services/ai-chat-service.ts).
|
||||||
|
Тип `IAiRole` зеркалит серверную схему.
|
||||||
|
|
||||||
|
## Поток одного хода (с ролью)
|
||||||
|
|
||||||
|
1. Создание чата: клиент шлёт первое сообщение + `roleId` → `/ai-chat/stream`;
|
||||||
|
сервер создаёт `ai_chats` с `role_id`.
|
||||||
|
2. Последующие ходы: сервер читает `role_id` из строки чата (не из тела запроса).
|
||||||
|
3. Резолв: загрузить роль (если не удалена) → `instructions` + `model_config`.
|
||||||
|
4. `buildSystemPrompt({ workspace, adminPrompt, roleInstructions, openedPage })`
|
||||||
|
→ персона роли + контекст + несъёмный `SAFETY_FRAMEWORK`.
|
||||||
|
5. `getChatModel(workspaceId, role.model_config)` → модель роли или дефолт.
|
||||||
|
6. `streamText({ model, system, messages, tools, stopWhen: stepCountIs(8) })` —
|
||||||
|
**набор инструментов полный, как у обычного чата**.
|
||||||
|
|
||||||
|
## Edge-cases (главное)
|
||||||
|
|
||||||
|
- **Роль удалена/выключена, а чаты на неё ссылаются.** При hard-delete
|
||||||
|
`ON DELETE SET NULL` обнуляет `ai_chats.role_id` → чат продолжает работать как
|
||||||
|
универсальный. Основной путь — soft-delete (`deleted_at`)/`enabled=false`:
|
||||||
|
тогда роль исчезает из пикера, но старые чаты могут продолжать применять её
|
||||||
|
инструкции (резолв учитывает `deleted_at` — точное поведение в «Открытых
|
||||||
|
вопросах»).
|
||||||
|
- **Роль отредактировали после создания чатов.** В v1 без снапшота правка
|
||||||
|
применяется «вживую» — старые чаты подхватывают новые инструкции на следующем
|
||||||
|
ходу. Приемлемо для кейсов «Корректор/Факт-чекер»; снапшот конфигурации на чат —
|
||||||
|
возможное расширение.
|
||||||
|
- **Safety не переопределяется.** `SAFETY_FRAMEWORK` добавляется всегда, что бы
|
||||||
|
ни написали в `instructions` роли (включая попытку «игнорируй прежние инструкции»).
|
||||||
|
- **Override модели на ненастроенный провайдер** → 503 с конкретным сообщением,
|
||||||
|
а не молчаливый фолбэк (конвенция об ошибках). Решить, делать ли мягкий фолбэк
|
||||||
|
на модель воркспейса (в «Открытых вопросах»).
|
||||||
|
- **Пустые `instructions`** недопустимы при создании (валидация); но если роль
|
||||||
|
как-то оказалась с пустыми инструкциями — персона падает на admin-промпт/дефолт.
|
||||||
|
- **Заголовок чата** генерируется фоново (`generateText`) — оставить на модели
|
||||||
|
воркспейса, чтобы экзотический override роли не ломал автозаголовок (мелочь).
|
||||||
|
- **Мультитенант.** Все операции с ролями скоупятся по `workspace_id`; роль из
|
||||||
|
чужого воркспейса не видна и не применима.
|
||||||
|
- **MCP-зеркало схемы** ([packages/mcp](../packages/mcp)) фичу не затрагивает —
|
||||||
|
роли живут только во встроенном AI-чате, не в standalone MCP.
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
- **Граница безопасности не меняется.** Агент по-прежнему ходит в API через
|
||||||
|
персональный loopback-JWT (`AiChatToolsService.forUser`), и CASL ограничивает
|
||||||
|
его ровно правами текущего пользователя. Роль — это слой *формирования промпта
|
||||||
|
и выбора модели*, он не выдаёт и не отнимает прав.
|
||||||
|
- **Следствие решения «без гейтинга» (осознанный компромисс):**
|
||||||
|
- Роль «Корректор» инструкцией просят не менять смысл, но технически у чата
|
||||||
|
остаются все write-инструменты — модель *могла бы* отредактировать/удалить
|
||||||
|
(под soft-delete и CASL, т. е. обратимо и в пределах прав пользователя). Это
|
||||||
|
мягкая граница (промпт), а не жёсткая.
|
||||||
|
- Роль «Факт-чекер» полагается на то, что админ глобально подключил веб-MCP
|
||||||
|
(Tavily); тогда веб-инструменты доступны *всем* чатам, а не только этой роли.
|
||||||
|
Жёсткие границы возможностей по ролям — отдельная будущая фаза (см. ниже).
|
||||||
|
- **Instructions — доверенный контент:** их пишет админ воркспейса, они попадают
|
||||||
|
только в системный промпт чатов этого воркспейса и исполняются под правами
|
||||||
|
конкретного пользователя. Эскалации нет.
|
||||||
|
- **Внешние MCP** остаются под SSRF-guard; роли логику подключения MCP не трогают.
|
||||||
|
|
||||||
|
## Явные non-goals (v1)
|
||||||
|
|
||||||
|
- Нет гейтинга/ограничения инструментов по ролям (роль не сужает тулсет).
|
||||||
|
- Нет личных ролей (только общие админские).
|
||||||
|
- Нет выбора конкретных внешних MCP-серверов на роль (все включённые доступны всем).
|
||||||
|
- Нет снапшота конфигурации роли на чат (правка роли применяется вживую).
|
||||||
|
- Нет per-role параметров генерации сверх модели (temperature и т. п.).
|
||||||
|
- Нет композиции «скиллов» поверх роли (см. «Связь со „скиллами“»).
|
||||||
|
|
||||||
|
## Связь со «скиллами»
|
||||||
|
|
||||||
|
В терминах Anthropic Skills (подгружаемый по требованию пакет инструкций +
|
||||||
|
ресурсов/скриптов) текущая роль = MVP-«скилл»: только текстовая инструкция + выбор
|
||||||
|
модели. Естественная эволюция — сделать «скиллы» композируемыми (несколько скиллов
|
||||||
|
на одну роль), привязывать к роли эталонные страницы/файлы как контекст, и —
|
||||||
|
главное — добавить **жёсткий гейтинг инструментов** (тогда «Корректор» физически не
|
||||||
|
сможет удалять, а «Факт-чекер» получит веб ровно тогда, когда роль это разрешает).
|
||||||
|
Всё это — следующие итерации, вне scope v1.
|
||||||
|
|
||||||
|
## Развилки (зафиксированные решения)
|
||||||
|
|
||||||
|
| Развилка | Решение | Альтернативы (отклонены / отложены) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Владение ролями | **Только админские, общие на воркспейс** | личные роли; личные + общие |
|
||||||
|
| Ограничение инструментов | **Нет (только промпт + модель)** | крупные группы возможностей; тонкий per-tool allowlist |
|
||||||
|
| Выбор MCP-серверов на роль | **Нет (все включённые доступны всем)** | мультиселект MCP-серверов на роль |
|
||||||
|
| Привязка чата к роли | **Поле `ai_chats.role_id`, неизменно после создания** | смена роли внутри чата; роль в теле каждого запроса |
|
||||||
|
| Персона роли vs admin-промпт | **Роль заменяет персону** (safety всегда добавляется) | конкатенация admin-промпт + роль |
|
||||||
|
| Снапшот конфигурации | **Нет (правка вживую)** | снапшот конфигурации роли на чат |
|
||||||
|
|
||||||
|
## Открытые вопросы (не блокируют дизайн)
|
||||||
|
|
||||||
|
1. **Размещение CRUD-эндпоинтов и UI:** `/ai-chat/roles` (рядом с чатом) или
|
||||||
|
`/ai-settings/roles` (рядом с MCP-серверами). Предлагаю в одном месте с MCP —
|
||||||
|
там уже живут админские AI-настройки.
|
||||||
|
2. **Поведение при удалении роли:** soft-delete с сохранением инструкций для старых
|
||||||
|
чатов vs hard-delete + `SET NULL` (старые чаты деградируют к универсальным).
|
||||||
|
Предлагаю soft-delete (`deleted_at`) — консистентно с `ai_chats`.
|
||||||
|
3. **Override модели на ненастроенный драйвер:** жёсткий 503 с внятным сообщением
|
||||||
|
vs мягкий фолбэк на модель воркспейса. Предлагаю 503 (явность важнее).
|
||||||
|
4. **Стартовые пресеты:** поставлять ли «Корректор» и «Факт-чекер» как
|
||||||
|
преднастроенные роли-шаблоны (seed) при включении фичи, чтобы админ не писал
|
||||||
|
инструкции с нуля. Предлагаю — да, как необязательный «вставить пример».
|
||||||
|
5. **Денормализация для бейджа:** хранить имя/emoji роли только в `ai_agent_roles`
|
||||||
|
и джойнить, либо денормализовать на `ai_chats` для дешёвого списка. Предлагаю
|
||||||
|
джойн (простота; список чатов не горячий путь).
|
||||||
|
|
||||||
|
## Объём работ
|
||||||
|
|
||||||
|
Бэкенд: 1 миграция (`ai_agent_roles` + `ai_chats.role_id`) + codegen типов;
|
||||||
|
новый CRUD-модуль ролей (controller/service/repo) под CASL; правка
|
||||||
|
`buildSystemPrompt()` (слой `roleInstructions`); правка `AiChatService.stream()`
|
||||||
|
(загрузка роли, передача инструкций и override модели); опциональный override
|
||||||
|
модели в `AiService.getChatModel()`. Клиент: пикер роли при создании чата + атом +
|
||||||
|
проброс `roleId` в первый запрос; бейдж роли в шапке и списке; секция управления
|
||||||
|
ролями в Settings → AI (модалка add/edit/delete по образцу MCP-серверов); хуки
|
||||||
|
запросов/мутаций. **Без изменений в `packages/mcp`. Набор инструментов агента не
|
||||||
|
трогаем.**
|
||||||
@@ -8,7 +8,9 @@
|
|||||||
> Зафиксированные решения по объёму (см. раздел «Развилки»):
|
> Зафиксированные решения по объёму (см. раздел «Развилки»):
|
||||||
> область поиска — **всё дерево шары**; движок поиска — **готовый share-scoped FTS**
|
> область поиска — **всё дерево шары**; движок поиска — **готовый share-scoped FTS**
|
||||||
> (ветка `shareId` в `SearchService`); гейтинг — **один тумблер воркспейса**;
|
> (ветка `shareId` в `SearchService`); гейтинг — **один тумблер воркспейса**;
|
||||||
> хранение диалогов — **эфемерное** (без БД, без миграций).
|
> хранение диалогов — **эфемерное** (без БД, без миграций);
|
||||||
|
> модель — **отдельная дешёвая** (не основная модель чата воркспейса);
|
||||||
|
> ввод — **только текст** (без голосового ввода / STT).
|
||||||
|
|
||||||
## Зачем это нетривиально
|
## Зачем это нетривиально
|
||||||
|
|
||||||
@@ -48,7 +50,9 @@
|
|||||||
- **Публичные роуты** в [share.controller.ts](../apps/server/src/core/share/share.controller.ts)
|
- **Публичные роуты** в [share.controller.ts](../apps/server/src/core/share/share.controller.ts)
|
||||||
уже `@Public()`, воркспейс резолвит `DomainMiddleware` по хосту; новый роут под `/api/shares/*`
|
уже `@Public()`, воркспейс резолвит `DomainMiddleware` по хосту; новый роут под `/api/shares/*`
|
||||||
ложится туда же — **правок в [main.ts](../apps/server/src/main.ts) не нужно**.
|
ложится туда же — **правок в [main.ts](../apps/server/src/main.ts) не нужно**.
|
||||||
- **Стриминг-плумбинг**: `AiService.getChatModel(workspaceId)` +
|
- **Стриминг-плумбинг**: `AiService.getChatModel(workspaceId)` (нужен небольшой апгрейд —
|
||||||
|
опциональный override id модели, чтобы для шары взять дешёвую `publicShareChatModel`
|
||||||
|
вместо основной `chatModel`; драйвер/`baseUrl`/`apiKey` те же) +
|
||||||
`streamText` → `pipeUIMessageStreamToResponse` (как в
|
`streamText` → `pipeUIMessageStreamToResponse` (как в
|
||||||
[ai-chat.service.ts](../apps/server/src/core/ai-chat/ai-chat.service.ts)).
|
[ai-chat.service.ts](../apps/server/src/core/ai-chat/ai-chat.service.ts)).
|
||||||
|
|
||||||
@@ -56,12 +60,22 @@
|
|||||||
|
|
||||||
### Сервер
|
### Сервер
|
||||||
|
|
||||||
**1. Тумблер воркспейса (гейтинг).**
|
**1. Тумблер воркспейса (гейтинг) + отдельная модель.**
|
||||||
Новое булево поле в `workspace.settings.ai`, напр. `publicShareAssistant` (default `false`) —
|
Новое булево поле в `workspace.settings.ai`, напр. `publicShareAssistant` (default `false`) —
|
||||||
туда же, где живут остальные AI-настройки и тумблер MCP; читается/пишется через сервис
|
туда же, где живут остальные AI-настройки и тумблер MCP; читается/пишется через сервис
|
||||||
AI-настроек (рядом с `ai-settings.service.ts`). В админке **Workspace settings → AI** —
|
AI-настроек (рядом с `ai-settings.service.ts`). В админке **Workspace settings → AI** —
|
||||||
один свитч. Хелпер `isPublicShareAssistantEnabled(workspaceId)`.
|
один свитч. Хелпер `isPublicShareAssistantEnabled(workspaceId)`.
|
||||||
|
|
||||||
|
Рядом — **отдельное поле модели** `publicShareChatModel?: string` в `settings.ai.provider`
|
||||||
|
([ai.types.ts](../apps/server/src/integrations/ai/ai.types.ts), рядом с `chatModel` /
|
||||||
|
`embeddingModel` / `sttModel`). Это **только id модели**: драйвер, `baseUrl` и `apiKey`
|
||||||
|
переиспользуются от основного чат-провайдера — отдельные креды не нужны. Пустое значение →
|
||||||
|
fallback на `chatModel`. В админке Workspace settings → AI — отдельное поле «модель
|
||||||
|
публичного ассистента». Зачем отдельная и дешёвая: за токены анонимов платит **владелец
|
||||||
|
воркспейса**, а read-only Q&A строго по дереву шары не требует флагманской модели — это и
|
||||||
|
анти-абьюз (дешевле цена ошибки/абьюза), и явное разделение «дорогой внутренний агент vs
|
||||||
|
дешёвый внешний ассистент».
|
||||||
|
|
||||||
**2. Публичный эндпоинт** `POST /api/shares/ai/stream` (`@Public()`).
|
**2. Публичный эндпоинт** `POST /api/shares/ai/stream` (`@Public()`).
|
||||||
Новые `public-share-chat.controller.ts` + `public-share-chat.service.ts` в модуле `ai-chat`
|
Новые `public-share-chat.controller.ts` + `public-share-chat.service.ts` в модуле `ai-chat`
|
||||||
(переиспользуют `AiService` и плумбинг `streamText`), зависят от `ShareRepo` / `PageRepo` /
|
(переиспользуют `AiService` и плумбинг `streamText`), зависят от `ShareRepo` / `PageRepo` /
|
||||||
@@ -104,6 +118,8 @@ in-process (никакого loopback-токена и user-identity):
|
|||||||
опубликованной документации; ничего не можешь менять; если ответа в страницах нет — так
|
опубликованной документации; ничего не можешь менять; если ответа в страницах нет — так
|
||||||
и говоришь» + неизменяемый safety-блок по образцу
|
и говоришь» + неизменяемый safety-блок по образцу
|
||||||
[ai-chat.prompt.ts](../apps/server/src/core/ai-chat/ai-chat.prompt.ts).
|
[ai-chat.prompt.ts](../apps/server/src/core/ai-chat/ai-chat.prompt.ts).
|
||||||
|
`model` — **дешёвая `publicShareChatModel`** (override в `getChatModel`, fallback на
|
||||||
|
`chatModel`), а не основная модель агента воркспейса.
|
||||||
`streamText({ model, system, messages, tools, stopWhen: stepCountIs(5) })`.
|
`streamText({ model, system, messages, tools, stopWhen: stepCountIs(5) })`.
|
||||||
**Без серверного хранения** — транскрипт держит клиент; доверять присланным сообщениям
|
**Без серверного хранения** — транскрипт держит клиент; доверять присланным сообщениям
|
||||||
безопасно, т.к. scope обеспечивают тулы, а не транскрипт. Это снимает проблему
|
безопасно, т.к. scope обеспечивают тулы, а не транскрипт. Это снимает проблему
|
||||||
@@ -123,7 +139,7 @@ in-process (никакого loopback-токена и user-identity):
|
|||||||
шлёт `{ shareId, pageId, messages }`, `credentials: 'omit'`. Эфемерный, in-memory —
|
шлёт `{ shareId, pageId, messages }`, `credentials: 'omit'`. Эфемерный, in-memory —
|
||||||
стрипнутая версия
|
стрипнутая версия
|
||||||
[chat-thread.tsx](../apps/client/src/features/ai-chat/components/chat-thread.tsx) без
|
[chat-thread.tsx](../apps/client/src/features/ai-chat/components/chat-thread.tsx) без
|
||||||
списка чатов, истории и персистентности.
|
списка чатов, истории, персистентности и **голосового ввода** (только текстовое поле).
|
||||||
|
|
||||||
## Поток одного хода
|
## Поток одного хода
|
||||||
|
|
||||||
@@ -131,7 +147,7 @@ in-process (никакого loopback-токена и user-identity):
|
|||||||
2. Воронка проверок (таблица выше); любой провал → выход без стрима.
|
2. Воронка проверок (таблица выше); любой провал → выход без стрима.
|
||||||
3. `getShareForPage(pageId)` — подтверждение принадлежности + резолв шары.
|
3. `getShareForPage(pageId)` — подтверждение принадлежности + резолв шары.
|
||||||
4. Сборка `forShare(shareId, workspaceId)` — 2–3 read-only тула, scope = дерево шары.
|
4. Сборка `forShare(shareId, workspaceId)` — 2–3 read-only тула, scope = дерево шары.
|
||||||
5. Запертый system-prompt + модель воркспейса → `streamText(stopWhen: stepCountIs(5))`.
|
5. Запертый system-prompt + **отдельная дешёвая модель** (`publicShareChatModel`, fallback на `chatModel`) → `streamText(stopWhen: stepCountIs(5))`.
|
||||||
6. Тулы при вызовах фильтруют по дереву шары (FTS-ветка `shareId`, `getShareForPage` для чтения).
|
6. Тулы при вызовах фильтруют по дереву шары (FTS-ветка `shareId`, `getShareForPage` для чтения).
|
||||||
7. Поток уходит клиенту; на сервере ничего не персистится.
|
7. Поток уходит клиенту; на сервере ничего не персистится.
|
||||||
|
|
||||||
@@ -154,6 +170,9 @@ in-process (никакого loopback-токена и user-identity):
|
|||||||
- Нет серверного хранения диалогов (эфемерно).
|
- Нет серверного хранения диалогов (эфемерно).
|
||||||
- Нет RAG/вектора — только share-scoped FTS.
|
- Нет RAG/вектора — только share-scoped FTS.
|
||||||
- Нет per-share гранулярности — один тумблер на воркспейс.
|
- Нет per-share гранулярности — один тумблер на воркспейс.
|
||||||
|
- **Нет голосового ввода / STT-диктовки** — только текстовый ввод (виджет не тянет
|
||||||
|
микрофонный путь внутреннего чата).
|
||||||
|
- Не основная модель агента — **отдельная дешёвая** `publicShareChatModel`.
|
||||||
|
|
||||||
## Развилки (зафиксированные решения)
|
## Развилки (зафиксированные решения)
|
||||||
|
|
||||||
@@ -163,6 +182,8 @@ in-process (никакого loopback-токена и user-identity):
|
|||||||
| Движок поиска | **Готовый share-scoped FTS** | share-scoped гибрид/RAG (`hybridSearchByPages`) — отложено |
|
| Движок поиска | **Готовый share-scoped FTS** | share-scoped гибрид/RAG (`hybridSearchByPages`) — отложено |
|
||||||
| Гейтинг | **Один тумблер воркспейса** | per-share флаг; тумблер + опт-ин на шару |
|
| Гейтинг | **Один тумблер воркспейса** | per-share флаг; тумблер + опт-ин на шару |
|
||||||
| Хранение диалогов | **Эфемерно** | отдельная таблица / nullable `creator_id` |
|
| Хранение диалогов | **Эфемерно** | отдельная таблица / nullable `creator_id` |
|
||||||
|
| Модель | **Отдельная дешёвая** (`publicShareChatModel`, fallback на `chatModel`) | основная модель чата воркспейса (дороже, незачем для read-only Q&A анонимов) |
|
||||||
|
| Голосовой ввод | **Не нужен** (только текст) | STT-диктовка как во внутреннем чате |
|
||||||
|
|
||||||
## Осталось решить (не блокирует)
|
## Осталось решить (не блокирует)
|
||||||
|
|
||||||
@@ -170,12 +191,16 @@ in-process (никакого loopback-токена и user-identity):
|
|||||||
сообщений в запросе, `stepCountIs` (старт 5).
|
сообщений в запросе, `stepCountIs` (старт 5).
|
||||||
- UX виджета: плавающая кнопка vs боковая панель vs блок под контентом.
|
- UX виджета: плавающая кнопка vs боковая панель vs блок под контентом.
|
||||||
- Финальная формулировка запертого промпта (персона + safety-блок).
|
- Финальная формулировка запертого промпта (персона + safety-блок).
|
||||||
|
- Дефолт/подсказка для `publicShareChatModel`: что предлагать админу как «дешёвую» модель
|
||||||
|
и поведение при пустом поле (сейчас — fallback на `chatModel`).
|
||||||
|
|
||||||
## Объём работ
|
## Объём работ
|
||||||
|
|
||||||
~2 новых серверных файла (controller + service) + tools-метод `forShare` + share-промпт +
|
~2 новых серверных файла (controller + service) + tools-метод `forShare` + share-промпт +
|
||||||
IP-троттлер + одно поле настройки и свитч в админке; на клиенте — виджет и лёгкий
|
IP-троттлер + два поля настройки (тумблер `publicShareAssistant` и модель
|
||||||
чат-компонент. **Без миграций БД.** Пользовательского агента не трогаем.
|
`publicShareChatModel`) и свитч + поле модели в админке + небольшой override id модели в
|
||||||
|
`getChatModel`; на клиенте — виджет и лёгкий чат-компонент (текстовый, без голосового ввода).
|
||||||
|
**Без миграций БД.** Пользовательского агента не трогаем.
|
||||||
|
|
||||||
## Возможные расширения (следующие итерации)
|
## Возможные расширения (следующие итерации)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user