# Переменные и макросы в Над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:316)
- Ноды: [ProviderCallNode.run()](agentui/pipeline/executor.py:2007), [RawForwardNode.run()](agentui/pipeline/executor.py:2477), [ReturnNode.run()](agentui/pipeline/executor.py:2798), [IfNode.run()](agentui/pipeline/executor.py:2892), SetVars внутри того же файла
- Шаблонизатор (подстановки [[...]] и {{ ... }}): [render_template_simple()](agentui/pipeline/templating.py:196), булевы выражения If: [eval_condition_expr()](agentui/pipeline/templating.py:382)
 
— — —
 
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: все parts[].text во всех candidates склеиваются через "\n" (пустые/пробельные части игнорируются)
- Claude: content[].text (склейка)
- Если формат неизвестен — идёт «лучший догадчик» по глубине, чтобы найти текст
- Можно задать пресет (JSONPath) в настройках ноды или глобально в «Запуск» → «Пресеты парсинга OUTx»; для Gemini альтернатива авто: json_path="candidates.*.content.parts.*.text", join_sep="\n"
 
Логика извлечения и пресетов находится в: [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:316)
- Узел вызова провайдера (сборка [[PROMPT]], лог HTTP): [ProviderCallNode.run()](agentui/pipeline/executor.py:2007)
- Прямой форвард запроса: [RawForwardNode.run()](agentui/pipeline/executor.py:2477)
- Финализация ответа под формат клиента: [ReturnNode.run()](agentui/pipeline/executor.py:2798)
- Условия If (contains, &&, ||, ! и макросы): [IfNode.run()](agentui/pipeline/executor.py:2892), [eval_condition_expr()](agentui/pipeline/templating.py:382)
- Шаблонизатор макросов [[...]] и {{ ... }}: [render_template_simple()](agentui/pipeline/templating.py:196)
- Определение провайдера по форме 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 &lt; x &lt;= 10

Примеры:
- (rand() &gt; 0.5) and (choice([True, False]) or True)
- 120 &lt;= choice([64,128,256]) and 256 &gt;= 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 &gt; 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).

10) Работа с изображениями (ProviderCall + переменные)

Обзор
- Теперь можно:
  - хранить изображения в переменных SetVars в виде data URL;
  - вставлять изображения в Prompt Blocks через Markdown-нотацию;
  - вызывать провайдеров OpenAI / Claude / Gemini с мультимодальными сообщениями.

Как хранить изображения в переменных
- В SetVars (mode=expr) доступны новые безопасные функции:
  - file_b64(path) — читает файл и возвращает base64-строку (без префикса).
  - data_url(b64, mime) — собирает data URL: data:mime;base64,&lt;b64&gt;.
  - file_data_url(path, mime?) — обёртка: читает файл, определяет mime по расширению (если не указан) и возвращает полноценный data URL.
- Примеры SetVars:
  - name: IMG1
    mode: expr
    value: file_data_url('static/samples/cat.png','image/png')
  - name: IMG2
    mode: expr
    value: data_url(file_b64('static/samples/dog.jpg'), 'image/jpeg')
- После этого [[IMG1]] / [[IMG2]] вернут строку-полный data URL, пригодный для мультимодальных LLM.

Как вставлять картинки в Prompt Blocks
- Внутри блока (system/user/assistant) используйте Markdown-нотацию:
  - ![alt]([[IMG1]])
  - ![alt](https://example.org/pic.png)
  - ![alt](data:image/webp;base64,....)
- Во время исполнения ProviderCall блок превращается в список частей:
  - текстовые сегменты → {"type":"text","text":"..."}
  - изображения → {"type":"image_url","url":"..."}
- Для plain‑текста ничего не меняется (обратная совместимость полностью сохранена).

Маппинг для провайдеров
- OpenAI (chat.completions):
  - message.content → либо строка, либо массив частей:
    - {"type":"text","text":"..."}
    - {"type":"image_url","image_url":{"url":"<http(s) или data:...>"}}
- Claude (messages v2023‑06‑01):
  - message.content — массив блоков:
    - {"type":"text","text":"..."}
    - {"type":"image","source":{"type":"url","url":"..."}} — для http(s)
    - {"type":"image","source":{"type":"base64","media_type":"image/png","data":"..."}} — для data URL
- Gemini (generateContent):
  - contents[].parts — микс:
    - {"text":"..."}
    - {"inline_data":{"mime_type":"image/png","data":"&lt;base64&gt;"}} — для data URL
  - http/https URL напрямую не инлайнится; либо преобразуйте в data URL, либо реализуйте внешнюю загрузку (не требуется для базовой поддержки).

Мини-рецепт
1) SetVars:
   - name: IMG
   - mode: expr
   - value: file_data_url('static/pictures/cat.png', 'image/png')
2) ProviderCall (OpenAI/Gemini/Claude), Prompt Blocks, user:
   - Опиши картинку ниже кратко.
   - ![cat]([[IMG]])

Примечания
- Никакого тримминга больших строк и base64 по умолчанию не применяется (по пожеланию). Храните картинки разумного размера.
- JSON‑тело ProviderCall остаётся валидным JSON после развёртывания [[PROMPT]], т.к. преобразование в «части» выполняется до формирования payload.
- Для OpenAI и Claude можно использовать как http/https URL, так и data URL. Для Gemini предпочтительно data URL (inline_data).

Где реализовано
- Backend преобразование блоков и маппинг под провайдеров — см. файл agentui/pipeline/executor.py.
- Новые функции expr для SetVars — см. тот же файл внутри SetVarsNode._safe_eval_expr().
- Шаблонизатор как и раньше отвечает за развёртку [[...]] / {{ ... }} — см. agentui/pipeline/templating.py.


10.1) Супер‑короткая запись для картинок: img(mime)[[...]]

Задача
- У вас есть base64‑строка (без префикса data:), например вы её извлекли пресетом JSONPath: candidates.0.content.parts.1.inlineData.data.
- Хотите получить ПОЛНЫЙ data URL без плясок с expr и функциям SetVars — прямо в обычной строке шаблона.

Решение (новый синтаксис)
- Пишите: img(mime)[[МАКРОС_С_BASE64]]
- На выходе получится строка: data:&lt;mime&gt;;base64,&lt;ваш_base64&gt;

Где это работает
- Везде, где используется шаблонизатор [render_template_simple()](agentui/pipeline/templating.py:191):
  - Return.text_template
  - ProviderCall.template / headers / endpoint
  - RawForward.extra_headers / override_path
  - SetVars (mode=string)
- Реализовано в препроцессоре шаблонов: [templating.py](agentui/pipeline/templating.py)

Синтаксис подробно
1) Базовый случай (по умолчанию image/png):
- img()[[OUT1]]
  → data:image/png;base64,[[OUT1]] (после разворачивания [[OUT1]] — получится полноценный data URL)

2) Явный тип по короткому имени:
- img(png)[[...]]        → image/png
- img(jpeg)[[...]]       → image/jpeg (alias: jpg → image/jpeg)
- img(webp)[[...]]       → image/webp
- img(gif)[[...]]        → image/gif
- img(svg)[[...]]        → image/svg+xml
- img(bmp)[[...]]        → image/bmp
- img(tif)[[...]] / img(tiff)[[...]] → image/tiff

3) Полный MIME:
- img(image/heic)[[...]] → data:image/heic;base64,...

4) Динамический MIME через фигурные скобки:
- img({{ OUT.n1.result.candidates.0.content.parts.1.inlineData.mimeType|default('image/png') }})[[OUT1]]
  - Важно: MIME в круглых скобках можно задавать фигурными {{ ... }} — после сборки строки препроцессор оставит «data:{{ ... }};base64,...», и следующий проход шаблонизатора подставит реальное значение.

Пошаговые примеры (копируйте и вставляйте)

A. Вернуть картинку как строку data URL из Return без SetVars и expr
- У вас пресет JSONPath на base64: candidates.0.content.parts.1.inlineData.data.
- В Return.text_template напишите:
  img(png)[[OUT1]]
  или, если [[OUT1]] — не base64, а вытащить надо конкретное поле:
  img(png)[[VAR:OUT.n1.result.candidates.0.content.parts.1.inlineData.data]]
- В результате Return отдаст текст:
  data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
- Это удобно, когда потребитель умеет работать с data URL строками.

B. Сформировать HTML &lt;img&gt; в Return прямо в шаблоне
- В Return.text_template:
  &lt;img src="img(jpeg)[[VAR:OUT.n1.result.candidates.0.content.parts.1.inlineData.data]]" alt="preview"&gt;
- На выходе получится полноценный HTML с src="data:image/jpeg;base64,...".

C. Положить data URL в переменную без expr
- SetVars → переменная IMG (mode=string):
  value: img()[[VAR:OUT.n1.result.candidates.0.content.parts.1.inlineData.data]]
- Далее [[IMG]] — готовый data URL (можно вставлять куда угодно).

D. Динамический MIME из ответа провайдера
- Если в JSON есть mimeType:
  img({{ OUT.n1.result.candidates.0.content.parts.1.inlineData.mimeType|default('image/png') }})[[VAR:OUT.n1.result.candidates.0.content.parts.1.inlineData.data]]

Часто задаваемые вопросы

Q1: Чем отличается от «старого» способа через SetVars + data_url()? 
- Старый (через expr) остаётся и удобен, когда нужно переиспользовать значение в нескольких местах или делать дополнительную логику.
- Новый — мгновенная подстановка «в одну строку» прямо в шаблоне, без expr и функций. Он быстрее для типовой задачи «base64 → data URL».

Q2: Что будет, если внутри [[...]] вернётся не строка?
- Шаблонизатор приведёт значение к строке (для dict/list — сериализует в JSON). Но для data URL ожидается именно base64‑строка. Следите, чтобы путь/макрос давал строку, не объект.

Q3: Можно ли внутри скобок img(...) использовать [[...]]?
- Нет, внутри круглых скобок лучше использовать {{ ... }} (фигурные). Пример выше с mimeType показывает правильный путь. Квадратные [[...]] в круглых скобках не разбираются этим прелюдом, зато {{ ... }} спокойно подставятся на следующем шаге.

Q4: Нужно ли дописывать data:...;base64, вручную?
- Нет. Именно для этого и сделан синтаксис img(mime)[[...]]. Вы указываете только MIME (или оставляете пустым), а шаблонизатор добавляет префикс сам.

Q5: Что если мне нужен не текст data URL, а отрисованное превью?
- Блок «ЛОГИ → Data» уже показывает мини‑превью изображений для HTTP‑ответов нод ProviderCall и RawForward, независимо от OUT1.
- Если вы хотите «превью прямо в Return», используйте HTML: &lt;img src="img(png)[[...]]"&gt; — потребитель, способный отображать HTML, увидит картинку.

Диагностика и отладка
- Если вы видите «сырую» base64‑строку вместо data URL, проверьте, что используете новый синтаксис img(...)[[...]] или явно дописали префикс вручную.
- Если «картинки» не видно в Return, удостоверьтесь, что получатель умеет отображать data URL напрямую. Для HTML‑получателей используйте тег &lt;img&gt;.
- Для ответов Gemini в логах панель «Data» показывает превью (мы подаём полные изображения через SSE), а в «Response» структура JSON остаётся читабельной — там триммится только base64, не нарушая остальной структуры.

Где в коде реализовано
- Препроцессор img(mime)[[...]] добавлен в [render_template_simple()](agentui/pipeline/templating.py:191); регулярное выражение и обработчик находятся в [templating.py](agentui/pipeline/templating.py).
- Общая логика развёртки [[...]] и {{ ... }} — тоже в [render_template_simple()](agentui/pipeline/templating.py:191).
- Подсветка и превью в логах — см. [static/editor.html](static/editor.html).

Короткая памятка (копируйте в нужные места шаблонов)
- По умолчанию PNG:
  img()[[OUT1]]
- Явный JPEG:
  img(jpeg)[[VAR:OUT.n1.result.candidates.0.content.parts.1.inlineData.data]]
- Динамический MIME:
  img({{ OUT.n1.result.candidates.0.content.parts.1.inlineData.mimeType|default('image/png') }})[[OUT1]]
- HTML превью:
  &lt;img src="img(png)[[OUT1]]" alt="image"&gt;

— — —
Приложение B. Что изменилось в парсинге Gemini и [[OUTx]] (2025‑09‑21)

Коротко
- Что поменяли: теперь текст из ответов Gemini собирается из ВСЕХ частей parts[].text во всех candidates, пустые/пробельные строки игнорируются, результат склеивается через "\n".
- Где реализовано:
  - Алгоритм best‑effort для [[OUTx]]/алиасов: [\_best_text_from_outputs()](agentui/pipeline/templating.py:133)
  - Явная стратегия "gemini" для нод: [\_extract_text_for_out()](agentui/pipeline/executor.py:1590)
- Что это даёт: [[OUT1]]/[[OUT2]]… и {{ OUT.nX.response_text }} больше не «пустеют», когда первая часть ответа — "\n". Возвращается весь человекочитаемый текст.

Что было и что стало
- Было: брали только первый элемент parts[0].text → если там "\n", получали пустоту.
- Стало: проходим все parts, берём непустые .text (strip) и склеиваем. Если частей нет — работает прежний «глубокий поиск текста» (fallback).

Примеры
- Ответ Gemini (упрощённо):
  {
    "candidates": [{
      "content": { "parts": [{ "text": "\n" }, { "text": "Полезный ответ" }] }
    }]}
  - Раньше [[OUT…]] → "" (или "\n")
  - Теперь [[OUT…]] → "Полезный ответ"

Альтернатива через пресет/JSONPath (без правок кода)
- Если хотите явно контролировать извлечение текста:
  - strategy="jsonpath"
  - json_path="candidates.*.content.parts.*.text"
  - join_sep="\n"
- Это эквивалент новой авто‑логики для Gemini и годится, когда нужна строгая предсказуемость.

Подсказка: RawForward и auto
- Для RawForward мы иногда используем «подсказку» провайдера (auto). Сейчас этого достаточно. При желании можно расширить авто‑детекцию для ответов Gemini по ключу "candidates" в [detect_vendor()](agentui/common/vendors.py:8), но это не обязательно для текущей логики [[OUTx]].

— — —
Приложение C. Как правильно писать условия If (шпаргалка)

Где работает
- Узел If выполняет булево выражение и открывает ветку true/false. Исполнение: [IfNode.run()](agentui/pipeline/executor.py:2892)
- Парсер/оценка выражений: [eval_condition_expr()](agentui/pipeline/templating.py:382)

Что поддерживается
- Операторы:
  - Логика: &&, ||, ! (вместо "not" используйте "!")
  - Сравнения: ==, !=, <, <=, >, >=
  - Ключевое слово: contains (подстрока для строк или membership для списков)
- Скобки: (...)
- Макросы внутри выражения:
  - [[OUT1]], [[OUT:n2.result...]], [[NAME]]
  - {{ OUT.nX.response_text|default('') }}, {{ params.temperature|default(0.7) }}
- Строковые литералы: "..." или '...' (следите за закрывающей кавычкой)

Семантика contains простым языком
- Строки: A contains B → строка B входит в строку A (по подстроке).
- Списки/множества: A contains B → элемент B присутствует в коллекции A.

Частые шаблоны (скопируйте)
- Проверка фразы в тексте провайдера:
  [[OUT3]] contains "Stream failed to"
- Объединение условий:
  ([[OUT3]] contains "Stream failed to") || ([[OUT3]] contains "gemini-2.5-pro")
- Числовые сравнения с запасным значением:
  {{ params.temperature|default(0.7) }} >= 0.3 && {{ params.temperature|default(0.7) }} <= 1
- Проверка «не пусто»:
  {{ OUT.n2.response_text|default('') }} != ""
- Негативная проверка:
  !([[OUT3]] contains "error")
- Сложная проверка с несколькими ветками:
  ({{ params.max_tokens|default(256) }} >= 128) && !([[OUT1]] contains "retry")

Советы по устойчивости
- Используйте |default(...) у {{ ... }}, чтобы не падать на None/пустых значениях.
- Для длинных/непредсказуемых строк проверяйте наличие ключевого фрагмента через contains.
- Оборачивайте группы условий в (...) — так легче читать и поддерживать.
- Внимание к кавычкам: "..." и '...' должны закрываться; это самая частая причина синтаксических ошибок.

Антишаблоны (чего избегать)
- Незакрытые кавычки в строковых литералах.
- Путаница с not: используйте "!" (а не слово not).
- Попытка вызывать произвольные функции — в If разрешён только contains(a, b) (под капотом), всё остальное запрещено.

Проверка на практике
- Пример из рабочего пайплайна (идея):
  If.expr: (([[OUT3]] contains "Stream failed to") || ([[OUT3]] contains "gemini-2.5-pro"))
  True‑ветка → Return "[[OUT3]]"
  False‑ветка → RawForward или другой ProviderCall

Где ещё посмотреть
- Развёртка макросов [[...]] и {{ ... }}: [render_template_simple()](agentui/pipeline/templating.py:196)
- Логи If с развернутым выражением (для отладки): [IfNode.run()](agentui/pipeline/executor.py:2892)
