From 87ce969a6f432adee9507a491ecd56d05a7bcbd8 Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Sat, 20 Jun 2026 06:30:06 +0300 Subject: [PATCH] docs: remove implemented ai-agent-roles plan Co-Authored-By: Claude Opus 4.8 --- docs/ai-agent-roles-plan.md | 362 ------------------------------------ 1 file changed, 362 deletions(-) delete mode 100644 docs/ai-agent-roles-plan.md diff --git a/docs/ai-agent-roles-plan.md b/docs/ai-agent-roles-plan.md deleted file mode 100644 index cc7707f9..00000000 --- a/docs/ai-agent-roles-plan.md +++ /dev/null @@ -1,362 +0,0 @@ -# Роли агента (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`. Набор инструментов агента не -трогаем.**