Перейти к содержанию

Резолверы кода

Бизнес-логика на Python или Node.

Если вы писали сервер GraphQL, этот шаблон вам знаком. Резолвер кода — это функция, которая отвечает за один тип объекта: в GraphQL функция с тегом @table и есть resolver для типа своей таблицы. Provisa вызывает её с аргументами запроса и управляет тем, что она возвращает.

Чем это отличается от resolver, написанного вручную:

  • Под управлением. Видимость, маски и правила строк применяются к строкам, которые возвращает функция, как и для любой таблицы.
  • Пакетами. Функция с параметром loader получает все ключи соединения за один вызов, по шаблону DataLoader.
  • С передачей фильтров. Фильтр по столбцу доходит до функции как параметр, если вы этого просите.
  • Везде. Одна и та же функция отвечает на GraphQL, SQL (через HTTP, pgwire и Arrow Flight), Cypher (через HTTP и Bolt), gRPC, REST, JSON:API и MCP.

Резолверы кода — один из трёх способов писать бизнес-логику в Provisa. См. Бизнес-логика в Provisa.

Первый resolver

from dataclasses import dataclass


@dataclass
class Region:
    code: str
    name: str


def regions() -> list[Region]:
    """Sales regions.

    @table
    """
    return [
        Region(code="emea", name="Europe, Middle East and Africa"),
        Region(code="amer", name="Americas"),
        Region(code="apac", name="Asia Pacific"),
    ]

Чтобы его опубликовать:

  1. Добавьте источник python и укажите на код: пакет PyPI, коммит Git, загрузку или папку (см. Откуда берётся код).
  2. Сохраните его. Provisa устанавливает код, читает его и предлагает regions как таблицу со столбцами code и name.
  3. Стюард регистрирует таблицу в домене, здесь sales, и выдаёт к ней доступ, как к любой другой.

Затем читайте её в GraphQL:

{
  regions {
    code
    name
  }
}

Или в SQL:

SELECT code, name
FROM sales.regions

Node работает так же, с тегами JSDoc и типами TypeScript: см. Node.

Сборка

Чем становится функция

Provisa читает объявления вашего кода: docstring и аннотации типов в Python, теги JSDoc и типы TypeScript в Node.

Тег Становится
@table Таблицей. Функция возвращает список или итерируемый объект, и каждый элемент — это строка.
@command Командой, выполняемой по запросу.
@stream Живой таблицей. Функция — генератор, и каждая строка отправляется сразу, как только выдана.
@columns Преобразованием столбцов: на вход пакет строк, на выход по одному результату на строку. См. Преобразования столбцов.
  • Функция без тега игнорируется.
  • Тег, который Provisa не знает, отклоняется, как и тип, который нельзя сопоставить. Отказ перечисляет все проблемы сразу, чтобы вы исправили их за один проход.

Тип строки задаёт столбцы:

  • Текст, числа, логические значения, даты и метки времени сопоставляются со своими типами столбцов. Списки остаются списками, а вложенные объекты становятся именованными типами.
  • В Python тип строки — это dataclass, TypedDict, NamedTuple, модель Pydantic или любой класс с аннотированными полями. Строки модели Pydantic отправляются в том виде, в каком их отдаёт model_dump(by_alias=True), поэтому поле с псевдонимом становится столбцом с именем псевдонима.
  • В TypeScript тип строки — это интерфейс, псевдоним типа или класс.
  • Собственная документация поля становится описанием его столбца при регистрации таблицы: Field(description=...) в Pydantic или документирующий комментарий у свойства TypeScript.
  • У нестрогого типа строки (Any или dict в Python, any, unknown или Record в TypeScript) нет столбцов, которые можно прочитать. Provisa один раз вызывает функцию без аргументов и предлагает столбцы по возвращённым строкам. Если у функции есть обязательный параметр, так её не вызвать, и типы столбцов вы указываете сами при регистрации таблицы.

Параметры

Параметры функции становятся параметрами её таблицы. Эта оценивает один аккаунт:

@dataclass
class Risk:
    score: float
    reasons: list[str]


def churn_risk(account_id: str) -> list[Risk]:
    """Churn risk of one account.

    @table
    """
    return [Risk(score=0.82, reasons=["no logins in 30 days"])]

В GraphQL параметр — это аргумент поля:

{
  churnRisk(account_id: "A-1042") {
    score
    reasons
  }
}

В SQL это условие _nf_:

SELECT score, reasons
FROM accounts.churn_risk
WHERE _nf_account_id = 'A-1042'
  • Параметр без значения по умолчанию обязателен. Чтение, в котором он пропущен, отклоняется до запуска вашего кода.
  • Параметр со значением по умолчанию необязателен. Пропустите его, и применится ваше значение по умолчанию.
  • Стюард всё равно может сделать необязательный параметр обязательным.

Что получает ваша функция

Вызов получает аргументы чтения и больше ничего из запроса. Значение каждого параметра приходит из аргумента поля, условия _nf_, переданного ему фильтра или ключей join. Ваша функция никогда не видит, кто спрашивает: личность и роли вызывающего остаются у Provisa, которая управляет строками, возвращёнными функцией.

На этом строится передача фильтров. Фильтр в запросе становится значением параметра, поэтому функция может получить только запрошенные строки.

Передача фильтров

Фильтр по одному из возвращаемых столбцов может дойти до функции как аргумент, чтобы она получала только запрошенные строки. Укажите, какой параметр его принимает, одним из трёх способов:

  • По имени. Параметр, названный точно как возвращаемый столбец, получает равенство по этому столбцу. Параметр с именем <op>_<column> получает этот оператор, например gte_score или in_region.
  • По тегу. Строка @<op>_<column> <parameter> (или @<column> <parameter>) в docstring или JSDoc либо встроенный /* @<op>_<column> */ перед параметром TypeScript.
  • Через Annotated. В Python — строка "<op>_<column>" (или "<column>") в аннотации типа параметра.

Операторы:

op Принимает
ne, gt, gte, lt, lte Значение типа столбца
in Список значений типа столбца
like Текст, для текстового столбца
  • Диапазон по столбцу заполняет оба его параметра: score BETWEEN 0.5 AND 0.9 даёт gte_score и lte_score.
  • Имена столбцов сравниваются точно так, как вы объявили их в типе строки. Имя, которое совпадает только после того, как Provisa переименует столбец для читателя, совпадением не считается.
  • Объявление, которое типы функции не могут принять, отклоняется.
  • Фильтр всё равно применяется к строкам, которые возвращает функция, поэтому функция, игнорирующая или ослабляющая переданное значение, не может вернуть неверные строки.

Пакетные ключи: параметр loader

Соединение может дойти до таблицы кода, и тогда её параметры задают строки другой таблицы: например, клиенты, соединённые со своей оценкой оттока. По умолчанию Provisa вызывает функцию один раз для каждого отдельного набора аргументов, который дают строки соединения, и отправляет все эти вызовы worker одним запросом.

Функция с параметром loader вызывается вместо этого один раз на пакет — шаблон, который сделал известным DataLoader из GraphQL.

  • Что она получает. loader — это хеш: dict (в TypeScript — объект) от стабильного хеша каждого ключа к самому ключу. Ключ — это объект JSON со значениями строк соединения для остальных параметров функции.
  • Без повторов. Каждое отдельное сочетание значений встречается один раз.
  • Разреженный. Поле без значения в ключ не попадает, поэтому применяется собственное значение функции по умолчанию, как при одиночном вызове.
  • Что она возвращает. Хеш от каждого полученного ключа к строкам этого ключа: обычным строкам таблицы, ни одной или многим. Provisa передаёт строки каждого ключа каждой строке соединения с этим ключом.
  • Сопоставление по хешу. Функция заполняет недостающие поля разреженного ключа значениями по умолчанию, поэтому значения, которые она видит, отличаются от значений строки соединения. Хеш вычисляет Provisa, а функция лишь возвращает полученные ключи.
  • Типизация как обычно. Функция по-прежнему объявляет только тип строки, список строк.
  • Нужны значения по умолчанию. Её параметрам рядом с loader нужны значения по умолчанию, потому что вызов с loader передаёт только loader.
  • Размер пакета. Ограничьте его, как maxBatchSize в DataLoader, записью loader N в одном из трёх мест: строкой @loader 100 рядом с @table, комментарием /* @loader 100 */ перед параметром loader в TypeScript или строкой Annotated в Python, loader: Annotated[dict[str, CustomerKey] | None, "loader 100"] = None. Provisa тогда делит ключи на пакеты этого размера и объединяет ответы по хешу. Без размера один пакет содержит все ключи.
  • Отклоняется. Размер у функции без параметра loader, размер, не являющийся положительным целым, и два размера у одной функции.

Используйте loader, когда один вызов по многим ключам выгоднее многих вызовов по одному:

  • Оценка модели по целому пакету за один векторизованный проход.
  • Объединение запросов к API или языковой модели, которые принимают много входов сразу.
  • Подготовка один раз на пакет, например загрузка модели или открытие соединения.
from dataclasses import dataclass
from typing import TypedDict


class CustomerKey(TypedDict, total=False):
    customer_id: str
    plan: str
    region: str


@dataclass
class Score:
    customer_id: str
    score: float


def churn_score(
    customer_id: str | None = None,
    plan: str = "basic",
    region: str = "US",
    loader: dict[str, CustomerKey] | None = None,
) -> list[Score]:
    """Churn score of each customer in the batch.

    @table
    """
    keys = loader or {"": {"customer_id": customer_id, "plan": plan, "region": region}}
    rows = {h: {"plan": "basic", "region": "US", **key} for h, key in keys.items()}
    scores = MODEL.predict(features(list(rows.values())))  # one vectorized call for the batch
    out = {h: [Score(r["customer_id"], s)] for (h, r), s in zip(rows.items(), scores)}
    return out if loader else out[""]  # a loader call returns the hash of key to rows

При соединении с таблицей клиентов она вызывается с loader вида {"9f2c...": {"customer_id": "637", "plan": "pro", "region": "EU"}, "41ab...": {"customer_id": "638"}, ...}. Второй ключ содержит только customer_id, поэтому тариф и регион берут значения по умолчанию. Функция возвращает {"9f2c...": [...], "41ab...": [...], ...}.

Преобразования столбцов: @columns

Функция с тегом @columns вычисляет новые столбцы из столбцов уже имеющихся у вас строк: разбивает адрес на части, классифицирует описание или обогащает имя. Она принимает пакет строк и возвращает по одному результату на строку в том же порядке.

Каждый параметр — это один входной столбец, переданный как список значений этого пакета в порядке строк. Функция возвращает список с одним объектом на каждую входную строку, и поля объекта — это выходные столбцы. На Python:

@dataclass
class Address:
    street: str
    city: str
    country: str


def clean_address(raw_address: list[str]) -> list[Address]:
    """Each raw address of the batch, as its parts.

    @columns
    """
    ...

На TypeScript объявите её как clean_address(raw_address: string[]): Promise<Address[]> с @columns в её JSDoc.

Вы регистрируете её как команду, по одной, как любую другую: на странице Commands выберите источник, и его функции @columns будут предложены для выбора. Объявление функции задаёт входные и выходные столбцы. Зарегистрируйте её в домене с ролями, которым разрешено её использовать.

Примените её в списке выборки, и её выходные столбцы займут место вызова:

SELECT id, clean_address(raw_address) FROM suppliers

Это возвращает id, street, city, country. Переименуйте выходы во внешнем запросе, если нужны другие имена. Вызов должен быть элементом собственного списка выборки запроса: Provisa выполняет функцию над строками после их чтения, поэтому вызов в WHERE, JOIN, GROUP BY или внутри другого выражения отклоняется, как и псевдоним на вызове. Каждый выходной столбец — именованный шаг происхождения данных проекции.

Строки поступают в функцию пакетами по 1 000. Функция, не хранящая состояние между вызовами, позволяет пакетам выполняться параллельно, распределяясь по worker источника: задайте Workers в форме источника (workers в его mapping, см. Режим dev и worker), чтобы запустить больше одного, и каждый добавленный worker увеличивает пропускную способность. Пакет, на который получено число результатов, отличное от числа переданных строк, отклоняется с указанием функции.

Функция может вызывать сервисы Provisa, в том числе сервис LLM, так что один вызов модели может очистить, классифицировать или обогатить целый пакет. Каждый вызов учитывается и ограничивается в рамках источника, как любой другой его вызов (Вызов модели).

Представление, применяющее преобразование столбцов, сохраняется на диск, объявите вы его материализованным представлением или нет: Provisa строит его так же, как материализованное представление, вызывая функцию пакетами над строками представления и сохраняя результат, и представление читается из этой сохранённой таблицы. Запрос затем может фильтровать, соединять или группировать по выходным столбцам, как по любым другим. Выходы пересчитываются при каждом обновлении представления, как объявляет его свежесть. Собственного расписания у них нет.

Откуда берётся код

  • Пакет. Пакет npm для Node или пакет PyPI для Python, в точной версии.
  • Репозиторий Git. Полный 40-символьный коммит и подкаталог, если код лежит не в корне.
  • Загрузка. Архив .zip, .tgz или .tar.gz с кодом.
  • Папка на вашем компьютере. Выбрать папку упаковывает папку в zip прямо в браузере, без node_modules, .venv, __pycache__ и .git, и загружает её как архив источника. В браузере, который может сохранить доступ к папке (Chrome, Edge), Сохранить упаковывает и отправляет её снова; другой браузер снова попросит выбрать папку.
  • Каталог на сервере. В установке с одной организацией — каталог внутри тех, что разрешил оператор. Он читается на месте, поэтому изменение увидит следующий запрос. Сервер с многими организациями отклоняет его.

Все источники кода, кроме каталога на сервере, закреплены, поэтому источник выполняет ровно то, что было сохранено.

  • Модуль входа. Если в коде Python больше одного модуля верхнего уровня, укажите, через какой входить.
  • Открыть в VS Code. Каталог на сервере открывается в установленном VS Code; источник Git открывается на своём коммите в vscode.dev. У пакета или загрузки такой ссылки нет.

Установка зависимостей

Коду с зависимостями нужен lock-файл, и каждая установка следует ему в точности. Без него пакет, объявляющий зависимости, отклоняется, и отказ предлагает добавить lock-файл.

Среда выполнения Lock-файл
Node.js package-lock.json или npm-shrinkwrap.json
Bun bun.lock или bun.lockb
Deno deno.lock
Python uv.lock (со своим pyproject.toml), pylock.toml или requirements.txt с --hash в каждой строке
  • Свой или переданный. Код со своим lock-файлом устанавливается по нему: репозиторий Git, загрузка или папка с lock-файлом либо пакет npm, автор которого опубликовал npm-shrinkwrap.json (npm никогда не публикует package-lock.json). У большинства пакетов npm и пакетов Python из PyPI его нет, так что передайте lock-файл рядом с ними.
  • Почему это важно. Без него две установки одного и того же кода могут различаться, и тест, проходящий сегодня, может упасть завтра на новой версии зависимости.
  • Собранный образ уже зафиксирован. Как только вы соберёте образ источника, его зависимости окажутся внутри. Lock-файл важен для режима dev и для каждой новой сборки.
  • Без скриптов установки. npm устанавливает с --ignore-scripts, а Python ставит только wheel и никогда не собирает из исходного дистрибутива.
  • Ваши реестры. Пакеты берутся из собственных реестров npm, индексов PyPI и хостов Git вашей организации, один раз указанных в Admin → Package registries. Публичному реестру npm и PyPI запись не нужна. См. Package Registries.
  • Кэширование. Установка кэшируется по хешу содержимого кода, поэтому один и тот же код устанавливается один раз.

Node

  • Код Node выполняется в Node.js, Bun или Deno — на ваш выбор.
  • Теги пишутся в JSDoc, а типы берутся из TypeScript.

Первый resolver на TypeScript:

export interface Risk {
  score: number;
  reasons: string[];
}

/** Churn risk of one account.
 * @table
 */
export async function churnRisk(account_id: string): Promise<Risk[]> {
  return [{ score: 0.82, reasons: ["no logins in 30 days"] }];
}

Пакетный churnScore на TypeScript:

export interface CustomerKey {
  customer_id: string;
  plan?: string;
  region?: string;
}

export interface Score {
  customer_id: string;
  score: number;
}

/** Churn score of each customer in the batch.
 * @table
 */
export async function churnScore(
  customer_id?: string,
  plan = "basic",
  region = "US",
  loader?: Record<string, CustomerKey>,
): Promise<Score[]> {
  const keys = loader ?? { "": { customer_id, plan, region } };
  const rows = Object.entries(keys).map(([h, key]) => [h, { plan: "basic", region: "US", ...key }] as const);
  const scores = await model.predict(rows.map(([, row]) => row)); // one call for the whole batch
  const out = Object.fromEntries(
    rows.map(([h, row], i) => [h, [{ customer_id: row.customer_id, score: scores[i] }]]),
  );
  return loader ? out : out[""]; // a loader call returns the hash of key to rows
}

Развёртывание

Режим dev и worker

По умолчанию источник работает на сервере Provisa; это режим dev.

  • Ваш код выполняется в собственном процессе worker, по одному на источник и версию его кода в каждом процессе Provisa, запускаемом при первом вызове.
  • Упавший worker завершает ошибкой текущие вызовы, называя источник и функцию. Следующий вызов запускает новый worker.
  • Вызов отменяется, когда истекает срок его запроса: worker получает указание остановить вызов, а ожидающий запрос сразу освобождается.

Чтение таблицы

То, как чтение доходит до вашей функции, зависит от того, что ещё оно читает:

  • Чтение одной таблицы кода идёт прямо к функции. Ничего не сохраняется. Provisa вызывает её с аргументами чтения и переданными фильтрами и применяет остаток запроса к возвращённым строкам.
  • Чтение с соединением других таблиц выполняет движок федерации. Таблица источника в режиме dev для него сохраняется: строки хранятся под этими аргументами в течение TTL кэша таблицы.
  • Таблица собранного источника в таком соединении читается вживую с его удалённого хоста, который движок подключает как каталог Postgres. Вместо этого она сохраняется, если её источник реплицирован или с нижней границей, если таблица хранится постоянно, если она объявляет TTL сохранения больше 0, или на движке, который не может подключить хост.

В любом случае видимость, маски и правила строк применяются к строкам, как и для любой таблицы. Аргументы передаются так же, как любые параметры таблицы: условиями _nf_ в SQL, аргументами полей в GraphQL.

Запуск на удалённом хосте

Если вашему коду нужна машина, которой не является сервер, например хост с GPU или хост в другой сети, соберите его образ и запустите там. Тогда источник работает в режиме сборки, а его таблицы, права и читатели остаются прежними.

Сборка образа.

  • Сначала сохраните источник в режиме dev, чтобы его код был прочитан и закреплён.
  • В разделе Remote host формы выберите архитектуру (Linux x86-64 или ARM64) и место назначения образа: один из реестров контейнеров вашей организации (Admin → Container registries) или скачивание.
  • Provisa собирает образ сам, на минимальной базе, без демона Docker: код на закреплённом хеше, его зависимости, установленные для этой платформы по lock-файлу через ваши реестры пакетов, закреплённую среду выполнения и программу удалённого хоста. Во время сборки ничего в образе не запускается.
  • Тег образа содержит хеш кода и версию Provisa, а его метки называют среду выполнения, хеш кода и экспорты.

Запуск. Форма показывает сервис compose для образа. Скачанный образ сначала загрузите командой docker load -i <file>. Контейнер слушает порт 7443 и хранит свою идентичность в своём томе. Порт хоста вы выбираете через PROVISA_HOST_PORT; без него сервис не запустится.

services:
  provisa-code-rates:
    image: ghcr.io/example/provisa-code-rates:3f9a1c2e7b40-0.1.0
    restart: unless-stopped
    ports:
      - "${PROVISA_HOST_PORT:?set PROVISA_HOST_PORT to a free port on this host}:7443"
    volumes:
      - provisa-code-rates-state:/var/lib/provisa
volumes:
  provisa-code-rates-state: {}

При первом запуске контейнер выводит в журнал строку, начинающуюся с pairing code:.

Подключение.

  • В форме укажите хост, по которому Provisa обращается к контейнеру, опубликованный порт и код сопряжения. В общей сети Docker или в Kubernetes укажите имя контейнера или Service и 7443, ничего не публикуя.
  • Provisa сопрягается с контейнером по взаимному TLS. Код используется один раз; после этого каждая сторона принимает только другую.
  • Provisa проверяет, что контейнер выполняет код источника и предлагает его экспорты, и переводит источник в режим сборки.
  • Каждый процесс Provisa держит собственное соединение с хостом и переподключается, если хост перестаёт отвечать. Пока он не отвечает, чтение таблиц источника завершается ошибкой с указанием хоста.

Если код меняется.

  • Контейнер, который возвращается с другим кодом, отклоняется, пока стюард не проверит код и не закрепит его заново в форме.
  • Чтобы изменить сам код, переключите источник обратно в dev, сохраните новый код и соберите заново.

Сервисы Provisa

Ваш код может импортировать модуль, который Provisa всегда предоставляет на сервере: provisa_services в Python, @provisa/services в Node.js, Bun и Deno. Ничего устанавливать не нужно, и в реестрах пакетов его нет. Вне Provisa его не существует, поэтому ваши тесты подменяют его.

import provisa_services as ps

region = ps.settings.get("region")
rates = ps.cache.get("rates") or fetch_rates()
ps.cache.set("rates", rates, ttl=3600)
import { settings, cache } from "@provisa/services";

const region = await settings.get("region");
const rates = (await cache.get("rates")) ?? (await fetchRates());
await cache.set("rates", rates, 3600);
Вызов Что делает
secrets.get(name) Один из объявленных секретов источника, прочитанный сейчас из хранилища секретов. См. Секреты.
settings.get(name, default), settings.all() Настройки источника из раздела Settings его формы, которые стюард может менять, не меняя код. Отсутствующая настройка без значения по умолчанию вызывает ошибку.
cache.get(key, default), cache.set(key, value, ttl), cache.delete(key) Собственный кэш источника, общий для всех его worker. Значение — всё, что вмещает JSON, до 1 МБ, хранится ttl секунд или до замены.
vectors.upsert(items, model), vectors.search(query, k, model), vectors.embed(texts, model), vectors.delete(ids) Собственная коллекция векторов источника для его справочных данных. У элемента есть id и text или vector; текст векторизуется одной из векторных моделей вашей организации. Она не может искать по вашим управляемым таблицам, данные которых попадают в код только как параметры.
http.get(url), http.post(url, ...) Запрос, который Provisa делает за код, через исходящий прокси и центры сертификации развёртывания. Хост должен быть в списке разрешённого исходящего трафика, и каждый запрос записывается в журнал аудита.
llm.chat(model, messages), llm.embeddings(model, input), llm.base_url() (llm.baseUrl() в JavaScript) Сервис LLM в Provisa. См. Вызов модели.
log(message, level, fields), metric(name, value, kind, attributes) Строка журнала и точка метрики с именем code_source.<name>, привязанные к запросу, который обслуживает ваш код.
  • Вызов, который сервис отклоняет, вызывает ServiceError с названием сервиса и причиной.
  • Сервисы отвечают коду, работающему на сервере Provisa. На удалённом хосте каждый вызов завершается ошибкой.

Секреты

Ключ платного API место в хранилище секретов вашей организации, а не в коде.

  • В разделе Secrets формы источника выберите секреты организации, которые может читать код. В файле конфигурации перечислите их в mapping источника:

    mapping:
      secrets: [openrouter]
    
  • Имя само по себе — это секрет организации с таким именем. Чтобы читать вместо него переменную окружения развёртывания, укажите ссылку: secrets: {openrouter: "${env:OPENROUTER_API_KEY}"}.

  • Список ссылается на хранилище и никогда не копирует значение, поэтому конфигурация, называющая секрет, который ещё никто не сохранил, всё равно загружается.

Ваш код читает секрет через secrets.get(name) или напрямую из $PROVISA_SERVICES/secrets/<name>:

import os, urllib.request
key = urllib.request.urlopen(f"{os.environ['PROVISA_SERVICES']}/secrets/openrouter").read().decode()
const key = await (await fetch(`${process.env.PROVISA_SERVICES}/secrets/openrouter`)).text();
  • PROVISA_SERVICES — единственная переменная окружения, которую получает ваш код. Запрос идёт через тот же исходящий прокси, что и остальной трафик, и прокси отвечает на него сам; ничего другого в Provisa так не достичь.
  • Адрес содержит токен, который есть только у worker этого источника. Он читает только имена, перечисленные источником, и перестаёт работать, когда worker останавливается.
  • Каждое чтение разрешает секрет в хранилище в этот момент, поэтому сменённый секрет действует со следующего чтения.
  • Имя, которого нет в списке источника, отклоняется, как и секрет, который нельзя разрешить. Отказ называет секрет, но никогда его значение.
  • Каждое чтение записывается в журнал аудита организации с источником, именем и временем. Никакое значение никогда не пишется в журнал.
  • Хост API всё равно должен быть в списке разрешённого исходящего трафика.

Вызов модели

Ваш код может вызывать языковую модель через Provisa, не храня ключ поставщика. $PROVISA_SERVICES/llm/v1 — конечная точка, совместимая с OpenAI, поэтому любой SDK OpenAI работает, если направить туда его базовый URL. Адрес и есть учётные данные, так что ключ, который просит SDK, может быть любым текстом.

import os
from openai import OpenAI

client = OpenAI(base_url=f"{os.environ['PROVISA_SERVICES']}/llm/v1", api_key="unused")
reply = client.chat.completions.create(
    model="provisa:sql_generation",
    messages=[{"role": "user", "content": "Summarise this account's last quarter."}],
)
import OpenAI from "openai";

const client = new OpenAI({ baseURL: `${process.env.PROVISA_SERVICES}/llm/v1`, apiKey: "unused" });
const reply = await client.chat.completions.create({
  model: "provisa:sql_generation",
  messages: [{ role: "user", content: "Summarise this account's last quarter." }],
});
  • По роли. Назовите модель как provisa:<role>, и вы получите модель, которую ваша организация назначает этой роли в Admin → AI Models; она следует этому выбору, когда администратор его меняет. Можно также назвать модель напрямую, если ей назначена роль.
  • Эмбеддинги. $PROVISA_SERVICES/llm/v1/embeddings принимает любую из векторных моделей вашей организации.
  • Любая другая модель отклоняется.
  • Вызов уходит через собственных поставщиков, конечные точки и ключи вашей организации, а то, что он отправляет, сначала проходит через правила маскирования Provisa.
  • Потоковая передача не поддерживается. Отправляйте stream: false — это значение по умолчанию.

Защитные механизмы

Песочница

  • Ваш код выполняется в собственном процессе worker, в песочнице. Он может читать свой установленный код и больше ничего на диске.
  • Он достигает только хостов из списка разрешённого исходящего трафика развёртывания, через прокси, который запускает Provisa, и никогда не достигает собственных портов Provisa.
  • Хост, который не может обеспечить песочницу, не запускает worker и называет, чего не хватает; код никогда не выполняется без ограничений. В Windows источники кода отклоняются.
  • На удалённом хосте границей исходящего трафика кода служит сетевая политика контейнера. Список разрешённого исходящего трафика сервера действует для режима dev.

Отказы

Каждый из этих случаев отклоняется, по возможности до запуска вашего кода:

  • Неизвестный тег или тип, который Provisa не может сопоставить, — все проблемы функции сразу.
  • Чтение, в котором пропущен обязательный параметр.
  • Объявление передачи фильтров или loader, которое типы функции не могут принять.
  • Пакет с зависимостями и без lock-файла.
  • Каталог на сервере вне разрешённых или на сервере с многими организациями.
  • Удалённый хост, выполняющий не тот код, на котором закреплён источник.
  • Секрет, которого нет в списке источника, неразрешённая модель, вызов сервиса, на который сервис не отвечает.

Учёт и лимиты

  • Каждый вызов модели записывается для источника с моделью, её токенами и стоимостью по записи модели в Polly model prices и попадает в журнал аудита организации.
  • Чтобы ограничить расходы источника, задайте LLM monthly cap в его форме, в USD. В файле конфигурации он указывается в mapping:

    mapping:
      llm: {cap_usd: 20}
    
  • Источник с лимитом может вызывать только модели, у которых там есть цена.

  • Когда расходы за месяц (UTC) достигают лимита, вызовы отклоняются до смены месяца. Вызовы, уже выполняющиеся в момент достижения лимита, всё равно завершаются, поэтому месяц может закончиться немного выше лимита.

Управление результатами

  • Строки функции проверяются на соответствие столбцам таблицы.
  • Затем для каждого читателя применяются видимость, маски и правила строк, как и для любой таблицы.
  • Ваш код видит данные управляемой таблицы только в виде параметров, которые передаёт ему чтение.

Трассировка

  • Каждый вызов вашего кода трассируется как часть запроса, который его вызвал: функция, её вид, сколько аргументов она получила (никогда не их значения), возвращённые строки и пакеты, длительность и CPU worker.
  • Каждый вызов сервиса трассируется так же.
  • При обычной детализации трассировки эти сведения находятся на span запроса; при отладочной детализации каждый вызов — отдельный span.
  • Если ваш код использует OpenTelemetry, его span присоединяются к той же трассе.