30 KiB
Роли агента (Agent Roles) — проектный план
Статус: проработанная фича, не реализована. Контекст: gitmost — форк Docmost. Идея: дать возможность создавать переиспользуемые роли агента (например «Корректор» или «Факт-чекер, который ходит в веб и проверяет факты») и заводить чат, привязанный к выбранной роли. Роль задаёт поведение агента (системный промпт) и, опционально, модель.
Зафиксированные решения по объёму (см. раздел «Развилки»):
- Владение — только админские, общие на воркспейс роли (как провайдер и внешние MCP-серверы сегодня). Личных ролей в v1 нет.
- Гейтинг инструментов — нет. Роль меняет только инструкции и (опц.) модель; набор инструментов всегда полный (тот же, что у обычного чата). Ограничение возможностей по ролям отложено (см. «Возможные расширения»).
- Артефакт этого шага — только дизайн-документ; код не пишется.
Зачем это (и почему ложится в текущую архитектуру)
Сегодня у встроенного AI-агента нет понятия персоны/роли на уровне чата: вся настройка поведения — один системный промпт на весь воркспейс. Пользователь хочет заводить разные чаты под разные задачи (вычитка орфографии, проверка фактов по вебу и т. д.), каждый — со своей инструкцией и, возможно, своей моделью.
Три факта из текущего кода определяют дизайн (всё сверено по исходникам):
-
Системный промпт — только на уровне воркспейса. Собирается в ai-chat.prompt.ts, функция
buildSystemPrompt(), по слоям: базовая персона (workspace.settings.ai.provider.systemPromptлибоDEFAULT_PROMPT) → контекст (имя воркспейса, открытая страница) → несъёмныйSAFETY_FRAMEWORK. Персоны на чат сейчас нет — её надо добавить как ещё один слой. -
Инструменты — всегда все включены. В ai-chat.service.ts:
const tools = { ...external.tools, ...docmostTools }. ~40 Docmost-инструментов строитAiChatToolsService.forUser()(tools/ai-chat-tools.service.ts), внешние MCP-инструменты подмешиваетmcpClients.toolsFor(workspaceId)(external-mcp/mcp-clients.service.ts). Механизма включать подмножество инструментов нет — есть только CASL-проверка в момент вызова (через персональный loopback-токен). По зафиксированному решению этот механизм мы и не вводим — роль не трогает набор инструментов. -
Веб-доступ уже решён внешними MCP. Внешние MCP-серверы (
ai_mcp_servers, напр. Tavily) с SSRF-защитой (external-mcp/ssrf-guard.ts) и шифрованием заголовков — это и есть «факт-чекер ходит в гугл». Поскольку гейтинга нет, веб-инструменты уже доступны каждому чату, если админ подключил соответствующий MCP-сервер. Роль «Факт-чекер» работает чисто за счёт инструкции «проверяй факты по веб-источникам и цитируй ссылки» — она направляет модель пользоваться уже доступными инструментами, а не добавляет их. -
Чат создаётся неявно при первом сообщении: клиент (chat-thread.tsx) шлёт POST
/api/ai-chat/streamсchatId: null, сервер (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. Образец стиля —
20260617T130000-ai-mcp-servers.ts.
Миграция — таблица ролей + привязка чата:
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: креды (apiKey/baseUrl) берутся от провайдера соответствующего драйвера изai_provider_credentials, отдельные креды на роль не нужны.
Типы: добавить AiAgentRoles в db.interface.ts (или поднять через codegen),
role_id появится в AiChats автоматически после codegen.
Бэкенд
1. Слой инструкций роли в системном промпте
В ai-chat.prompt.ts добавить
вход roleInstructions в buildSystemPrompt(). Приоритет персоны:
effectivePersona = roleInstructions?.trim() || adminPrompt?.trim() || DEFAULT_PROMPT
return `${effectivePersona}${context}\n${SAFETY_FRAMEWORK}`
Ключевое: SAFETY_FRAMEWORK по-прежнему добавляется всегда и не отключается
ролью. Роль задаёт только персону; контекст (воркспейс, открытая страница) и
safety-блок остаются как есть.
Решение «роль заменяет, а не дополняет admin-промпт» выбрано намеренно: для узкой роли вроде «Корректора» нужно, чтобы её инструкция доминировала, а не конкурировала с общим промптом воркспейса. (Альтернатива «конкатенировать admin-промпт + роль» — в «Открытых вопросах».)
2. Применение роли в стриме
В 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)
получает опциональный аргумент override модели (паттерн из
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
управляет провайдером и MCP-серверами), но list должен быть доступен всем
участникам, иначе обычный пользователь не сможет выбрать роль при заведении
чата. Все запросы строго скоупятся по workspace_id (мультитенант по хосту).
Валидация при create/update: непустые name и instructions; если задан
model_config.driver — он из числа поддерживаемых (openai/gemini/ollama).
Клиент
1. Пикер роли при создании чата
В зоне «New chat» / композере
(ai-chat-window.tsx,
chat-input.tsx) —
селектор роли (Mantine Select/SegmentedControl), дефолт «Универсальный
ассистент» (без роли). Выбранный roleId хранится в новом Jotai-атоме рядом с
atoms/ai-chat-atom.ts
и уходит в теле первого запроса на /stream (расширить
prepareSendMessagesRequest в chat-thread.tsx: добавить roleId). После того
как сервер создал чат с ролью, пикер для этого чата фиксируется (роль чата
неизменна; смена роли = новый чат — простое и предсказуемое поведение для v1).
2. Бейдж роли
Показывать emoji+имя роли в шапке окна чата и в строке списка
(conversation-list.tsx),
чтобы было видно, «с кем» разговор. role_id/денормализованное имя+emoji роли
добавить в выдачу списка чатов и тип IAiChat
(types/ai-chat.types.ts).
3. Управление ролями в настройках
Новая секция «Роли агента» в Settings → AI (pages/settings/workspace/ai-settings.tsx), рядом с «External tools». Переиспользовать паттерн add/edit/delete-модалки из ai-mcp-servers.tsx. Форма роли: имя, emoji, описание, instructions (textarea — как редактор системного сообщения в ai-provider-settings.tsx), опциональный override модели. Подпись-напоминание под полем instructions: «встроенный safety-фреймворк добавляется автоматически» (как у системного сообщения).
4. Слой запросов
Новые TanStack Query хуки в
queries/ai-chat-query.ts
(или отдельный файл): useAiRolesQuery() (list), useCreate/Update/DeleteAiRoleMutation()
- функции в
services/ai-chat-service.ts.
Тип
IAiRoleзеркалит серверную схему.
Поток одного хода (с ролью)
- Создание чата: клиент шлёт первое сообщение +
roleId→/ai-chat/stream; сервер создаётai_chatsсrole_id. - Последующие ходы: сервер читает
role_idиз строки чата (не из тела запроса). - Резолв: загрузить роль (если не удалена) →
instructions+model_config. buildSystemPrompt({ workspace, adminPrompt, roleInstructions, openedPage })→ персона роли + контекст + несъёмныйSAFETY_FRAMEWORK.getChatModel(workspaceId, role.model_config)→ модель роли или дефолт.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) фичу не затрагивает — роли живут только во встроенном 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-промпт + роль |
| Снапшот конфигурации | Нет (правка вживую) | снапшот конфигурации роли на чат |
Открытые вопросы (не блокируют дизайн)
- Размещение CRUD-эндпоинтов и UI:
/ai-chat/roles(рядом с чатом) или/ai-settings/roles(рядом с MCP-серверами). Предлагаю в одном месте с MCP — там уже живут админские AI-настройки. - Поведение при удалении роли: soft-delete с сохранением инструкций для старых
чатов vs hard-delete +
SET NULL(старые чаты деградируют к универсальным). Предлагаю soft-delete (deleted_at) — консистентно сai_chats. - Override модели на ненастроенный драйвер: жёсткий 503 с внятным сообщением vs мягкий фолбэк на модель воркспейса. Предлагаю 503 (явность важнее).
- Стартовые пресеты: поставлять ли «Корректор» и «Факт-чекер» как преднастроенные роли-шаблоны (seed) при включении фичи, чтобы админ не писал инструкции с нуля. Предлагаю — да, как необязательный «вставить пример».
- Денормализация для бейджа: хранить имя/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. Набор инструментов агента не
трогаем.