28 KiB
Переменные и макросы в НадTavern: очень понятный гайд
Этот файл объясняет, как в проекте подставлять нужные кусочки данных прямо в шаблоны, не ломая голову. Представьте «наклейки» — вы клеите их в JSON на нужные места, и система сама подменяет их на значения: из входящего запроса, из предыдущих узлов, из ваших переменных, из памяти выполнения.
Если вы открыли визуальный редактор по адресу http://127.0.0.1:7860/ui/editor.html — все примеры ниже можно копировать прямо туда.
Файлы, где «живет» эта магия:
- Сервер и конечные точки: agentui/api/server.py
- Исполнитель узлов (сердце конвейера): PipelineExecutor.run()
- Ноды: ProviderCallNode.run(), RawForwardNode.run(), ReturnNode.run(), IfNode.run(), SetVars внутри того же файла
- Шаблонизатор (подстановки ... и {{ ... }}): render_template_simple(), булевы выражения If: eval_condition_expr()
— — —
- Что такое «переменные» и «макросы»
- Переменные — это значения, которые вы где-то определили: в узле SetVars, они появились из входящего запроса, их вернул провайдер (OpenAI/Gemini/Claude), или их сохранил движок выполнения в «память».
- Макросы — короткие «заклинания» вида ... или {{ ... }}, которые вы вставляете прямо в текст/JSON. На ходу они заменяются на нужные значения.
Простейший пример:
- Было в шаблоне: "Authorization":"Bearer VAR:incoming.headers.authorization"
- Стало при выполнении: "Authorization":"Bearer eyJhbGciOi..."
— — —
- Откуда берутся данные (куда «лезут» макросы)
-
incoming.* — всё про входящий HTTP‑запрос клиента:
- method, url, path, query
- headers — словарь заголовков
- json — JSON из тела запроса
- Быстро взять: VAR:incoming.headers.authorization, VAR:incoming.json.model
- Формируется на сервере: build_macro_context()
-
params.* — «нормализованные» параметры (температура, топ‑п и т.д.):
- Примеры: VAR:params.temperature, VAR:params.max_tokens
- См. нормализацию: normalize_to_unified()
-
model, system, vendor_format — общие поля запроса:
-
chat.* — удобный доступ к сообщениям:
- VAR:chat.last_user — текст последнего пользователя
- VAR:chat.messages — весь список сообщений
-
vars.* — ваши переменные из SetVars:
- Можно писать коротко, без VAR и точек: MY_VAR или {{ MY_VAR }}
- Задаются в узле SetVars (подробно ниже)
-
OUT.* — машина времени с результатами узлов:
- OUT1 — «лучший текст» из узла n1 (короткая форма)
- OUT:n2.result — весь JSON результата узла n2
- OUT:n3.result.choices.0.message.content — вложенный путь в JSON
- Как извлекается «лучший текст», описано ниже (раздел «OUTx внутри»)
-
STORE.* — постоянное хранилище (между шагами пайплайна):
- STORE:MY_VAR — значение переменной, сохранённой узлами (например SetVars)
- VAR:STORE.snapshot.OUT_TEXT.n1 — текст, который движок сохранил как «лучший» для узла n1
- Доступ фигурными скобками тоже работает: {{ store.snapshot.OUT_TEXT.n1 }}
— — —
- Главные «заклинания» (макросы)
-
VAR:путь — взять значение из контекста
-
OUT1, OUT2, … — быстро взять «лучший» текст из узла n1, n2, …
- Удобно для Return и подсказок к следующему провайдеру
-
OUT:nX.что‑то — взять сырой JSON из узла и провалиться по пути
- OUT:n2.result — весь ответ провайдера
- OUT:n2.result.candidates.0.content.parts.0.text — кусочек Gemini‑ответа
-
PROMPT — хитрый фрагмент JSON из Prompt Blocks
- В ноде ProviderCall он разворачивается в правильные поля, в зависимости от провайдера:
- OpenAI → "messages": [...]
- Gemini → "contents": [...], "systemInstruction": {...}
- Claude → "system": "...", "messages": [...]
- Под капотом это делает: ProviderCallNode.run()
- В ноде ProviderCall он разворачивается в правильные поля, в зависимости от провайдера:
-
{{ путь }} — «фигурные скобки» возвращают значение без кавычек
- Подходит для чисел/массивов/объектов. Пример: "temperature": {{ params.temperature|default(0.7) }}
- Есть фильтр по умолчанию: |default(значение) — безопасно подставит запасной вариант
- Разбирает наш шаблонизатор: render_template_simple()
-
NAME — «голая» переменная из SetVars (короткая запись)
- Ищется сперва в ваших vars, потом в общем контексте
-
STORE:путь — достать из постоянного хранилища (store.*)
Подсказка: макросы ... можно писать в строках JSON, а {{ ... }} — там, где нужно вставить число/массив/объект без кавычек.
— — —
- Куда именно это писать (по узлам)
4.1 ProviderCall — «собрать запрос к провайдеру»
Где живут макросы:
- endpoint — можно вставлять {{ model }} или ключи в URL (например Gemini ?key=…)
- headers — это JSON‑объект в текстовом поле: сюда можно писать VAR:incoming.headers.authorization, Clod и т.д.
- template — главное поле: JSON‑тело запроса. Обязательно валидный JSON после разворачивания макросов.
Мини‑шпаргалка:
- OpenAI (пример заголовка): {"Authorization":"Bearer VAR:incoming.headers.authorization"}
- Gemini (ключ в URL): /v1beta/models/{{ model }}:generateContent?key=VAR:incoming.api_keys.key
- Claude (заголовки): {"x-api-key":"VAR:incoming.headers.x-api-key","anthropic-version":"2023-06-01"}
Полный пример OpenAI‑тела: { "model": "{{ model }}", PROMPT, "temperature": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }}, "top_p": {{ incoming.json.top_p|default(params.top_p|default(1)) }}, "max_tokens": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }}, "stop": {{ incoming.json.stop|default(params.stop|default([])) }}, "stream": {{ incoming.json.stream|default(false) }} }
Важное правило:
- PROMPT — это «кусок» JSON без запятых по краям. Убедитесь, что вокруг него стоят запятые там, где нужно, но не лишние.
- Если шаблон не превращается в валидный JSON — ProviderCall упадёт с понятной ошибкой.
4.2 RawForward — «пропусти запрос как есть» (reverse proxy)
Куда писать:
- base_url — куда слать (можно с макросами)
- override_path — переопределить путь (опционально)
- extra_headers — JSON‑объект (строкой), можно вставлять ... и {{ ... }}
По умолчанию узел пробрасывает заголовки клиента (кроме Host/Content‑Length) и тело JSON «как есть». Это удобно, если вы хотите просто дойти до провайдера без сложной сборки, но при этом подмешать один‑два заголовка.
4.3 Return — «оформить финальный ответ в стиле клиента»
- target_format: auto/openai/gemini/claude — во что завернуть ответ
- text_template — обычно "OUT1", но можно собрать возврат из нескольких кусков:
- "Вот ваш результат: OUT2"
Return сам завернёт текст в правильную структуру. Пример OpenAI‑ответа возвращается как "choices[0].message.content".
4.4 If — «ветвление по условию»
Пишем одно выражение, которое возвращает true/false. Поддерживаются:
- Логика: &&, ||, ! (not)
- Сравнения: ==, !=, <, <=, >, >=
- Ключевое слово contains — проверка подстроки или вхождения в список
- Макросы ... и {{ ... }} прямо в выражении
Примеры:
- OUT1 contains "Красиво"
- {{ OUT.n6.response_text|default('') }} != ""
- {{ params.temperature|default(0.7) }} >= 0.3 && {{ params.temperature|default(0.7) }} <= 1
- !(OUT3 contains "error")
Вычислитель выражений: eval_condition_expr()
4.5 SetVars — «завести свои переменные»
Два режима на каждую переменную:
- mode=string — строка после разворачивания макросов (обычный случай)
- mode=expr — мини‑формула (безопасная), в которой доступны специальные функции.
Доступные безопасные функции в expr:
- from_json(x)
- Парсит JSON‑строку в объект (если в строке макросы — они сначала подставятся)
- jp(value, path, join_sep="\n")
- Достаёт данные по «dot‑JSONPath» (индексы и звёздочка поддерживаются)
- Примеры путей: a.b.0.c, items.*.title
- jp_text(value, path, join_sep="\n")
- То же, что jp, но вернёт строку: если массив — склеит через join_sep
Готовые рецепты:
-
Извлечь текст из ответа провайдера в переменную TXT и вернуть:
- name: TXT
- mode: expr
- value: jp_text(OUT:n2.result, 'candidates.0.content.parts.0.text')
- Return.text_template: "TXT"
-
Парсинг JSON‑строки и взять число:
- name: VAL
- mode: expr
- value: jp(from_json('{"a":{"b":[{"x":1},{"x":2}]}}'), 'a.b.1.x')
- результат: 2
-
Собрать несколько полей в строку:
- name: TITLES
- mode: expr
- value: jp_text(OUT:n4.result, 'items.*.title', ' | ')
- Return: "TITLES" → "Title1 | Title2 | Title3"
Подсказки:
- Если путь возвращает несколько элементов — используйте jp_text, чтобы сразу получить склейку.
- Если у вас уже объект/массив (не строка) — можно сразу передать его в jp/jp_text без from_json.
- Внутри expr макросы пишите внутри кавычек (в аргументах функций) — они развернутся перед вычислением.
Ограничения безопасности в expr:
- Разрешены только литералы, базовая арифметика/логика, сравнения и ф‑ции rand(), randint(a,b), choice(list), from_json(), jp(), jp_text().
- Доступ к произвольным атрибутам/импортам Python запрещён.
— — —
- Где в редакторе быстро подсмотреть макросы
Вверху справа есть кнопка «ПЕРЕМЕННЫЕ». Откроется панель:
- Выберите «snapshot», чтобы видеть всю «картину мира» последнего запуска (incoming, params, OUT_TEXT, алиасы OUT1/OUT2 и т.д.)
- Клик по строке копирует готовый макрос в буфер (можно переключить режим «фигурные» — для {{ ... }})
- Поиск работает и по именам, и по значениям
За поведение панели отвечают страничка и скрипты:
— — —
- Что на самом деле делает OUTx (как выбирается «лучший текст»)
Когда нода (ProviderCall или RawForward) получила JSON от провайдера, движок старается «вынуть» из него удобный текст:
- OpenAI: choices[0].message.content
- Gemini: candidates[0].content.parts[0].text
- Claude: content[].text (склейка)
- Если формат неизвестен — идёт «лучший догадчик» по глубине, чтобы найти текст
- Можно задать пресет (JSONPath) в настройках ноды или глобально в «Запуск» → «Пресеты парсинга OUTx»
Логика извлечения и пресетов находится в: ProviderCallNode.run(), RawForwardNode.run()
Для продвинутых: глобальные мета‑настройки пайплайна (timeouts, стратегия, пресеты) редактируются в «Запуск», а сохраняются в pipeline.json.
— — —
- Безопасность (прочитать обязательно)
- Не храните настоящие API‑ключи в файлах пайплайна/пресетах. Передавайте их через заголовки клиента:
- OpenAI: Authorization: Bearer XXXXXX
- Anthropic: x-api-key: XXXXXX
- Gemini: ?key=XXXXXX в URL
- В шаблонах используйте значения из входящего запроса: VAR:incoming.headers.authorization, VAR:incoming.headers.x-api-key, VAR:incoming.api_keys.key
- Логи в редакторе показывают запросы/ответы целиком. Для продакшена отключайте/маскируйте чувствительные части.
- Прокси и TLS‑проверка (если нужно сниффить трафик/пробрасывать через Burp) настраиваются здесь: agentui/config.py, HTTP‑клиент: build_client()
— — —
- Рецепты «скопируй и вставь»
8.1 Самый короткий пайплайн «получил → отправил к OpenAI → вернул»
- Узел ProviderCall:
- provider: openai
- headers: {"Authorization":"Bearer VAR:incoming.headers.authorization"}
- template — возьмите из примера OpenAI выше (с PROMPT)
- Prompt Blocks:
- system: «Ты — помощник. Отвечай коротко.»
- user: «VAR:chat.last_user — перепиши аккуратнее»
- Узел Return:
- target_format: auto
- text_template: "OUT1"
8.2 Gemini через ключ в URL, без заголовков
- ProviderCall:
- provider: gemini
- endpoint: /v1beta/models/{{ model }}:generateContent?key=VAR:incoming.api_keys.key
- headers: {}
- template — пример Gemini выше (с PROMPT)
8.3 Склейка двух ответов и проверка условием If
- ProviderCall(n2) и ProviderCall(n3) что‑то генерят
- ProviderCall(n6): user prompt: «Объедини OUT3 и OUT2 красиво. Напиши слово "Красиво" в конце.»
- If(n7): expr = OUT6 contains "Красиво"
- Return: text_template = "OUT6"
— — —
- Где это в коде (для любопытных)
- Создание приложения/роутов: create_app()
- Нормализация payload (OpenAI/Gemini/Claude → единый вид): normalize_to_unified()
- Контекст для макросов (incoming/chat/params/…): build_macro_context()
- Исполнитель конвейера и «волны»: PipelineExecutor.run()
- Узел вызова провайдера (сборка PROMPT, лог HTTP): ProviderCallNode.run()
- Прямой форвард запроса: RawForwardNode.run()
- Финализация ответа под формат клиента: ReturnNode.run()
- Условия If (contains, &&, ||, ! и макросы): IfNode.run(), eval_condition_expr()
- Шаблонизатор макросов ... и {{ ... }}: render_template_simple()
- Определение провайдера по форме JSON: detect_vendor()
— — —
Всё. Старайтесь думать так: «Мне нужен кусочек нужного значения в нужное место». Найдите его в панели «ПЕРЕМЕННЫЕ», скопируйте макрос — и вклейте. Если «кусочков» несколько — заведите переменные SetVars и соберите их в строку. Если сомневаетесь — начните с Return и OUT1: это самый короткий путь увидеть, что реально получается. — — —
Приложение A. SetVars: функции и операции в mode=expr (простым языком)
Где это запрограммировано
- Логика разбора и разрешённые операции реализованы в SetVarsNode._safe_eval_expr(). Если коротко: там «белый список» безопасных функций и операций, всё остальное запрещено.
- Подстановка макросов внутри строк (для функций from_json/jp/jp_text) делает render_template_simple().
Что можно писать в mode=expr
- Случайные значения
-
rand() — число от 0 до 1 (типа 0.0–1.0) Пример: 0.35, 0.92 …
-
randint(a, b) — целое в диапазоне [a; b] включительно Примеры:
- randint(1, 6) → бросок кубика d6
- randint(100, 999) → трёхзначный код
-
choice(list_or_tuple) — случайный элемент из списка/кортежа Примеры:
- choice(['ru','en','de'])
- choice((128, 256, 512))
- Арифметика (как в обычных формулах)
-
Операции: +, -, *, /, //, %
- / — деление с плавающей точкой
- // — целочисленное деление (отбрасывает дробную часть)
- % — остаток от деления
-
Скобки и унарные знаки:
- (1 + 2) * 3
- -5, +7
Примеры:
- 9.99 * 1.2 → 11.988
- (rand() * 100) // 1 → псевдо‑процент от 0 до 99
- randint(1, 6) + 10 → 11..16
- Логика и сравнения
- Логические: and, or
- Сравнения: ==, !=, <, <=, >, >=
- Можно «цепочкой»: 1 < x <= 10
Примеры:
- (rand() > 0.5) and (choice([True, False]) or True)
- 120 <= choice([64,128,256]) and 256 >= 200 → True
- Литералы (что можно писать напрямую)
- Числа: 42, 3.14
- Строки: 'привет', "hello"
- Списки/кортежи/словарики: [1,2,3], (1,2,3), {'a':1, 'b':2}
- Логические и null‑подобные: True, False, None
Отдельный режим: «чистый JSON»
- Если ВЕСЬ expr — корректный JSON (и только он), он принимается как значение.
Примеры:
- {"a":{"b":[{"x":1},{"x":2}]}}
- ["ru","en","de"]
- true / false / null
- Внутри «обычных» выражений используйте Python‑формы True/False/None.
- Макросы внутри expr — как правильно Важно: «голые» макросы внутри expr не работают. Подставлять их можно ТОЛЬКО как строки‑аргументы специальных функций from_json/jp/jp_text — они сначала разворачиваются в текст, потом парсятся.
Примеры (готовы к копипасте):
-
Взять JSON из предыдущей ноды и достать одно число: name: SCORE mode: expr value: jp(from_json('OUT:n2.result'), 'meta.score')
-
Вытащить текст из OpenAI‑ответа и склеить варианты через двойной перенос: name: TEXTS mode: expr value: jp_text(from_json('OUT:n6.result'), 'choices.*.message.content', '\n\n')
-
Разобрать JSON из входящего запроса: name: UID mode: expr value: jp(from_json('VAR:incoming.json'), 'user.id')
-
Подставить фигурные скобки как строку и распарсить: name: LIMIT mode: expr value: jp(from_json('{{ params|default({"max_tokens":256}) }}'), 'max_tokens')
Доступные функции (подробнее)
-
from_json(x)
- Принимает строку JSON (в т.ч. со вставленными макросами), возвращает объект/массив/число/строку/bool/None.
- Удобно, когда источник — текст, а работать хочется с полями.
-
jp(value, path, join_sep="\n")
- «Точечный JSONPath»: a.b.0.c, items.*.title
- Возвращает значение или список значений (если в пути есть «*»)
- join_sep здесь игнорируется (имеет смысл в jp_text)
-
jp_text(value, path, join_sep="\n")
- То же, но всегда возвращает строку. Если значений несколько — склеит через join_sep.
-
rand(), randint(a,b), choice(list_or_tuple)
- Случайности без сидирования (не детерминированы).
- randint — границы включительные. choice — список/кортеж не должен быть пустым.
Чего делать нельзя (и почему)
- Нельзя любые другие функции: len(), sum(), not, print(), и т.п. — «белый список» (смотри SetVarsNode._safe_eval_expr())
- Нельзя обращаться к переменным/атрибутам по именам (кроме ваших NAME в шаблонах вне expr)
- Нельзя генераторы/списки‑выражения, **kwargs, *args
- Нельзя «голые» макросы внутри expr: ... или {{ ... }} нужно класть строкой в аргумент from_json/jp/jp_text
Быстрая шпаргалка «на каждый день»
- Бросок кубика d6
- name: DICE
- mode: expr
- value: randint(1, 6)
- Случайный стиль ответа
- name: TONE
- mode: expr
- value: choice(['официально','дружелюбно','коротко'])
- Выбрать лимит токенов
- name: TOKENS
- mode: expr
- value: choice((128, 256, 512))
- Наценка 20%
- name: PRICE_WITH_VAT
- mode: expr
- value: price * 1.2 ← если price — ваша строковая переменная, используйте вне expr (в шаблоне) {{ PRICE }}; внутри expr внешние имена не видны
- Вытянуть текст из ответа модели n6
- name: ANSWER
- mode: expr
- value: jp_text(from_json('OUT:n6.result'), 'choices.0.message.content')
Подсказка по ошибкам
- Function X is not allowed — вызвали неразрешённую функцию
- choice() expects list or tuple — передали не список/кортеж
- choice() on empty sequence — пустой список
- randint invalid arguments — границы не числа или a > b
- Expression not allowed — в выражении есть запрещённые элементы (например, not/len/компрехеншены)
Если видите ошибку — упростите выражение: разбейте задачу на 2 переменные SetVars (сначала jp/from_json, потом математика/выбор), либо перенесите часть логики в шаблон {{ ... }} (но помните: в шаблоне нет rand/randint/choice).
Где посмотреть действующие значения
- В редакторе нажмите «ПЕРЕМЕННЫЕ»: там видно STORE (включая snapshot OUT_TEXT и алиасы OUT1/OUT2). Клик по строке — копирование макроса для вставки. Схема работы панели описана в static/editor.html и скриптах static/js/serialization.js, static/js/pm-ui.js.