Эта документация разработки ИИ сервисов на платформе ТРОПАСС и подготовки необходимых файлов. Если вам нужно только вызвать готовый сервис из своего кода, сразу переходите к разделу «Где взять API-ключ и model_id».

Готовый пример проекта лежит в репозитории tropass-ai/hello-tropass на GitHub: в нём есть описание примера, metadata.json, готовые файлы для archive.zip и AGENTS.md с правилами для ИИ-агента. Репозиторий можно использовать как шаблон (кнопка Use this template).

Основы

Сервисы ТРОПАСС описываются входными и выходными контрактами и определённой структурой файлов. Структура типового Python-проекта:

hello-tropass/
├── main.py              # главная функция и приложение сервера
├── utils.py             # вспомогательные функции (можно свои модули и пакеты в modules/)
├── modules/
│   └── __init__.py
├── metadata.json        # входы и выходы (грузится отдельным файлом, в архив не входит)
├── requirements.txt
├── input_examples/
│   └── input.json       # тестовый вход
├── output_examples/     # файлы для панелей image/video (если нужны)
├── tests/               # тесты pytest
├── run_test.py          # запуск main() без сервера
├── build.sh             # сборка deploy/archive.zip и копия metadata.json в deploy/
└── deploy/              # то, что грузится на платформу (в .gitignore)

Обязательно подготовить два файла: metadata.json и main.py. В файле metadata.json содержится структура входных и выходных данных сервиса, а в файле main.py - главная исполняемая функция.

Папка modules - для ваших скриптов; таких папок может быть сколько угодно. Библиотека tropass_sdk подключается как обычный Python-пакет: tropass-sdk на PyPI. Достаточно указать tropass-sdk[server] в requirements.txt (в примере tropass-sdk[server]==2.1), копировать её в проект не нужно.

В папке input_examples лежит тестовый вход. Он нужен для автотеста модели после сборки; рекомендуем его добавлять, но это не обязательно.

В requirements.txt перечисляются библиотеки сервиса. В архив archive.zip для загрузки на платформу идут main.py, utils.py, requirements.txt, modules/, input_examples/ и output_examples/; тесты, run_test.py и metadata.json не входят. Состав архива не ограничен: добавляйте свои файлы и пакеты.

Требования к версии Python

Минимальная поддерживаемая версия: Python >= 3.10

Описание полей и файл metadata.json

Для нового сервиса следует описать входные и выходные поля в отдельном файле metadata.json (пример: hello-tropass/metadata.json). Он состоит из набора ключей, назначение которых - описать в максимально краткой форме, какие входные и выходные поля нужны вашему сервису.

Файл metadata.json имеет следующий вид:

{ 
  "model_name": "work", 
  "model_service": "Демо модель для разработчиков",
  "inputs": [ 
    {
      "indicator_name": "text_input",
      "display_field_name": "Тут вводится текст",
      "indicator_type": "text",
      "is_required": true,
      "allowed_file_extensions": ["png", "jpg"],
      "default_value": "Тут вводится дефолтное значение"
    }
  ]
  "outputs": [ 
    { 
      "indicator_name": "text_output",
      "display_result_name": "Тут будет текстовая панель",
      "indicator_type": "text"
    }
 ]
}

Поле is_required позволяет пометить поле как обязательное или необязательное для заполнения пользователем.

Поле default_value позволяет задать дефолтное значение для инпута. Пример:

  • text, dropdown - "любая строка"
  • number - строка, которую можно перевести в число, разделитель только точка. Пример валидного значения - "0.42"
  • checkbox - "True" или "False"
  • multi-select - строка, содержащая json массива строк. Пример валидного значения - "["Выбор +100500","Выбор 1"]" (в json требуются только двойные кавычки, одинарные - невалидный синтаксис)
  • selector-slider - строка, содержащая json из двух элементов, строки и числа. Пример валидного значения - "["Выбор +100500",1]" .
  • voice, file, multi-file - нельзя установить дефолтное значение

Необязательное поле верхнего уровня python_version ("3.10", "3.11", "3.12", "3.13" или "3.14") задает версию Python для сборки; допустимые версии зависят от окружения. Поле system_dependencies ({"extra_apt_packages": [...]}) задает apt-пакеты (см. раздел про системные зависимости). Типы панелей в outputs берутся из списка: text, series, diagram, donut, dumbbell, bd, polarLine, polarColumn, circles, image, video, file.

Поле model_name в файле задает название модели на латинице. Оно нужно, чтобы найти модель на платформе.

Поле allowed_file_extensions позволяет указать расширения файлов, которые можно положить в этот инпут.

Массив inputs задает перечень входных полей модели. Каждый элемент массива - это отдельное поле ввода с названием на естественном языке (display_field_name), типом (indicator_type), ярлыком для скрипта (indicator_name)

Варианты инпутов и их описание ниже.

Поле outputs задает характеристики панелей, которые выводятся в Ядре. Это поле позволяет спланировать, какие панели с данными будут выводиться в результате операций внутри сервиса. Элементы поля состоят из ярлыка-метки (indicator_name), названия панели на естественном языке (display_result_name) и типа панели (indicator_type).

Входные поля

Инпуты моделей для сервиса собираются в файле metadata.json. Поле инпута состоит из частей:

indicator_name - задает название ярлыка для входного поля. Именно это название будет приходить в исполняемый скрипт сервиса. По нему вы сможете найти данные, передаваемые от пользователей в своем сервисе. Постарайтесь сделать его уникальным, чтобы ничего не упало.

display_field_name - отображаемое название входного поля. Это название будет видно пользователю в Ядре. Тут следует написать, что вы хотите, чтобы он заполнил.

indicator_type - тип индикатора. Отвечает за вид входного поля, который будет отображаться пользователю, подробно в таблице ниже.

possible_values - список возможных значений входного поля (нужно для разного рода селекторов)

Обратите внимание, что для некоторых полей (например, для слайдера) требуется привести дополнительную информацию в специальных ключах. Детали приводятся в таблице

Типы входных полей

№ТипКак выглядит элементКак будет отображаться поле
1Селектор{"indicator_name": "region", "display_field_name": "Регион проживания", "indicator_type": "dropdown", "possible_values": ["Архангельская область", "Владимирская область", "Московская область"]}Снимок экрана 2025-11-17 182821.png
2Числовое значение{"indicator_name": "dohod", "display_field_name": "Ваш возраст", "indicator_type": "number"}Снимок экрана 2025-11-17 182933.png
3Логическое значение{"indicator_name": "place", "display_field_name": "Являетесь ли вы государственным служащим?", "indicator_type": "checkbox"}Снимок экрана 2025-11-17 182946.png
4Слайдер{"indicator_name": "proc", "display_field_name": "Сколько лет вы хотели бы прожить на пенсии?", "indicator_type": "number", "min_value": 1, "max_value": 35, "ui_elem_type": "slider"}Снимок экрана 2025-11-17 182955.png
5Текст{"indicator_name": "text1", "display_field_name": "inputs", "indicator_type": "text"}image.png
6Дата{"indicator_name": "date1", "display_field_name": "Тут мог быть ваша дата", "indicator_type": "date"}Похоже, его нет
7Файл{"indicator_name": "file1", "display_field_name": "Тут мог быть ваш файл", "indicator_type": "file"}image.png
8Диктофон{"indicator_name": "voice1", "display_field_name": "Запишите голосовое сообщение", "indicator_type": "voice"}Кнопка записи голосового сообщения (диктофон)
9JSON{"indicator_name": "json1", "display_field_name": "Введите валидный json", "indicator_type": "json"}Поле для ввода json значения

Выходные поля и панели

Отображение панелей в ТРОПАСС зависит от их типа, который указывается в файле metadata.json и данных, отправляемых моделью. Сначала рассмотрим данные, которые нужно заполнить в metadata.json.

indicator_name - ярлык панели на латинице, по которому затем в скрипте можно определить панель.

display_result_name - название панели русском, будет отображаться в ТРОПАСС

indicator_type - тип панели.

Типы панелей являются жесткими шаблонами ТРОПАСС. Вся информация, которая поступает из вашей модели ИИ должна выводиться в каком-то из используемых типов панелей. Такой "жесткий слой" позволяет избавиться разработчику от необходимости самому программировать графики и картинки и отдать эту нудную работу платформе.

Число панелей на 1 сервис не ограничено, поэтому структурировать и вывести можно любой объем информации с любым содержанием. Более того, число типов панелей постоянно растет, и мы можем добавить новые по запросам разработчика. Перечень типов с привязкой к выводу приводятся в таблице ниже.

Типы выходных панелей

№ТипКак добавитьВид в Ядре
1Ниточка{"indicator_name": "name", "display_result_name": "Хорошая ли идея - купить недвижимость", "indicator_type": "series"}Снимок экрана 2025-11-17 182412.png
2Ромашка{"indicator_name": "name", "display_result_name": "Оценка регионов", "indicator_type": "polarColumn"}Снимок экрана 2025-11-17 182240.png
3Линии{"indicator_name": "name", "display_result_name": "1. Физическая культура и спорт. Рекомендации по ресурсу", "indicator_type": "dumbbell"}Снимок экрана 2025-11-17 182304.png
4Розочка{"indicator_name": "name", "display_result_name": "Изменения по сравнению с прошлым", "indicator_type": "polarLine"}Снимок экрана 2025-11-17 182330.png
5Слайды{"indicator_name": "name", "display_result_name": "Какое-то название", "indicator_type": "bd"}Снимок экрана 2025-11-17 182442.png
6Колончатая{"indicator_name": "name", "display_result_name": "Состав платежа", "indicator_type": "diagram"}Снимок экрана 2025-11-17 182503.png
7Текстовая{"indicator_name": "name", "display_result_name": "Тут могло быть ваше название панели", "indicator_type": "text"}image.png
8Видео{"indicator_name": "name", "display_result_name": "Тут могло быть ваше название панели", "indicator_type": "video"}image.png
9Картиночная{"indicator_name": "name", "display_result_name": "Тут могло быть ваше название панели", "indicator_type": "image"}image.png
10Файловая{"indicator_name": "name", "display_result_name": "Тут могло быть ваше название панели", "indicator_type": "file"}image.png

Остальная настройка вывода производится в исполняемом файле main.py

Где взять API-ключ и model_id

Для вызова сервиса снаружи нужны три вещи: адрес шлюза, API-ключ и model_id.

  • Адрес шлюза - https://апи.тропасс.рф/api Полный адрес вызова: https://апи.тропасс.рф/api/rpc/call-model.
  • API-ключ выпускается в личном кабинете (ядра), а не в админке:
    1. откройте раздел Api ключи;
    2. нажмите «Новый ключ», введите название и отметьте сервисы, к которым ключ должен давать доступ (в списке только те модели, которые добавлены вам в Маркете);
    3. скопируйте токен кнопкой «копировать». Токен передаётся в заголовке Authorization: Bearer <токен>. Не храните его в репозитории.
  • model_id - это UUID сервиса. Аналитик берёт его в админке: раздел «Модели аналитика», откройте нужный сервис на редактирование. Адрес страницы имеет вид /analytics-models/edit/<model_id>, UUID в конце адреса и есть model_id.

Python-примеры для вызова сервиса снаружи (через API платформы)

Базовый шаблон запроса (общий для всех типов):

import json
import httpx

API_URL = "https://апи.тропасс.рф/api"
MODEL_ID = "uuid-вашей-модели"
USER_API_TOKEN = "ваш-api-токен"

payload = {
    "model_id": MODEL_ID,
    "model_request_data": {
        # indicator_name: значение (см. примеры ниже по номерам)
    }
}

response = httpx.post(
    f"{API_URL}/rpc/call-model",
    data={"model_payload": json.dumps(payload)},
    headers={"Authorization": f"Bearer {USER_API_TOKEN}"},
)
result = response.json()

1. Селектор (dropdown) — одна строка из possible_values:

"model_request_data": {"region": ["Московская область"]}

2. Числовое значение (number) — int или float:

"model_request_data": {"dohod": [35]}

3. Логическое значение (checkbox) — bool:

"model_request_data": {"place": [True]}   # или False

4. Слайдер (number + ui_elem_type: slider) — число в пределах min..max:

"model_request_data": {"proc": [20]}

5. Текст (text) — строка до 100 000 символов:

"model_request_data": {"text1": ["Ваш текст здесь"]}

6. Дата (date) — строка в формате ДД.ММ.ГГГГ:

"model_request_data": {"date1": ["24.03.2026"]}

7. Файл (file) — файл загружается как multipart, в model_request_data список с именем файла:

payload = {
    "model_id": MODEL_ID,
    "model_request_data": {"file1": ["document.pdf"]},
}
response = httpx.post(
    f"{API_URL}/rpc/call-model",
    data={"model_payload": json.dumps(payload)},
    files=[("files", ("document.pdf", open("document.pdf", "rb"), "application/pdf"))],
    headers={"Authorization": f"Bearer {USER_API_TOKEN}"},
)

Вместо загрузки файла можно передать прямую S3-ссылку:
"file1": ["https://<хранилище>/путь/к/файлу.pdf"]

8. Диктофон (voice) — аналогично файлу, но аудио:

payload = {
    "model_id": MODEL_ID,
    "model_request_data": {"voice1": ["recording.ogg"]},
}
response = httpx.post(
    f"{API_URL}/rpc/call-model",
    data={"model_payload": json.dumps(payload)},
    files=[("files", ("recording.ogg", open("recording.ogg", "rb"), "audio/ogg"))],
    headers={"Authorization": f"Bearer {USER_API_TOKEN}"},
)

9. JSON (json) — строка с валидным JSON-массивом объектов:

"model_request_data": {
    "json1": json.dumps([{"ключ": "значение", "число": 42}])
}
# Итоговое значение: '[{"ключ": "значение", "число": 42}]'

Файл main.py и контракт вывода данных

Несмотря на то, что классы контракта в пакете tropass_sdk.schemas могут выглядеть сложно, сам вывод достаточно простой и в меру удобный. В своем максимально развернутом виде контракт выглядит так

from tropass_sdk import schemas

custom_panel = schemas.ModelPanelOutputSchema(
    panel_output_name="panel",
    panel_type="series",
    panel_show_order=1,
    
    # Кастомизируем вложенные структуры
    primary_data=schemas.ModelPrimaryDataSchema(
        plot_data=schemas.ModelPlotDataSchema(
            x_axis_values=["Сегодня", "Вчера", "На прошлой неделе", "На позапрошлой неделе"],
            series_data=[
                schemas.ModelSeriesDataSchema(
                    legend_name="Продажи",
                    plot_values=[100.5, 200.3, 150.0, 300.2]
                ),
                schemas.ModelSeriesDataSchema(
                    legend_name="Другие продажи",
                    plot_values=[10.5, 20.3, 15.0, 30.2]
                )
            ]
        ),
        media=[
            schemas.ModelMediaItemSchema(
                file_name="Отчет",
                local_abs_path="/tmp/report.png"
            ),
            schemas.ModelMediaItemSchema(
                file_name="Другой отчет",
                local_abs_path="/tmp/report.png"
            )
        ]
    ),
    
    # Добавляем описание (опционально)
    descriptions=[
        schemas.ModelDescriptionSchema(
            content="# Тестовое описание\n*Пример*",
            description_type=schemas.DescriptionTypeEnum.MD
        )
    ],

    # Прикрепляем файлы для скачивания (опционально)
    attachments=[
        schemas.ModelMediaItemSchema(
            file_name=out_name,
            local_abs_path=str(out_path)
        )
    ]
)

Давайте рассмотрим ключи подробно.

panel_output_name - ярлык-метка панели. Он соответствует indicator_name из файла metadata.json и нужен, чтобы мы смогли найти вашу панель

panel_type - тип панели (соответствует indicator_type из файла metadata.json)

panel_show_order - порядок вывода панели. Вы можете управлять порядком вывода панелей в своем сервисе. Но если не хотите, мы определим его за вас и это поле можно не заполнять.

primary_data - блок основных данных панели. Если вы выводите данные для графиков, то в нем следует заполнять блок plot_data. Если медийную информацию (картинки, видео и пр.) - следует заполнять блок media.

plot_data - блок данных для графиков

x_axis_values - набор подписей по оси абсцисс (она же ось Х). Обычно это массив строк.

series_data - массив с данными для графика, в него складываются данные для вывода.

legend_name - название ряда на графике

plot_values - массив числовых данных для вывода на график. Одному массиву соответствует одна ряд на графике. Как видно из структуры контракта, таких рядов может быть много.

media - начало блока медийной информации

file_name - название файла, которое будет отображаться в Ядре

local_abs_path - адрес пути до файла

descriptions - список подписей для панели. В каждой панели может выводиться текстовая подпись, ее можно вставить в этот блок

content - сам текст подписи

description_type - тип вывода. Сейчас поддерживается 3 типа, которые можно выбирать: простой текст (тип PLAIN), текст в html (тип HTML), текст в разметке markdown (тип MD). Как видно, в примере выбран тип MD.

attachments - список прикрепленных файлов (для панели типа file именно сюда кладутся файлы для скачивания). К каждой панеле можно прикрепить файлик, если захотите. Все они указываются в этом блоке

file_name - отображаемое название файла

local_abs_path - адрес прикрепленного файла

Ненужные блоки можно не заполнять. Примеры вывода разных панелей есть в hello-tropass. Если контракт заполнен неверно, ТРОПАСС не обработает данные и вы их не увидите; проверить его можно локально: все классы типизированы и лежат в пакете tropass_sdk.schemas, а при неверных полях ModelPrimaryDataSchema не соберётся.

Применительно к конкретной панели, описанный контракт выглядит следующим образом.

Слайд1.PNG

Слайд2.PNG

Типовой шаблон скрипта main.py

Шаблон - hello-tropass/main.py. Ниже - описание его частей. Общее описание примера (панели, тестирование, сборка) - в README примера.

Файл main.py начинается с импорта библиотек Python. Любые импортируемые библиотеки должны быть в requirements.txt, иначе автосборщик их не подгрузит.

В блоке # Загрузка классов ТРОПАСС подключаются schemas и ModelServer из tropass_sdk, которые необходимы для работы сервиса на платформе. Этот блок не удаляйте. Ниже подключаются ваши модули: в примере это utils.py, лежащий рядом с main.py; свои пакеты удобно складывать в папку modules/.

Тестовый вход лежит в input_examples/input.json: в нём приведены примеры для всех типов инпутов, имеющихся на платформе. Именно такой объект model_input приходит от пользователя в функцию main(). Скопируйте model_input, с которым отлаживаетесь, в input.json: это позволит запускать сервис командой python3 run_test.py, а платформе - проводить автотест при сборке.

Код примера состоит из трёх частей:

  1. Входы. Класс Inputs и функция read_inputs() разбирают model_input и показывают, как читать поле каждого типа. Ошибки пользователя (поле не заполнено, не число, некорректный JSON) выбрасываются как utils.InputError и возвращаются текстовой панелью с причиной.
  2. Панели. На каждый тип панели своя функция panel_* (panel_text, panel_series, panel_diagram, panel_images и т.д.). Ненужные функции удаляются вместе со строкой в списке панелей и записью в outputs из metadata.json.
  3. Главная функция main(). Она разбирает входы, содержит место для вашей логики, собирает список панелей и расставляет порядок вывода (порядок в списке = порядок на экране). Функция может быть синхронной или асинхронной, для нас это не имеет значения. Завершается MLModelResponseSchema(...).model_dump(), этот вызов мы советуем не трогать.

В конце файла создаётся сервер: ModelServer(model_func=main, model_name="hello_tropass", model_version="1.0.0") и application = server.build_application(). Переменная application - это приложение, которое запускает платформа, ее имя менять не нужно. В model_name и model_version укажите название и версию вашего сервиса.

Особенности SDK 2.1

Эти правила проверены валидацией tropass-sdk и часто становятся причиной «молчаливых» ошибок:

  • ModelPrimaryDataSchema содержит только два поля: plot_data и media. Поля text_content в схеме нет: если его передать, оно будет молча проигнорировано и текст до пользователя не доедет.
  • Текст панели передавайте в descriptions[].content.
  • У text-панели primary_data можно не передавать вовсе (primary_data=None). Если передаёте, то plot_data в нём обязателен: поставьте заглушку (x_axis_values=["1"], одна серия с plot_values=[0.0]). В примере для этого есть utils.empty_primary_data().
  • plot_data по-настоящему нужен для графических панелей: diagram, series, dumbbell, polarLine, polarColumn, donut.
  • Значения description_type в ответе - строчные: "md", "html", "plain". Через DescriptionTypeEnum.MD и т.п. это получается само; если собираете словарь вручную, заглавные "MD"/"HTML" валидация отклонит.
  • Файлы результата (картинки, видео, вложения) должны лежать внутри files_directory_path. Платформа сама подхватит их по local_abs_path и загрузит в хранилище.
  • Значения plot_values должны быть числами (float). Пропуски (NaN) из pandas замените нулями или уберите до сборки ответа.
  • Если считаете таблицы в pandas, для DataFrame.to_markdown() добавьте в requirements.txt пакет tabulate: он не входит в зависимости pandas.
  • panel_output_name каждой панели обязан совпадать с indicator_name из outputs в metadata.json, иначе панель не отобразится.

Доступ к входным данным

Значения в model_input всегда приходят списками, ключи - это indicator_name из metadata.json:

# Текстовое поле и выпадающий список
text_value = model_input.get("text_input", [""])[0]
dropdown_value = model_input.get("dropdown_input", [""])[0]

# Число
number_value = model_input.get("number_input", ["0"])[0]
number = float(number_value) if number_value else 0

# Чекбокс (в разных клиентах приходит как true/false, "1"/"0" или "True"/"False")
checkbox_value = model_input.get("checkbox_input", ["0"])[0]
is_checked = str(checkbox_value).lower() in ("1", "true")

# Файл: приходит АБСОЛЮТНЫЙ ПУТЬ к файлу внутри контейнера
# (платформа сама кладёт туда загруженный файл; имя может не совпадать с исходным)
file_path = model_input.get("file_input", [""])[0]

# Мультиселект и multi-file: список значений / список путей
multi_select = model_input.get("multi_select_input", [])

Необязательные поля (is_required: false) могут отсутствовать в model_input, поэтому читайте их через .get(...). Рабочий пример чтения всех 12 типов - функция read_inputs() в main.py примера.

Ресурсы платформы: common_resources и request_metadata

common_resources

Второй аргумент main() - словарь с системными ресурсами. Платформа заполняет его сама, из конфигурации вашего сервиса:

КлючТипОписание
files_directory_pathstringКаталог для файлов ответа (всегда есть)
model_api_gateway_paramsdictURL и токен шлюза для вызова других сервисов (см. Вызов других сервисов)
big_llm_api_params, medium_llm_api_params, small_llm_api_paramsdictПараметры LLM трёх размеров (см. LLM-вызовы)
web_search_paramsdictСервис веб-поиска (опционально)
coredev_database_params, hot_database_paramsdictПодключения к БД (опционально)

Состав зависит от настроек сервиса: читайте нужные ключи через .get(...) и не рассчитывайте, что они есть всегда. Пример (секреты заменены):

common_resources = {
    "files_directory_path": "/srv/www/temp/<request-id>/response",
    "model_api_gateway_params": {
        "url": "https://апи.тропасс.рф/api/rpc/call-model",
        "api_token": "<api_token>"
    },
    "big_llm_api_params": {
        "api": "https://tropass-m3-mxfp8.tropass.me",
        "model": "tropass-m3-mxfp8",
        "user": "<user>",
        "password": "<password>",
        "verify": False
    }
}

Секреты (пароли БД, токены, пароли LLM) никогда не кладите в код, input.json и репозиторий: их выдаёт платформа в common_resources.

request_metadata и токен пользователя

SDK-сервер добавляет в model_input["request_metadata"] данные из заголовков запроса:

ЗаголовокКлюч в request_metadata
X-User-IDuser_id
X-User-Localelocale
X-User-Api-Tokenuser_api_token

Токен пользователя имеет приоритет над сервисным: подставляйте его в model_api_gateway_params.api_token перед вызовами других сервисов, чтобы вызов списывался на аккаунт пользователя:

request_metadata = model_input.get("request_metadata") or {}
user_api_token = request_metadata.get("user_api_token")
if user_api_token:
    gateway_params = dict(common_resources.get("model_api_gateway_params") or {})
    gateway_params["api_token"] = user_api_token
    common_resources = dict(common_resources)
    common_resources["model_api_gateway_params"] = gateway_params

Паттерн app_config_scope: не таскать common_resources через все функции

Если в сервисе много модулей, передавать common_resources в каждую функцию неудобно. Положите его в ContextVar и читайте из любого места:

# modules/context.py
import contextlib
import contextvars

_CONFIG: contextvars.ContextVar[dict] = contextvars.ContextVar("_service_config", default=None)

@contextlib.contextmanager
def app_config_scope(common_resources: dict):
    token = _CONFIG.set(common_resources or {})
    try:
        yield
    finally:
        _CONFIG.reset(token)

def get_config(key: str, default=None):
    cfg = _CONFIG.get()
    return cfg.get(key, default) if cfg else default

def get_files_dir(default: str = "/tmp") -> str:
    return get_config("files_directory_path", default)


# main.py
with context.app_config_scope(common_resources):
    return _run(model_input)   # все модули внутри читают get_config() / get_files_dir()

Параллельность. asyncio.to_thread и asyncio.gather копируют ContextVar в поток сами, а вот ThreadPoolExecutor не прокидывает ContextVar в рабочие потоки (в отличие от asyncio). Оборачивайте каждую задачу в копию контекста, причём отдельную на каждый submit (один Context нельзя входить параллельно из нескольких потоков): pool.submit(contextvars.copy_context().run, functools.partial(fn, ...)).

Вызов других сервисов через model gateway

Сервис может вызывать другие сервисы платформы (LLM-шлюзы, генераторы картинок, конвертеры документов, веб-поиск и т.д.) через единый RPC-шлюз.

POST https://апи.тропасс.рф/api/rpc/call-model
Content-Type: multipart/form-data
Authorization: Bearer <api_token>
Idempotency-Key: <uuid>            # уникален на каждый вызов

Адрес и формат авторизации берите из common_resources["model_api_gateway_params"] (url, api_token). Блок «Python-примеры для вызова сервиса снаружи» выше показывает вызов сервиса снаружи (например, из своего приложения, с личным API-ключом); изнутри сервиса действуют правила этого раздела.

Тело - multipart из двух полей:

ПолеТипСодержимое
model_payloadtextJSON-строка {"model_id": "<uuid сервиса>", "model_request_data": {...}}
filesfileфайлы для загрузки (если у вызываемого сервиса есть file-инпуты)

Критичные правила формата:

  1. Все значения инпутов - массивы: "user_prompt": ["текст"], "width_px": ["1600"], даже если значение одно.
  2. Файловые инпуты: значение - ИМЯ загружаемого файла, а сам файл прикладывается в files под тем же именем: "data_file": ["data.json"] + files=(("data.json", fh),). Платформа положит файл в контейнер вызываемого сервиса и подставит путь. Расширение имени определяет парсер на той стороне.
  3. common_resources вызываемого сервиса передавать не нужно: платформа подставит их из его конфигурации.
  4. model_id - это UUID модели, а не model_name. Берите его из таблицы ниже или из URL карточки сервиса на платформе.

Авторизация

Authorization: Bearer <api_token>, где токен берётся так:

  1. user_api_token из request_metadata - приоритет, вызов спишется с пользователя;
  2. иначе сервисный api_token из model_api_gateway_params.

Плюс обязательный заголовок Idempotency-Key: <uuid4> - уникальный на каждый вызов (защита от дублей при ретраях на стороне шлюза).

Синхронный и асинхронный режимы

  • Без заголовка Tropass-Model-Call-Version (по умолчанию) вызов синхронный: ответ приходит в теле POST (HTTP 200). Так вызывается большинство сервисов.
  • С заголовком Tropass-Model-Call-Version: 2 вызов асинхронный: POST вернёт 202 {"task_id": "...", "status": "distributed"}, а результат забирайте поллингом GET https://апи.тропасс.рф/api/rest/model-tasks/{task_id} (тот же Bearer) до статуса, отличного от distributed/processing. Ошибка исполнения: status=processing_error, текст в error_message.

Разбор ответа

{
  "panel_items": [
    {
      "panel_output_name": "chart_image",
      "panel_type": "image",
      "primary_data": {
        "media": [
          {"file_name": "график.png", "mime_type": "image/png",
           "s3_url": "https://<хранилище>/...?X-Amz-Expires=3600"}
        ]
      },
      "descriptions": [{"content": "### Подпись", "description_type": "md"}],
      "panel_show_order": 1
    }
  ]
}
Что искатьГде
Панелиpanel_items[], опознавать по panel_output_name, порядок - panel_show_order
Картинки и файлыprimary_data.media[]: local_abs_path (путь к файлу на файловой системе платформы) или s3_url (presigned-ссылка, живёт около часа - скачивайте сразу)
Текстdescriptions[].content (поля primary_data.text_content не существует)

Ошибки и таймауты

HTTPПричинаЧто делать
422files не приложены или имя не совпадает с file-инпутомпроверить multipart
503сервис или контейнер упалодин ретрай через ~30 с, дальше обратитесь в поддержку платформы
таймаут клиентатяжёлый сервис (LLM, рендер)клиентский таймаут 300-600 с

Ретраить имеет смысл только transient-ошибки (сеть, 5xx, битый JSON) и с экспоненциальной задержкой. Ошибки исполнения внутри вызываемого сервиса (например, упавший рендер приходит как HTTP 200 и текстовая панель с ошибкой) ретраем без изменения запроса не лечатся.

Известные сервисы

Сервисmodel_idНазначение
MD-конвертерсм. карточку сервиса (начинается с 019f5b06-)docx/pdf/xlsx в Markdown
Image-gen019e2248-902a-77c1-8a48-335d175f866bгенерация изображений по промпту
Upscaler019e224a-33b1-7b42-a654-2b5b3ab6c3c6повышение разрешения изображений
Web-search019fd153-2b4e-77fc-bae0-855bd71161bdпоиск по открытым источникам
Инфографер (base)01a06309-f4b5-7a16-a827-aca0e70fccdePNG-инфографика (R+ggplot2) по табличным данным и промпту
Инфографер (R-part)01a06bb2-6c2f-753f-be5e-4942d1875f45низкоуровневый исполнитель R-кода

Пример клиента внутри сервиса

Не пишите клиент с нуля: возьмите за основу минимальный рабочий вариант:

import json
import uuid

import requests


def call_tropass_model(common_resources: dict, model_id: str, payload_data: dict,
                       files: dict[str, str] | None = None, timeout: int = 600) -> dict:
    """Синхронный вызов сервиса через шлюз. files: {имя_в_payload: локальный_путь}"""
    params = common_resources["model_api_gateway_params"]
    body = {"model_id": model_id, "model_request_data": payload_data}
    opened = [(name, open(path, "rb")) for name, path in (files or {}).items()]
    try:
        resp = requests.post(
            params["url"],
            headers={"Authorization": f"Bearer {params['api_token']}",
                     "Idempotency-Key": str(uuid.uuid4())},   # на каждый вызов - новый
            data={"model_payload": json.dumps(body, ensure_ascii=False)},
            files=[("files", (name, fh)) for name, fh in opened],
            timeout=timeout,
            verify=False,   # внутри контура используются самоподписанные сертификаты
        )
    finally:
        for _, fh in opened:
            fh.close()
    resp.raise_for_status()
    return resp.json()


def panel_text(panel: dict) -> str:
    return "\n\n".join(d.get("content", "") for d in panel.get("descriptions") or [])


def panel_media(panel: dict) -> list[dict]:
    return (panel.get("primary_data") or {}).get("media") or []


# Пример: сгенерировать картинку
result = call_tropass_model(common_resources, "019e2248-902a-77c1-8a48-335d175f866b",
                            {"prompt": ["Нарисуй кота"]})
for panel in result.get("panel_items", []):
    for media in panel_media(panel):
        url = media.get("s3_url")   # presigned-ссылка живёт ~1 час - скачивайте сразу

Что стоит добавить в боевой версии: ретраи на transient-ошибки с экспоненциальной задержкой и jitter; для файлов - seek(0) перед повторной отправкой; для результата - сначала пробовать local_abs_path, затем s3_url; Idempotency-Key генерировать заново на каждую попытку, если нужно реальное повторное исполнение.

Пример вызова с файлом через curl:

curl -s 'https://апи.тропасс.рф/api/rpc/call-model' \
  -H 'accept: application/json' \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F 'model_payload={
        "model_id": "<uuid целевого сервиса>",
        "model_request_data": {
          "data_file": ["data.json"],
          "user_prompt": ["Построй график по данным"]
        }
      }' \
  -F 'files=@data.json;type=application/json'

LLM-вызовы

Для работы с LLM используйте big_llm_api_params, medium_llm_api_params и small_llm_api_params из common_resources: это OpenAI-совместимые эндпоинты /v1/chat/completions.

{
    "big_llm_api_params": {
        "api": "https://tropass-m3-mxfp8.tropass.me",
        "model": "tropass-m3-mxfp8",
        "user": "<user>",
        "password": "<password>",      # вариант 1: basic-auth
        "verify": False                # самоподписанные сертификаты внутри контура
    },
    "medium_llm_api_params": {
        "api": "https://tropass36-35b-a3b-fp8.tropass.me",
        "model": "tropass36-35B-A3B-FP8",
        "api_key": "<api_key>"         # вариант 2: Bearer-токен
    }
}

Авторизация (приоритет сверху вниз): 1) api_key - заголовок Authorization: Bearer <api_key> (basic при этом не шлётся); 2) user + password - HTTPBasicAuth; 3) иначе без авторизации.

import requests

def ask_llm(params: dict, system_prompt: str, user_text: str,
            temperature: float = 0.2, max_tokens: int = 2000, timeout: int = 300) -> str:
    api_key = params.get("api_key")
    headers, auth = None, None
    if api_key:
        headers = {"Authorization": f"Bearer {api_key}"}
    elif params.get("user") and params.get("password"):
        auth = (params["user"], params["password"])
    resp = requests.post(
        f"{params['api'].rstrip('/')}/v1/chat/completions",
        json={
            "model": params["model"],
            "messages": [
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_text},
            ],
            "temperature": temperature,
            "max_tokens": max_tokens,
        },
        auth=auth, headers=headers, verify=params.get("verify", True), timeout=timeout,
    )
    resp.raise_for_status()
    return resp.json()["choices"][0]["message"]["content"]

Практика, проверенная в боевых сервисах:

  • Цепочка запасных моделей: big, затем medium, затем small. Если большая недоступна после всех ретраев, пробуйте следующий размер, а не падайте.
  • Ретраи только на transient-ошибки (сеть, 5xx): 3-5 попыток с экспоненциальной задержкой.
  • Thinking-модели: ответ может лежать в message.reasoning_content / message.reasoning, а в content попадают блоки <think>...</think> - вырезайте их регулярным выражением. Полезно передавать "chat_template_kwargs": {"enable_thinking": False}.
  • JSON-ответы: просите «строгий JSON без markdown» и парсите по шагам: прямой json.loads, затем починка типовых ошибок ("k"="v", = вместо :, лишние запятые, strict=False для управляющих символов), затем fenced-блок ```json, затем первый {...} или [...].
  • VL (картинки): картинка передаётся как image_url с data-URL base64: {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}. Перед отправкой уменьшайте ширину до ~1600 px.

Системные зависимости и requirements.txt

requirements.txt

В requirements.txt обязательно укажите SDK с суффиксом [server]: без него при деплое поставятся не те зависимости.

tropass-sdk[server]==2.1
pandas==2.3.3
requests==2.32.5

Рекомендации: фиксируйте точные версии, следите за конфликтами между пакетами, проверяйте совместимость с Python 3.12+. Если используете psycopg2, берите psycopg2-binary.

Системные пакеты (apt)

Если сервису нужны системные утилиты (ffmpeg, pandoc и т.п.), перечислите их в metadata.json, в секции system_dependencies:

{
  "model_name": "my_service",
  "model_service": "Мой сервис",
  "inputs": [],
  "outputs": [],
  "system_dependencies": {
    "extra_apt_packages": ["ffmpeg", "pandoc", "poppler-utils", "libgomp1"]
  }
}
ПакетНазначение
ffmpegобработка аудио и видео
pandocконвертация документов
poppler-utilsработа с PDF
libgomp1OpenMP для параллельных вычислений

Тестирование сервиса

Данные для тестов

Положите в input_examples/ один или несколько файлов в формате {"model_input": {...}, "common_resources": {...}}. Платформа использует input.json для автотеста после сборки, а вам такие файлы заодно документируют контракт сервиса. Для отладки в самом main.py удобно держать закомментированный блок с тестовыми model_input и common_resources (как в примере).

Прогон без сервера: run_test.py (рекомендуемый способ)

Лёгкий раннер вызывает main() напрямую, без uvicorn. Положите run_test.py рядом с main.py (готовый файл есть в примере) и запустите python3 run_test.py или python3 run_test.py test_input_2.json. Он читает файл из input_examples/, подставляет локальный каталог для результатов, вызывает main() и сохраняет ответ в response.json:

result = asyncio.run(main(model_input, common_resources))
with open(os.path.join(files_dir, "response.json"), "w", encoding="utf-8") as f:
    json.dump(result, f, ensure_ascii=False, indent=2, default=str)

Проверка через сервер (uvicorn + curl)

Локальное тестирование делается с библиотекой tropass-sdk. Из корня проекта запустите сервер:

uvicorn main:application --host 0.0.0.0 --port 8000 --workers 4

Через несколько секунд (до 10) отправьте запрос:

curl -X 'POST' 'http://localhost:8000/prediction' \
  -H 'accept: application/json' \
  -H 'X-User-Locale: ru' \
  -H 'Content-Type: application/json' \
  -d '{
  "model_input": {
    "text_input": ["Тут ваш текст"],
    "selector_input": ["Выбор +100500"],
    "number_input": [123.0],
    "checkbox_input": [true],
    "request_metadata": {"user_id": "<user_uuid>", "locale": "ru", "user_api_token": "<api_token>"}
  },
  "common_resources": {
    "model_api_gateway_params": {"url": "https://апи.тропасс.рф/api/rpc/call-model", "api_token": "<api_token>"},
    "files_directory_path": "/tmp/test_response"
  }
}'

Если сервис вызывает другие сервисы или LLM, подставьте в common_resources реальные параметры из вашего окружения (токены и пароли - только из своих секретов, не из репозитория). Каталог из files_directory_path должен существовать.

Сборка и деплой

  1. Убедитесь, что на месте metadata.json, main.py, requirements.txt и, если используются, utils.py, modules/, input_examples/, output_examples/.

  2. Проверьте requirements.txt (tropass-sdk[server]==2.1) и протестируйте сервис локально.

  3. Соберите архив скриптом ./build.sh из примера: он создаёт deploy/archive.zip и копирует в deploy/ файл metadata.json. Вручную то же делается так:

    zip -r deploy/archive.zip main.py utils.py requirements.txt modules input_examples output_examples
    

    Внутри архива main.py и requirements.txt должны лежать в корне:

    archive.zip
    ├── main.py
    ├── utils.py
    ├── requirements.txt
    ├── modules/
    │   └── __init__.py
    ├── input_examples/
    │   └── input.json
    └── output_examples/
    
  4. Войдите в админку платформы под ролью аналитика и откройте раздел «Модели аналитика», затем нажмите создание новой модели.

  5. Заполните форму:

    • Название модели и Техническое описание - произвольный понятный текст;
    • Язык модели - Python; Версия Python и Менеджер пакетов - выберите из списка (должны подходить вашему requirements.txt);
    • Архив модели (ZIP) - загрузите archive.zip или вставьте прямую HTTP(S)-ссылку на него;
    • Metadata (JSON) - загрузите ваш metadata.json.
  6. Нажмите «Сохранить». Платформа соберёт сервис; после сборки она прогонит input.json как автотест. Статус сборки и логи видны в карточке модели.

  7. Когда сборка прошла, сервис можно запускать: из интерфейса платформы или из своего кода, см. раздел «Где взять API-ключ и model_id».

Сколько занимает сборка и какие ресурсы выбирать

Сборка Python-сервиса с pandas и несколькими пакетами занимает около 10-12 минут, после неё сервис запускается за несколько секунд. Не переименовывайте сервис за это время (см. «Известные проблемы»).

Ресурсы: начинайте с небольших запросов (порядка 0,1 CPU и 0,5 ГБ памяти) и лимитов 1-2 CPU и 1-2 ГБ памяти; тяжёлым сервисам (OCR, обработка больших файлов) нужно больше. Завышенные запросы увеличивают время ожидания места под сервис и расходуют общие ресурсы платформы. Если сервис перезапускается из-за нехватки памяти, поднимайте лимит памяти, а не запрос.

Чек-лист перед деплоем

  • metadata.json содержит model_name, model_service, inputs, outputs
  • model_name уникален, на латинице
  • В requirements.txt указана tropass-sdk[server]==2.1
  • main() возвращает MLModelResponseSchema(...).model_dump()
  • Все panel_output_name совпадают с indicator_name из outputs
  • plot_data заполнен для графических панелей; для text-панелей primary_data=None либо заглушка plot_data
  • Текст панелей лежит в descriptions[].content, description_type - md/html/plain
  • Файлы результата лежат в files_directory_path и указаны в media[].local_abs_path или attachments[].local_abs_path
  • При вызовах шлюза: model_id - UUID, значения инпутов списками, Idempotency-Key уникален, пользовательский токен приоритетнее сервисного
  • s3_url из ответов скачивается сразу (ссылка живёт около часа)
  • Клиентские таймауты для тяжёлых сервисов 300-600 с, ретраи только на transient-ошибки
  • Локальное тестирование пройдено (run_test.py и/или uvicorn + curl)
  • В коде и input.json нет секретов
  • Создан archive.zip, в нём есть все нужные файлы

Частые вопросы

Как передать несколько значений в одном поле? Используйте тип multi-select (или multi-file для файлов).

Как сохранить файл, созданный в сервисе? Скопируйте или создайте его в files_directory_path и укажите в media (панели image/video) или attachments (любая панель, в том числе file).

Как вызвать другой сервис ТРОПАСС? Через model gateway: параметры берутся из model_api_gateway_params.

Как вызвать LLM? Через LLM-вызовы: параметры из big/medium/small_llm_api_params.

Почему не отображается панель или график? Проверьте, что panel_output_name совпадает с indicator_name в metadata.json, а plot_data содержит числовые значения.

Почему не доехал текст панели? Текст нужно передавать в descriptions[].content: поля text_content в primary_data нет, оно молча игнорируется.

Известные проблемы

Список того, о чём нужно знать до загрузки сервиса. Обновляется по мере исправления.

ТемаЧто делать
Health checkПараметр «Включить health check» при загрузке сервиса пока лучше не включать.
ПереименованиеСервис пока не переименовываем во время сборки: название модели не меняйте, пока сборка не закончилась (статус VALID или ошибка).

Нашли что-то, чего здесь нет? Сообщите нам (см. ниже), и мы добавим это в список.

Обратная связь и предложения

  • Форма: оставить предложение или сообщить об ошибке. Подходит для идей по функционалу, ошибок в документации, вопросов, которые остались без ответа, и замечаний к сборке и деплою. Форма открыта без входа в систему.
  • Почта: dev@tropass.me. Если удобнее написать письмом, укажите в нём то же самое, что просит форма: что хотите изменить или что не получилось, в каком разделе документации и название сервиса с версией.

Опишите задачу конкретно, а при ошибке приложите версию tropass-sdk и текст ошибки из карточки модели.

Код примера

Полный рабочий сервис, он же шаблон, лежит в репозитории tropass-ai/hello-tropass: main.py, utils.py, run_test.py, тесты и build.sh.

Разработка сервисов в ТРОПАСС | Новости | Тропасс