← AI Foundation

Модель данных

ai-foundation · workzone

Дом данных AI-домена — системный prompt нейросетей, реестр AI-моделей (провайдеры · модели · назначение по функциям · списки чата и агентов · расходы) и каталог инструментов. Карта ниже — весь модуль одним взглядом, каждый узел ведёт к своей таблице. ORM-модели живут в общем ядре (core), Admin даёт CRUD и экраны; потребители — Harvester, Knowledge Store, Query Engine, Agent Engine, Auth — читают из core, без циклов. Глобальная конфигурация платформы (бюджеты AI) — в Admin Panel.

1Системный prompt
prompt_settingssingleton · safety + org
3Миграция
один логический шаг — создаёт реестр и засевает Platform-провайдера со встроенными моделями
1 Системный prompt Singleton. Override + слойная композиция. → Экран Prompt AI
prompt_settings системный prompt нейросетей
singleton · CHECK (id = 1)
id BigInteger PKCHECK всегда 1 · singleton
safety_text Text NULL override prompt безопасности · NULL = встроенный дефолт · → Дефолт в коде
org_text Text NULL override prompt организации · NULL = встроенный дефолт
updated_by BigInteger FK→usersNULL кто последним правил · SET NULL · след правки — в Audit Log
created_at DateTime(tz) DEFAULT server_default=now()
updated_at DateTime(tz) DEFAULT now() + trigger
Слойная композиция prompt → Тексты prompt → Экран Prompt AI
1 prompt безопасности admin · safety_text
2 prompt организации admin · org_text
3 инженерный function-prompt поверхность · чат · агент
4 рантайм-контекст фрагменты · история
= системный prompt каждого AI вызова · собран сверху вниз
Первые два слоя — единый платформенный слой: модель в пакете ai_foundation/models.py (конвенция кода — пакет на модуль), CRUD и экран — в Admin, а читают её потребители — Query Engine на шаге сборки prompt и Agent Engine в рамке агента. Третий слой свой у каждой поверхности (grounding-контракт чата, рамка задачи агента). Так общий тон и правила задаются один раз, а специфика поверхности остаётся в её инженерном prompt, который admin не правит — корректность RAG защищена.
Дефолт в коде, БД хранит только override
Встроенные тексты обоих блоков — seed-константы в коде, не строка в базе. NULL = взять дефолт (улучшается деплоем); непустой текст = правка админа, заморожённая под него. «Сбросить» зануляет колонку — возврат к актуальному дефолту. Дефолт локализован: seed-константы есть под каждую локаль платформы (platform_settings.locale) — locale=en даёт английский prompt, ru — русский; NULL берёт дефолт под текущую локаль. Так кастомизация переживает обновления платформы, а инсталляция из коробки несёт prompt без ручной настройки.
Безопасность и организация — раздельно; периметр не в prompt
Два независимых поля — само разделение делает правку защиты осознанной: меняя тон в org_text, его не спутать с safety_text. Сам текст — defense-in-depth поверх архитектуры, не периметр доступа; настоящую границу держит архитектура доступа, а не prompt — разбор и пометка v2 в Текстах prompt.
Ответ API — effective-текст плюс признак override
GET отдаёт по каждому блоку готовый к показу effective-текст (override, иначе встроенный дефолт под локаль) и флаг is_default — пуста ли колонка. По флагу экран Prompt AI отличает дефолт от правки: помечает «на дефолте» и держит «Сбросить» активной только при заданном override. Так фронт не угадывает источник текста сравнением строк, а PATCH с null — это сброс к дефолту.
2 Реестр AI-моделей Провайдеры → модели → назначение и расходы. → Экран AI-моделей
ai_providers подключения к моделям
ключ at-rest
id BigInteger PK
name Text NOT NULL отображаемое имя · «OpenAI» · «Ollama (on-prem)»
kind Text NOT NULLDEFAULTCHECK где крутится · cloud · local
adapter Text NOT NULLCHECK диалект API · решает SDK-клиент и discovery · openai · anthropic · google · ollama · openai_compatible · → Диалект API
base_url Text NULLCHECK endpoint · у cloud опционален, у local обязателен (CHECK)
api_key_enc Text NULL write-only · AES-256-GCM · NULL у локальных · → ключи
is_system Boolean NOT NULLDEFAULT встроенный рантайм, засеян миграцией · неудаляем, base_url/adapter под управлением · → Встроенный эмбеддер
status Text NOT NULLDEFAULTCHECK итог последней проверки · active · error · unchecked
last_check_at DateTime(tz) NULL NULL = не проверяли
created_at DateTime(tz) DEFAULT server_default=now()
updated_at DateTime(tz) DEFAULT now() + trigger
ai_models каталог моделей
FK → ai_providers UNIQUE(provider_id, model_id)
id BigInteger PK
provider_id BigInteger FK→ai_providersIDX CASCADE
model_id Text NOT NULLUNIQUE* идентификатор у провайдера · «gpt-4o»
display_name Text NOT NULL отображаемое имя · дефолт = model_id
model_type Text NOT NULLCHECK что выдаёт · chat · embedding · → тип решает назначение
origin Text NOT NULLCHECK как попала в каталог · discovered · manual · builtin (засеяна со встроенным рантаймом)
is_enabled Boolean NOT NULLDEFAULT включена в платформу · DEFAULT false
price_input Numeric NULL цена за input-токен · NULL = не задана (локальные) · → Цена вход/выход
price_output Numeric NULL цена за output-токен · NULL у embedding и локальных
meta JSONB NULL интринсики модели · у embedding: embedding_dim (задаёт halfvec(N) в KS), max_input_tokens, instruction_prefix · → Интринсики
created_at DateTime(tz) DEFAULT now()
updated_at DateTime(tz) DEFAULT now() + trigger
model_assignments системные функции
FK → ai_models UNIQUE(function)
id BigInteger PK
function Text NOT NULLUNIQUECHECK одна строка на функцию · системные: harvester_embedding · query_rag · → Словарь функций
model_id BigInteger FK→ai_modelsNULL назначенная модель · NULL = ждёт модуль · RESTRICT
created_at DateTime(tz) DEFAULT now()
updated_at DateTime(tz) DEFAULT now() + trigger
chat_models пользовательский чат
FK → ai_models ровно один default
id BigInteger PK
model_id BigInteger FK→ai_modelsUNIQUEIDX разрешённая chat-модель · RESTRICT — в списке = занята
is_default Boolean NOT NULLDEFAULT partial UNIQUE WHERE is_default — ровно один
created_at DateTime(tz) DEFAULT now()
updated_at DateTime(tz) DEFAULT now() + trigger
agent_models модели агентов
FK → ai_models ровно один default
id BigInteger PK — · agents.model_id ссылается сюда
model_id BigInteger FK→ai_modelsUNIQUEIDX разрешённая модель агентов · chat-тип, с поддержкой инструментов · RESTRICT — в списке = занята
is_default Boolean NOT NULLDEFAULT partial UNIQUE WHERE is_default — ровно один · преднабор в редакторе агента
created_at DateTime(tz) DEFAULT now()
updated_at DateTime(tz) DEFAULT now() + trigger
model_usage расходы · агрегат по дню
FK → ai_models UNIQUE(model_id, function, bucket_date)
id BigInteger PK
model_id BigInteger FK→ai_modelsIDX SET NULL · расход переживает удаление модели
function Text NOT NULLCHECK какая функция потребила · общий словарь · → Словарь функций
bucket_date Date NOT NULLUNIQUE* день агрегации
request_count BigInteger NOT NULLDEFAULT DEFAULT 0
input_tokens BigInteger NOT NULLDEFAULT токены на входе · DEFAULT 0
output_tokens BigInteger NOT NULLDEFAULT токены на выходе · 0 у embedding
cost Numeric NULL input·price_input + output·price_output · NULL если цены не заданы
created_at DateTime(tz) DEFAULT now()
updated_at DateTime(tz) DEFAULT now() + trigger · последний инкремент бакета
tools каталог инструментов AI
секрет at-rest UNIQUE(name)
id BigInteger PK
name Text NOT NULLUNIQUE ключ инструмента = ключ связи с типом в реестре · web_search · fetch_url · имя/описание — через t(), не в БД
source Text NOT NULLDEFAULTCHECK откуда схема · preset · custom (v1) · mcp · openapi (v2) · → Источники
access Text NOT NULLDEFAULTCHECK read_only (v1) · write (v2)
config JSONB NULL несекретная конфигурация · провайдер · base_url · опции · не секрет
credential_enc Text NULL write-only secret · AES-256-GCM крипто-ядром Auth · наружу лишь флаг is_set · NULL = ключ не нужен
chat_enabled Boolean NOT NULLDEFAULT подан в чат · DEFAULT false
agents_allowed Boolean NOT NULLDEFAULT допуск в allowlist агентов · DEFAULT false · владелец выбирает в редакторе
status Text NOT NULLDEFAULTCHECK итог проверки работоспособности · active · error · unchecked · как у ai_providers · → Проверка
last_check_at DateTime(tz) NULL NULL = не проверяли
created_at DateTime(tz) DEFAULT now()
updated_at DateTime(tz) DEFAULT now() + trigger
Инструмент — каталог в Admin, выбор агента — в Agent Engine → Каталог инструментов → Agent Engine
Инструмент — вызываемая функция, подаётся модели как tool-schema (не промпт). Каталог открытый — реестр типов (пресеты платформы + свой класс организации, зеркало коннекторов): список — живой реестр, эта таблица хранит состояние по name (нет строки = тип выключен, дефолты), строка заводится лениво. v1 засевает два read-only пресета — web_search и fetch_url, оба выключены. Допуск — две независимые поверхности: chat_enabled (виден чату) и agents_allowed (разрешён к выбору агентам); инструмент активен, пока подан хоть одной. Сам выбор агента — связь agent_tools в Agent Engine; рантайм фильтрует её по текущему agents_allowed, снятие → агент на ядре KS (мягкая деградация). Несекретная настройка (провайдер web_search, опции) — в config, правит админ на экране, не конфиг-файлом; секрет — в credential_enc, не в config: как ключи ai_providers и SMTP, наружу лишь флаг is_set. status / last_check_at держат итог проверки работоспособности — тем же приёмом, что ai_providers.
Модели в ai_foundation, CRUD и экраны — в Admin → Назначение по функциям
ORM-модели — в пакете ai_foundation/models.py (конвенция кода — пакет на модуль), не в admin: назначенные модели читают AI Harvester (embed), Knowledge Store (реэмбеддинг), Query Engine (RAG) и Agent Engine — четыре потребителя, не только Admin. Admin предоставляет CRUD и экраны; зависимости идут к core — без циклов.
Ключи — крипто-ядро Auth → Крипто-ядро
api_key_enc — write-only secret, шифротекст AES-256-GCM крипто-ядром Auth; наружу (API, экспорт) не возвращается, UI получает лишь маску ••••xxxx. У локальных провайдеров ключа обычно нет.
Диалект API ≠ где крутится → Экран провайдеров
Две ортогональные оси. kind (cloud / local) — где модель крутится: от него зависит, нужен ли ключ и обязателен ли base_url. adapter — на каком протоколе с ней говорить: он выбирает SDK-клиент и эндпоинт discovery (openaiGET /v1/models, ollamaGET /api/tags). openai_compatible покрывает vLLM · llama.cpp · прокси. Из name (отображаемое имя) протокол не выводится — потому отдельным полем.
Встроенный эмбеддер — из коробки, лениво, не по умолчанию → Назначение по функциям
Платформа везёт собственный локальный embeddings-рантайм — отдельный сервис docker-compose (OpenAI-совместимый, model-agnostic), поднимается на make up рядом с backend; контейнер лёгкий, ML-зависимости в API-образ не тянет. Веса при установке не качаются: рантайм тянет их лениво — по назначению модели (и сразу прогревает её, не ожидая первого запроса), в кеш-том. До выбора эмбеддер не занимает ни диска, ни памяти, и потому не лимитирует деплой. Миграция засевает is_system-провайдера Platform (неудаляем, base_url на внутренний сервис, без ключа) и каталог встроенных моделей — выбираемых (is_enabled=true), но не загруженных. Набор задаёт seed, не экраны (первая итерация — bge-m3 и Qwen3-Embedding-0.6B, обе 1024-мерные); макеты ссылаются на них лишь примером. Назначения по умолчанию нет: строки model_assignments.harvester_embedding на старте не существует, и Harvester без назначения приём не стартует — Admin обязан назначить embedding-модель (встроенную или свою) до первого приёма. Причина жёсткая: выбор фиксирует размерность N колонки chunks.embedding, а поздняя смена — массовый переэмбеддинг (при иной N — миграция схемы). Дефолт намеренно пуст: ни модель, ни её вес не навязываются инсталляцией. Как этот сервис держит потоки нагрузки и масштабируется (v1 → v2) — embeddings-рантайм.
Интринсики модели — в реестре, не в коде потребителя → Где живут параметры
Параметры, заданные самой моделью — размерность вектора (embedding_dim), потолок окна (max_input_tokens), обязательный префикс (instruction_prefix) — живут в meta модели, не в конфиге потребителя (no hardcoding): чанкер Harvester читает потолок оттуда, KS берёт embedding_dim назначенной embedding-модели и провизионит halfvec(N). Встроенные модели засеваются со своими интринсиками — bge-m3: { embedding_dim: 1024, max_input_tokens: 8192 }, Qwen3-Embedding-0.6B: { 1024, 32768 }. Размерность облачной модели (embedding_dim) discovery не возвращает, поэтому админ задаёт её при добавлении модели; модель без заданной размерности — или с размерностью, не совпадающей с выделенным halfvec(N), — отклоняется при назначении (409), ещё до старта переэмбеддинга.
Тип решает назначение
model_type (chat / embedding) ограничивает, на какие функции модель можно назначить: embedding — только в embedding-функции, chat — в чат, RAG, агентов. Смена типа и выключение модели заблокированы, пока она назначена какой-либо функции или состоит в списке чата либо агентов (FK RESTRICT на всех трёх таблицах): иначе назначение осталось бы с несовместимым или пустым типом, а список — без модели.
Назначение: системное vs списки → Agent Engine
Системная функция — одна строка model_assignments (одна модель). Чат и агенты — два списка одной формы: chat_models (сотрудник выбирает модель диалога) и agent_models (владелец выбирает модель агента), в каждом ровно один is_default и только модели типа chat. Дефолт защищён симметрично системным функциям: пометку is_default нельзя снять, не назначив другую, а модель-дефолт держится в списке через RESTRICT — список не остаётся без модели по умолчанию. Связь агента с выбором — agents.model_idagent_models (ON DELETE SET NULL): сняли модель из списка — ссылка агента обнуляется, и агент встаёт.
Цена и расход — раздельно вход/выход
У cloud-чата output-токен дороже input в разы, поэтому цена — два поля (price_input · price_output), а расход копит input_tokens и output_tokens раздельно. cost = input·price_input + output·price_output. У embedding выхода нет — price_output и output_tokens пусты / нули. Валюта пока единая (доллары), без отдельного поля — мультивалютность отложена.
Единый словарь функций
Набор AI-функций задан один раз (enum в core): harvester_embedding · query_rag · agent_engine · chat. model_assignments.function CHECK-ается подмножеством системных двух (harvester_embedding · query_rag) — чат и агенты назначаются не здесь, а списками chat_models / agent_models; model_usage.function копит расход по полному набору, включая chat и agent_engine. Один источник правды — расхождение исключено.
Расходы — агрегат; бюджет — настройка инстанса в Admin → platform_settings
model_usage копит requests / tokens / cost по (модель · функция · день), не event-log на каждый запрос. Порог бюджета — настройка инстанса (ai_monthly_budget + ai_budget_alert_enabled) и живёт не здесь, а в platform_settings, рядом с maintenance_mode: админ вводит сумму руками и включает тумблером (CHECK не даёт включить оповещение без суммы) на экране Расходы AI, в общем доме лимитов. При достижении порога поднимается уведомление типа Budget — канал держит модуль уведомлений. Панорама расхода за неделю / месяц / год на том же экране — производная SUM по периоду, без отдельной таблицы-счётчика. Как устроен учёт целиком — бизнес-логика расходов.
3 Миграция Создаёт реестр и заселяет встроенный рантайм.
alembic/versions/NNN_core_ai_models.py
Создаёт реестр AI одним логическим шагом в порядке зависимостей: ai_providersai_modelsmodel_assignments · chat_models · agent_models · model_usage · tools, а также singleton prompt_settings. Тем же шагом INSERT платформенного провайдера (is_system, name='Platform') и каталога встроенных моделей (origin='builtin') — это строки реестра, без весов и без назначений (встроенный эмбеддер). Остальной каталог наполняется при добавлении первого провайдера. downgrade() сносит таблицы в обратном порядке.