docs(public-share): add model & voice input notes to public share plan

docs: add AI agent roles plan documentation
This commit is contained in:
vvzvlad
2026-06-19 16:25:21 +03:00
parent 3d03417c73
commit 053a9c0d3f
2 changed files with 394 additions and 7 deletions

362
docs/ai-agent-roles-plan.md Normal file
View 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`. Набор инструментов агента не
трогаем.**

View File

@@ -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`; на клиенте — виджет и лёгкий чат-компонент (текстовый, без голосового ввода).
**Без миграций БД.** Пользовательского агента не трогаем.
## Возможные расширения (следующие итерации)