# Переменные и макросы в НадTavern: очень понятный гайд Этот файл объясняет, как в проекте подставлять нужные кусочки данных прямо в шаблоны, не ломая голову. Представьте «наклейки» — вы клеите их в JSON на нужные места, и система сама подменяет их на значения: из входящего запроса, из предыдущих узлов, из ваших переменных, из памяти выполнения. Если вы открыли визуальный редактор по адресу http://127.0.0.1:7860/ui/editor.html — все примеры ниже можно копировать прямо туда. Файлы, где «живет» эта магия: - Сервер и конечные точки: [agentui/api/server.py](agentui/api/server.py) - Исполнитель узлов (сердце конвейера): [PipelineExecutor.run()](agentui/pipeline/executor.py:170) - Ноды: [ProviderCallNode.run()](agentui/pipeline/executor.py:1631), [RawForwardNode.run()](agentui/pipeline/executor.py:1939), [ReturnNode.run()](agentui/pipeline/executor.py:2256), [IfNode.run()](agentui/pipeline/executor.py:2350), SetVars внутри того же файла - Шаблонизатор (подстановки [[...]] и {{ ... }}): [render_template_simple()](agentui/pipeline/templating.py:191), булевы выражения If: [eval_condition_expr()](agentui/pipeline/templating.py:336) — — — 1) Что такое «переменные» и «макросы» - Переменные — это значения, которые вы где-то определили: в узле SetVars, они появились из входящего запроса, их вернул провайдер (OpenAI/Gemini/Claude), или их сохранил движок выполнения в «память». - Макросы — короткие «заклинания» вида [[...]] или {{ ... }}, которые вы вставляете прямо в текст/JSON. На ходу они заменяются на нужные значения. Простейший пример: - Было в шаблоне: "Authorization":"Bearer [[VAR:incoming.headers.authorization]]" - Стало при выполнении: "Authorization":"Bearer eyJhbGciOi..." — — — 2) Откуда берутся данные (куда «лезут» макросы) - incoming.* — всё про входящий HTTP‑запрос клиента: - method, url, path, query - headers — словарь заголовков - json — JSON из тела запроса - Быстро взять: [[VAR:incoming.headers.authorization]], [[VAR:incoming.json.model]] - Формируется на сервере: [build_macro_context()](agentui/api/server.py:143) - params.* — «нормализованные» параметры (температура, топ‑п и т.д.): - Примеры: [[VAR:params.temperature]], [[VAR:params.max_tokens]] - См. нормализацию: [normalize_to_unified()](agentui/api/server.py:44) - model, system, vendor_format — общие поля запроса: - [[VAR:model]], [[VAR:system]], [[VAR: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 }} — — — 3) Главные «заклинания» (макросы) - [[VAR:путь]] — взять значение из контекста - Примеры: [[VAR:incoming.headers.authorization]], [[VAR:params.temperature]] - [[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()](agentui/pipeline/executor.py:1631) - {{ путь }} — «фигурные скобки» возвращают значение без кавычек - Подходит для чисел/массивов/объектов. Пример: "temperature": {{ params.temperature|default(0.7) }} - Есть фильтр по умолчанию: |default(значение) — безопасно подставит запасной вариант - Разбирает наш шаблонизатор: [render_template_simple()](agentui/pipeline/templating.py:191) - [[NAME]] — «голая» переменная из SetVars (короткая запись) - Ищется сперва в ваших vars, потом в общем контексте - [[STORE:путь]] — достать из постоянного хранилища (store.*) Подсказка: макросы [[...]] можно писать в строках JSON, а {{ ... }} — там, где нужно вставить число/массив/объект без кавычек. — — — 4) Куда именно это писать (по узлам) 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()](agentui/pipeline/templating.py:336) 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 запрещён. — — — 5) Где в редакторе быстро подсмотреть макросы Вверху справа есть кнопка «ПЕРЕМЕННЫЕ». Откроется панель: - Выберите «snapshot», чтобы видеть всю «картину мира» последнего запуска (incoming, params, OUT_TEXT, алиасы OUT1/OUT2 и т.д.) - Клик по строке копирует готовый макрос в буфер (можно переключить режим «фигурные» — для {{ ... }}) - Поиск работает и по именам, и по значениям За поведение панели отвечают страничка и скрипты: - [static/editor.html](static/editor.html) - [static/js/serialization.js](static/js/serialization.js) - [static/js/pm-ui.js](static/js/pm-ui.js) — — — 6) Что на самом деле делает [[OUTx]] (как выбирается «лучший текст») Когда нода (ProviderCall или RawForward) получила JSON от провайдера, движок старается «вынуть» из него удобный текст: - OpenAI: choices[0].message.content - Gemini: candidates[0].content.parts[0].text - Claude: content[].text (склейка) - Если формат неизвестен — идёт «лучший догадчик» по глубине, чтобы найти текст - Можно задать пресет (JSONPath) в настройках ноды или глобально в «Запуск» → «Пресеты парсинга OUTx» Логика извлечения и пресетов находится в: [ProviderCallNode.run()](agentui/pipeline/executor.py:1631), [RawForwardNode.run()](agentui/pipeline/executor.py:1939) Для продвинутых: глобальные мета‑настройки пайплайна (timeouts, стратегия, пресеты) редактируются в «Запуск», а сохраняются в [pipeline.json](pipeline.json). — — — 7) Безопасность (прочитать обязательно) - Не храните настоящие 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](agentui/config.py), HTTP‑клиент: [build_client()](agentui/providers/http_client.py:21) — — — 8) Рецепты «скопируй и вставь» 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]]" — — — 9) Где это в коде (для любопытных) - Создание приложения/роутов: [create_app()](agentui/api/server.py:270) - Нормализация payload (OpenAI/Gemini/Claude → единый вид): [normalize_to_unified()](agentui/api/server.py:44) - Контекст для макросов (incoming/chat/params/…): [build_macro_context()](agentui/api/server.py:143) - Исполнитель конвейера и «волны»: [PipelineExecutor.run()](agentui/pipeline/executor.py:170) - Узел вызова провайдера (сборка [[PROMPT]], лог HTTP): [ProviderCallNode.run()](agentui/pipeline/executor.py:1631) - Прямой форвард запроса: [RawForwardNode.run()](agentui/pipeline/executor.py:1939) - Финализация ответа под формат клиента: [ReturnNode.run()](agentui/pipeline/executor.py:2256) - Условия If (contains, &&, ||, ! и макросы): [IfNode.run()](agentui/pipeline/executor.py:2350), [eval_condition_expr()](agentui/pipeline/templating.py:336) - Шаблонизатор макросов [[...]] и {{ ... }}: [render_template_simple()](agentui/pipeline/templating.py:191) - Определение провайдера по форме JSON: [detect_vendor()](agentui/common/vendors.py:8) — — — Всё. Старайтесь думать так: «Мне нужен кусочек нужного значения в нужное место». Найдите его в панели «ПЕРЕМЕННЫЕ», скопируйте макрос — и вклейте. Если «кусочков» несколько — заведите переменные SetVars и соберите их в строку. Если сомневаетесь — начните с Return и [[OUT1]]: это самый короткий путь увидеть, что реально получается. — — — Приложение A. SetVars: функции и операции в mode=expr (простым языком) Где это запрограммировано - Логика разбора и разрешённые операции реализованы в [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1092). Если коротко: там «белый список» безопасных функций и операций, всё остальное запрещено. - Подстановка макросов внутри строк (для функций from_json/jp/jp_text) делает [render_template_simple()](agentui/pipeline/templating.py:191). Что можно писать в mode=expr 1) Случайные значения - 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)) 2) Арифметика (как в обычных формулах) - Операции: +, -, *, /, //, % - / — деление с плавающей точкой - // — целочисленное деление (отбрасывает дробную часть) - % — остаток от деления - Скобки и унарные знаки: - (1 + 2) * 3 - -5, +7 Примеры: - 9.99 * 1.2 → 11.988 - (rand() * 100) // 1 → псевдо‑процент от 0 до 99 - randint(1, 6) + 10 → 11..16 3) Логика и сравнения - Логические: and, or - Сравнения: ==, !=, <, <=, >, >= - Можно «цепочкой»: 1 < x <= 10 Примеры: - (rand() > 0.5) and (choice([True, False]) or True) - 120 <= choice([64,128,256]) and 256 >= 200 → True 4) Литералы (что можно писать напрямую) - Числа: 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. 5) Макросы внутри 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()](agentui/pipeline/executor.py:1092)) - Нельзя обращаться к переменным/атрибутам по именам (кроме ваших [[NAME]] в шаблонах вне expr) - Нельзя генераторы/списки‑выражения, **kwargs, *args - Нельзя «голые» макросы внутри expr: [[...]] или {{ ... }} нужно класть строкой в аргумент from_json/jp/jp_text Быстрая шпаргалка «на каждый день» 1) Бросок кубика d6 - name: DICE - mode: expr - value: randint(1, 6) 2) Случайный стиль ответа - name: TONE - mode: expr - value: choice(['официально','дружелюбно','коротко']) 3) Выбрать лимит токенов - name: TOKENS - mode: expr - value: choice((128, 256, 512)) 4) Наценка 20% - name: PRICE_WITH_VAT - mode: expr - value: price * 1.2 ← если price — ваша строковая переменная, используйте вне expr (в шаблоне) {{ PRICE }}; внутри expr внешние имена не видны 5) Вытянуть текст из ответа модели 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/editor.html) и скриптах [static/js/serialization.js](static/js/serialization.js), [static/js/pm-ui.js](static/js/pm-ui.js).