Appearance
AI Router
Назначение
Центральный сервис работы с LLM в платформе Unishift. Маршрутизирует запросы к нескольким провайдерам (OpenAI, Anthropic, Google, Ollama и OpenAI-compatible эндпоинты вроде Yandex AI Studio), управляет чатами и knowledge base (RAG) на основе hybrid-поиска: PostgreSQL FTS + pgvector, слияние через Reciprocal Rank Fusion (RRF), опциональный LLM-rerank и переформулировка запросов.
Порты
- HTTP: 8101
- БД:
ai_router_db
Ключевые возможности
- Мультипровайдерность: OpenAI, Anthropic, Google, Ollama за единым API (Yandex AI Studio / Alice AI — через vendor
openai+ custombase_url) - Capabilities провайдера:
chat,embed,planner,diagram,fill - Sidecar-провайдеры llama-planner / llama-diagram / llama-fill — upsert при старте из
LLAMA_PLANNER_URL/LLAMA_DIAGRAM_URL/LLAMA_FILL_URL - Чаты с историей сообщений, стримингом ответа и reasoning (
thinking) - Function calling — вызов инструментов из чата (tool calls)
- Diagram-агент: инструменты draw.io / BPMN (
create_drawio,update_drawio, …) - RAG / Knowledge base:
- ingest с очисткой текста, чанкованием и эмбеддингами
- hybrid retrieval (FTS + vector) + RRF
- score threshold, LLM-rerank, query rewriting, multi-hop
- цитирование источников и anti-hallucination system prompt
- space-scoped ACL: scope
space-{uuid}привязан к workspace; поиск фильтруется по spaces, доступным текущему пользователю - автосинхронизация заметок (
documents→ RAG) через NATS
- CRUD провайдеров для администраторов (модели, API-ключи, base URL)
- Учёт вызовов: сохранение истории, токенов, длительности и статистики по пользователю
- Двойная аутентификация: JWT (пользователи) и Service Token (сервисы)
- Ролевая модель: управление провайдерами требует роль
admin - Публикация NATS-событий: стриминг чанков, статусы обработки, tool-call'ы
- Swagger UI, health checks
RAG / Knowledge base
Пайплайн
Ingest Retrieval Generation
─────── ───────── ──────────
clean text ──► chunk ──► rewrite query ──► system prompt
embed ──► store FTS + vector ──► RRF ──► (anti-hallucination
(pgvector + tsvector) [rerank] ──► [multi-hop] + cite sources)- Ingest — документ чистится (HTML/шум), режется на чанки с overlap, для каждого чанка считаются эмбеддинги и заполняется
content_tsv(FTS). - Retrieval — сырой вопрос опционально переписывается LLM; параллельно ищутся кандидаты по FTS и по cosine similarity; списки сливаются RRF; при включённом rerank LLM оставляет top-N; при слабом recall — второй hop.
- Generation — в чат с привязанными scopes подмешивается жёсткий system prompt («отвечай только по документам», «цитируй Source»), tool
search_knowledge_baseвозвращает чанки в формате с заголовком источника.
Ingest
POST /api/v1/rag/documents
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
scope | string | да | Область знаний (space-{uuid} или legacy id) |
title | string | да | Заголовок документа (попадает в метаданные чанков) |
content | string | да | Текст / markdown / HTML |
content_type | string | нет | text (default), markdown или html |
source_id | string | нет | Внешний id (UUID заметки) — upsert по (scope, source_id) |
space_id | uuid | нет | Workspace id (должен совпадать с space-* scope) |
metadata | object | нет | Доп. поля чанков: category, author, date, … |
При ingest:
- upsert: если задан
source_id— по(scope, source_id); иначе по(scope, title)(legacy). В ответеupdated: trueпри обновлении.document_countувеличивается только при создании. - для
space-*scopes требуется доступ пользователя к соответствующему space (через spaces API +SERVICE_TOKEN) - HTML/script/style вырезаются, whitespace нормализуется
markdownрежется по заголовкам#;htmlпарсится в markdown-ish (заголовки/списки) и тоже режется по заголовкам;text— по абзацам. Крупные секции дополнительно режутся поRAG_CHUNK_MAX_SIZEс overlap.- в metadata чанка всегда пишутся
title,content_type,scope,ingested_at - FTS-индекс (
content_tsv) обновляется триггером БД - векторы пишутся в
document_chunk_embeddings_{dim}(не в колонку чанка), dim берётся из embed-провайдера, привязанного к scope
Embeddings (multi-dim + per-scope)
Разные модели эмбеддингов несовместимы. Поддерживаемые размерности: 256 / 768 / 1536 — отдельные таблицы document_chunk_embeddings_{256|768|1536} + HNSW.
| Сущность | Поля |
|---|---|
Embed-провайдер (type=embed) | embedding_dim, default_model (doc), query_model (search), is_default (пресет для новых scopes) |
| Scope | embedding_provider_id, embedding_dim (immutable после create) |
Правила:
- при создании scope выбирается embed (или берётся
is_default); embedding_dimкопируется с провайдера и не меняется;- сменить провайдера на scope можно только с той же dim → авто-reindex;
- ingest использует
default_model; search —query_model||default_model; - поиск по нескольким scopes группирует их по провайдеру и сливает через RRF с FTS.
POST /rag/scopes/:scope/reindex — пересчитать векторы текущим провайдером scope.
Yandex embeddings
Отдельного vendor нет: vendor=openai. base_url можно оставить OpenAI-совместимым (https://ai.api.cloud.yandex.net/v1) — для эмбеддингов ai-router всё равно ходит в нативный https://llm.api.cloud.yandex.net/foundationModels/v1/textEmbedding (modelUri + text, заголовок Api-Key для ключей AQVN…).
| Поле | Значение |
|---|---|
embedding_dim | 256 |
default_model | emb://<folder_id>/text-search-doc/latest |
query_model | emb://<folder_id>/text-search-query/latest |
Preview /models у Yandex часто отдаёт 404 — UI позволяет ввести URI вручную. Без query_model поиск деградирует (query и documents в разных пространствах Yandex).
Проверьте, что скоуп (например tt) привязан именно к этому embed-провайдеру, а не к чатовому. Ingest берёт провайдера из настроек скоупа.
Space scopes и ACL
Конвенция имени: space-{spaceUUID}.
GET /rag/scopesвозвращает:- все
space-*для spaces, доступных пользователю (owner или shared); - админам — все существующие scopes (общие и space);
- общие (не
space-*) scopes, пошаренные пользователю (shared_user_ids); - legacy scopes, в которых у пользователя есть собственные документы.
- все
- Space scopes auto-provision при list (alias = имя space).
- Общую базу можно открыть выбранным пользователям через
shared_user_idsнаPUT /rag/scopes/:scope. Доступ к space-базам по-прежнему через ACL пространства. SearchKnowledgeBase/ toolsearch_knowledge_baseпринимаютuser_idна бэкенде и отбрасывают недоступные space scopes. Пошаренные общие scopes ищутся целиком (без фильтраowner_id). Legacy scopes без шаринга — только среди документов сowner_id = user.- AI-чат в Notes подставляет
defaultScopes = ["space-{currentSpaceId}"].
Синхронизация заметок
Сервис documents публикует NATS-события:
| Subject | Когда |
|---|---|
documents.created | создание / duplicate |
documents.updated | patch |
documents.deleted | soft/hard delete |
ai-router подписан и делает trusted ingest / delete по (scope=space-{spaceID}, source_id=documentID).
Retrieval
Поиск вызывается tool'ом search_knowledge_base (чат с scopes) или через usecase SearchKnowledgeBase(userID, …).
| Шаг | Описание | Конфиг |
|---|---|---|
| ACL filter | пересечение запрошенных scopes с доступными spaces / шарингом / admin | — |
| Query rewrite | LLM исправляет опечатки / раскрывает аббревиатуры | RAG_QUERY_REWRITE_ENABLED |
| FTS | websearch_to_tsquery('simple') + ts_rank_cd | — |
| Vector | cosine distance (<=>), отсев по similarity | RAG_VECTOR_MIN_SCORE |
| RRF | score = Σ 1/(k + rank) | RAG_RRF_CONSTANT, RAG_CANDIDATE_LIMIT |
| Rerank | LLM listwise ranking top-кандидатов → top-N | RAG_RERANK_ENABLED |
| Multi-hop | повторный поиск с другим rewrite при слабом recall | RAG_MULTI_HOP_ENABLED |
Формат результата для LLM:
Source: «FAQ» [category=support, author=team]
текст чанка…Scopes
| Метод | Путь | Описание |
|---|---|---|
GET | /rag/scopes | Список scopes, доступных текущему пользователю (админ — все) |
GET | /rag/scopes/:scope | Настройки одного scope (403 без доступа) |
PUT | /rag/scopes/:scope | Upsert alias / description / system_prompt / default_origin / embedding_provider_id / shared_user_ids |
POST | /rag/scopes/:scope/reindex | Пересчитать embeddings scope |
DELETE | /rag/scopes/:scope | Удалить документы и чанки scope |
DELETE | /rag/scopes/:scope/settings | Удалить только настройки scope |
system_prompt scope дополняет дефолтный anti-hallucination prompt, а не заменяет его.
Конфигурация
| Переменная | По умолчанию | Описание |
|---|---|---|
PORT | 8101 | Порт HTTP-сервера |
DB_* | — | PostgreSQL (DB_NAME=ai_router_db) |
JWT_SECRET | — | Валидация JWT |
SERVICE_TOKEN | — | Межсервисный токен (ACL через spaces, sync) |
AI_DEFAULT_PROVIDER | openai | Провайдер по умолчанию |
AI_REQUEST_TIMEOUT | 120s | Timeout запросов к LLM |
AI_MAX_RETRIES | 3 | Число повторов при ошибке |
RAG_CANDIDATE_LIMIT | 20 | Сколько кандидатов брать из FTS и из vector до RRF |
RAG_DEFAULT_LIMIT | 5 | Лимит результатов по умолчанию |
RAG_VECTOR_MIN_SCORE | 0.2 | Мин. cosine similarity (0 = без порога) |
RAG_RRF_CONSTANT | 60 | Константа k в формуле RRF |
RAG_RERANK_ENABLED | true | LLM-rerank кандидатов после RRF |
RAG_QUERY_REWRITE_ENABLED | true | Переформулировка запроса перед поиском |
RAG_MULTI_HOP_ENABLED | true | Повторный поиск при слабом recall |
RAG_CHUNK_MAX_SIZE | 2000 | Размер чанка при ingest (символы) |
RAG_CHUNK_OVERLAP | 200 | Overlap чанков (символы) |
MESSAGING_URL | nats://localhost:4222 | NATS URL |
SERVER_WRITE_TIMEOUT | 300s | Write timeout (длинный под стриминг) |
Для снижения latency на retrieval можно отключить тяжёлые шаги:
bash
RAG_RERANK_ENABLED=false
RAG_QUERY_REWRITE_ENABLED=false
RAG_MULTI_HOP_ENABLED=falseAPI (кратко)
RAG (/api/v1/rag)
| Метод | Путь | Описание |
|---|---|---|
POST | /rag/documents | Ingest документа |
GET | /rag/scopes | Список scopes |
GET | /rag/scopes/:scope | Настройки scope |
PUT | /rag/scopes/:scope | Upsert настроек scope |
POST | /rag/scopes/:scope/reindex | Reindex embeddings scope |
DELETE | /rag/scopes/:scope | Удалить документы scope |
DELETE | /rag/scopes/:scope/settings | Удалить настройки scope |
Полный список эндпоинтов (провайдеры, чаты, completions) — в Swagger UI: http://localhost:8101/swagger/index.html.
Function calling / Tools
Инструменты регистрируются в internal/infrastructure/tools и подключаются через ToolRegistry в container. Клиент передаёт имена в tools[] на send message; при наличии scopes автоматически добавляется search_knowledge_base.
| Tool | Side effects | Назначение |
|---|---|---|
run_command | command-manager: create job | Запуск команды по имени/UUID |
search_commands | read-only | Поиск команд перед запуском |
get_job_status / get_job_logs / stop_job | read / command-manager | Статус, логи, стоп job |
run_flow | flow-manager: test pipeline | Запуск/тест процесса |
write_starlark_code / test_starlark_code | none / TryCodeNode | Scaffold и прогон Starlark |
create_task / update_task / search_tasks / get_task / change_task_status | task-tracker | Задачи в space |
create_note / update_note / search_notes / get_note | documents | Заметки |
create_drawio / update_drawio / read_drawio | documents (draw.io) | Диаграммы; capability diagram |
create_bpmn / update_bpmn / read_bpmn | documents (BPMN) | Процессные схемы |
search_media / get_media_file | spark-grid | Медиатека пространства |
search_tables / get_table / create_table / *_table_rows | data-tables | Таблицы в space |
search_env / create_env / update_env | env-manager | Секреты и конфиги |
web_search / fetch_url | SearXNG / HTTP | Поиск в вебе и загрузка URL |
update_command_draft / update_note_draft / update_flow_draft / update_skill_draft | none (UI patch) | Черновики в открытый редактор |
search_knowledge_base | RAG read | Поиск по scopes (авто при scopes) |
ask_user | UI clarify | Уточняющий вопрос пользователю |
UI-patch tools (update_*_draft) возвращают JSON; фронт слушает NATS ai.chat.tool.call / ai.chat.tool.result и применяет поля в открытый редактор.
Sidecar GGUF (ush platform enable llama-planner|llama-diagram|llama-fill) поднимает OpenAI-compatible /v1; ai-router при старте пишет провайдеров unishift-llama-planner / -diagram / -fill (capability = тип sidecar). Подробнее: Локальный LLM.
Starlark
См. starlark-api.md — модуль up.ai_router (провайдеры, чаты, ingest_document с metadata, scopes).