Files
gitmost/docs/ai-agent-roles-plan.md
vvzvlad 053a9c0d3f docs(public-share): add model & voice input notes to public share plan
docs: add AI agent roles plan documentation
2026-06-19 16:25:21 +03:00

30 KiB

Роли агента (Agent Roles) — проектный план

Статус: проработанная фича, не реализована. Контекст: gitmost — форк Docmost. Идея: дать возможность создавать переиспользуемые роли агента (например «Корректор» или «Факт-чекер, который ходит в веб и проверяет факты») и заводить чат, привязанный к выбранной роли. Роль задаёт поведение агента (системный промпт) и, опционально, модель.

Зафиксированные решения по объёму (см. раздел «Развилки»):

  • Владение — только админские, общие на воркспейс роли (как провайдер и внешние MCP-серверы сегодня). Личных ролей в v1 нет.
  • Гейтинг инструментовнет. Роль меняет только инструкции и (опц.) модель; набор инструментов всегда полный (тот же, что у обычного чата). Ограничение возможностей по ролям отложено (см. «Возможные расширения»).
  • Артефакт этого шага — только дизайн-документ; код не пишется.

Зачем это (и почему ложится в текущую архитектуру)

Сегодня у встроенного AI-агента нет понятия персоны/роли на уровне чата: вся настройка поведения — один системный промпт на весь воркспейс. Пользователь хочет заводить разные чаты под разные задачи (вычитка орфографии, проверка фактов по вебу и т. д.), каждый — со своей инструкцией и, возможно, своей моделью.

Три факта из текущего кода определяют дизайн (всё сверено по исходникам):

  1. Системный промпт — только на уровне воркспейса. Собирается в ai-chat.prompt.ts, функция buildSystemPrompt(), по слоям: базовая персона (workspace.settings.ai.provider.systemPrompt либо DEFAULT_PROMPT) → контекст (имя воркспейса, открытая страница) → несъёмный SAFETY_FRAMEWORK. Персоны на чат сейчас нет — её надо добавить как ещё один слой.

  2. Инструменты — всегда все включены. В 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-токен). По зафиксированному решению этот механизм мы и не вводим — роль не трогает набор инструментов.

  3. Веб-доступ уже решён внешними MCP. Внешние MCP-серверы (ai_mcp_servers, напр. Tavily) с SSRF-защитой (external-mcp/ssrf-guard.ts) и шифрованием заголовков — это и есть «факт-чекер ходит в гугл». Поскольку гейтинга нет, веб-инструменты уже доступны каждому чату, если админ подключил соответствующий MCP-сервер. Роль «Факт-чекер» работает чисто за счёт инструкции «проверяй факты по веб-источникам и цитируй ссылки» — она направляет модель пользоваться уже доступными инструментами, а не добавляет их.

  4. Чат создаётся неявно при первом сообщении: клиент (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()

Поток одного хода (с ролью)

  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) фичу не затрагивает — роли живут только во встроенном 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. Набор инструментов агента не трогаем.