From 053a9c0d3f70874373ebb75cbee3a4c866e62c38 Mon Sep 17 00:00:00 2001 From: vvzvlad Date: Fri, 19 Jun 2026 16:25:21 +0300 Subject: [PATCH] docs(public-share): add model & voice input notes to public share plan docs: add AI agent roles plan documentation --- docs/ai-agent-roles-plan.md | 362 ++++++++++++++++++++++++++++ docs/public-share-assistant-plan.md | 39 ++- 2 files changed, 394 insertions(+), 7 deletions(-) create mode 100644 docs/ai-agent-roles-plan.md diff --git a/docs/ai-agent-roles-plan.md b/docs/ai-agent-roles-plan.md new file mode 100644 index 00000000..cc7707f9 --- /dev/null +++ b/docs/ai-agent-roles-plan.md @@ -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`. Набор инструментов агента не +трогаем.** diff --git a/docs/public-share-assistant-plan.md b/docs/public-share-assistant-plan.md index 511634a0..c14c90f9 100644 --- a/docs/public-share-assistant-plan.md +++ b/docs/public-share-assistant-plan.md @@ -8,7 +8,9 @@ > Зафиксированные решения по объёму (см. раздел «Развилки»): > область поиска — **всё дерево шары**; движок поиска — **готовый share-scoped FTS** > (ветка `shareId` в `SearchService`); гейтинг — **один тумблер воркспейса**; -> хранение диалогов — **эфемерное** (без БД, без миграций). +> хранение диалогов — **эфемерное** (без БД, без миграций); +> модель — **отдельная дешёвая** (не основная модель чата воркспейса); +> ввод — **только текст** (без голосового ввода / STT). ## Зачем это нетривиально @@ -48,7 +50,9 @@ - **Публичные роуты** в [share.controller.ts](../apps/server/src/core/share/share.controller.ts) уже `@Public()`, воркспейс резолвит `DomainMiddleware` по хосту; новый роут под `/api/shares/*` ложится туда же — **правок в [main.ts](../apps/server/src/main.ts) не нужно**. -- **Стриминг-плумбинг**: `AiService.getChatModel(workspaceId)` + +- **Стриминг-плумбинг**: `AiService.getChatModel(workspaceId)` (нужен небольшой апгрейд — + опциональный override id модели, чтобы для шары взять дешёвую `publicShareChatModel` + вместо основной `chatModel`; драйвер/`baseUrl`/`apiKey` те же) + `streamText` → `pipeUIMessageStreamToResponse` (как в [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`) — туда же, где живут остальные AI-настройки и тумблер MCP; читается/пишется через сервис AI-настроек (рядом с `ai-settings.service.ts`). В админке **Workspace settings → AI** — один свитч. Хелпер `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()`). Новые `public-share-chat.controller.ts` + `public-share-chat.service.ts` в модуле `ai-chat` (переиспользуют `AiService` и плумбинг `streamText`), зависят от `ShareRepo` / `PageRepo` / @@ -104,6 +118,8 @@ in-process (никакого loopback-токена и user-identity): опубликованной документации; ничего не можешь менять; если ответа в страницах нет — так и говоришь» + неизменяемый safety-блок по образцу [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) })`. **Без серверного хранения** — транскрипт держит клиент; доверять присланным сообщениям безопасно, т.к. scope обеспечивают тулы, а не транскрипт. Это снимает проблему @@ -123,7 +139,7 @@ in-process (никакого loopback-токена и user-identity): шлёт `{ shareId, pageId, messages }`, `credentials: 'omit'`. Эфемерный, in-memory — стрипнутая версия [chat-thread.tsx](../apps/client/src/features/ai-chat/components/chat-thread.tsx) без - списка чатов, истории и персистентности. + списка чатов, истории, персистентности и **голосового ввода** (только текстовое поле). ## Поток одного хода @@ -131,7 +147,7 @@ in-process (никакого loopback-токена и user-identity): 2. Воронка проверок (таблица выше); любой провал → выход без стрима. 3. `getShareForPage(pageId)` — подтверждение принадлежности + резолв шары. 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` для чтения). 7. Поток уходит клиенту; на сервере ничего не персистится. @@ -154,6 +170,9 @@ in-process (никакого loopback-токена и user-identity): - Нет серверного хранения диалогов (эфемерно). - Нет RAG/вектора — только share-scoped FTS. - Нет per-share гранулярности — один тумблер на воркспейс. +- **Нет голосового ввода / STT-диктовки** — только текстовый ввод (виджет не тянет + микрофонный путь внутреннего чата). +- Не основная модель агента — **отдельная дешёвая** `publicShareChatModel`. ## Развилки (зафиксированные решения) @@ -163,6 +182,8 @@ in-process (никакого loopback-токена и user-identity): | Движок поиска | **Готовый share-scoped FTS** | share-scoped гибрид/RAG (`hybridSearchByPages`) — отложено | | Гейтинг | **Один тумблер воркспейса** | per-share флаг; тумблер + опт-ин на шару | | Хранение диалогов | **Эфемерно** | отдельная таблица / nullable `creator_id` | +| Модель | **Отдельная дешёвая** (`publicShareChatModel`, fallback на `chatModel`) | основная модель чата воркспейса (дороже, незачем для read-only Q&A анонимов) | +| Голосовой ввод | **Не нужен** (только текст) | STT-диктовка как во внутреннем чате | ## Осталось решить (не блокирует) @@ -170,12 +191,16 @@ in-process (никакого loopback-токена и user-identity): сообщений в запросе, `stepCountIs` (старт 5). - UX виджета: плавающая кнопка vs боковая панель vs блок под контентом. - Финальная формулировка запертого промпта (персона + safety-блок). +- Дефолт/подсказка для `publicShareChatModel`: что предлагать админу как «дешёвую» модель + и поведение при пустом поле (сейчас — fallback на `chatModel`). ## Объём работ ~2 новых серверных файла (controller + service) + tools-метод `forShare` + share-промпт + -IP-троттлер + одно поле настройки и свитч в админке; на клиенте — виджет и лёгкий -чат-компонент. **Без миграций БД.** Пользовательского агента не трогаем. +IP-троттлер + два поля настройки (тумблер `publicShareAssistant` и модель +`publicShareChatModel`) и свитч + поле модели в админке + небольшой override id модели в +`getChatModel`; на клиенте — виджет и лёгкий чат-компонент (текстовый, без голосового ввода). +**Без миграций БД.** Пользовательского агента не трогаем. ## Возможные расширения (следующие итерации)