Files
HadTavern/docs/VARIABLES.md

46 KiB
Raw Blame History

Переменные и макросы в НадTavern: очень понятный гайд

Этот файл объясняет, как в проекте подставлять нужные кусочки данных прямо в шаблоны, не ломая голову. Представьте «наклейки» — вы клеите их в JSON на нужные места, и система сама подменяет их на значения: из входящего запроса, из предыдущих узлов, из ваших переменных, из памяти выполнения.

Если вы открыли визуальный редактор по адресу http://127.0.0.1:7860/ui/editor.html — все примеры ниже можно копировать прямо туда.

Файлы, где «живет» эта магия:

— — —

  1. Что такое «переменные» и «макросы»
  • Переменные — это значения, которые вы где-то определили: в узле SetVars, они появились из входящего запроса, их вернул провайдер (OpenAI/Gemini/Claude), или их сохранил движок выполнения в «память».
  • Макросы — короткие «заклинания» вида ... или {{ ... }}, которые вы вставляете прямо в текст/JSON. На ходу они заменяются на нужные значения.

Простейший пример:

— — —

  1. Откуда берутся данные (куда «лезут» макросы)
  • incoming.* — всё про входящий HTTPзапрос клиента:

  • params.* — «нормализованные» параметры (температура, топ‑п и т.д.):

  • model, system, vendor_format — общие поля запроса:

  • chat.* — удобный доступ к сообщениям:

  • 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 }}

— — —

  1. Главные «заклинания» (макросы)
  • VAR:путь — взять значение из контекста

  • OUT1, OUT2, … — быстро взять «лучший» текст из узла n1, n2, …

    • Удобно для Return и подсказок к следующему провайдеру
  • OUT:nX.что‑то — взять сырой JSON из узла и провалиться по пути

  • PROMPT — хитрый фрагмент JSON из Prompt Blocks

    • В ноде ProviderCall он разворачивается в правильные поля, в зависимости от провайдера:
      • OpenAI → "messages": [...]
      • Gemini → "contents": [...], "systemInstruction": {...}
      • Claude → "system": "...", "messages": [...]
    • Под капотом это делает: ProviderCallNode.run()
  • {{ путь }} — «фигурные скобки» возвращают значение без кавычек

    • Подходит для чисел/массивов/объектов. Пример: "temperature": {{ params.temperature|default(0.7) }}
    • Есть фильтр по умолчанию: |default(значение) — безопасно подставит запасной вариант
    • Разбирает наш шаблонизатор: render_template_simple()
  • NAME — «голая» переменная из SetVars (короткая запись)

    • Ищется сперва в ваших vars, потом в общем контексте
  • STORE:путь — достать из постоянного хранилища (store.*)

Подсказка: макросы ... можно писать в строках JSON, а {{ ... }} — там, где нужно вставить число/массив/объект без кавычек.

— — —

  1. Куда именно это писать (по узлам)

4.1 ProviderCall — «собрать запрос к провайдеру»

Где живут макросы:

  • endpoint — можно вставлять {{ model }} или ключи в URL (например Gemini ?key=…)
  • headers — это JSONобъект в текстовом поле: сюда можно писать VAR:incoming.headers.authorization, Clod и т.д.
  • template — главное поле: JSONтело запроса. Обязательно валидный JSON после разворачивания макросов.

Мини‑шпаргалка:

Полный пример 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/ContentLength) и тело 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")
    • Достаёт данные по «dotJSONPath» (индексы и звёздочка поддерживаются)
    • Примеры путей: 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 запрещён.

— — —

  1. Где в редакторе быстро подсмотреть макросы

Вверху справа есть кнопка «ПЕРЕМЕННЫЕ». Откроется панель:

  • Выберите «snapshot», чтобы видеть всю «картину мира» последнего запуска (incoming, params, OUT_TEXT, алиасы OUT1/OUT2 и т.д.)
  • Клик по строке копирует готовый макрос в буфер (можно переключить режим «фигурные» — для {{ ... }})
  • Поиск работает и по именам, и по значениям

За поведение панели отвечают страничка и скрипты:

— — —

  1. Что на самом деле делает 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(), RawForwardNode.run()

Для продвинутых: глобальные мета‑настройки пайплайна (timeouts, стратегия, пресеты) редактируются в «Запуск», а сохраняются в pipeline.json.

— — —

  1. Безопасность (прочитать обязательно)
  • Не храните настоящие 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()

— — —

  1. Рецепты «скопируй и вставь»

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"

— — —

  1. Где это в коде (для любопытных)

— — —

Всё. Старайтесь думать так: «Мне нужен кусочек нужного значения в нужное место». Найдите его в панели «ПЕРЕМЕННЫЕ», скопируйте макрос — и вклейте. Если «кусочков» несколько — заведите переменные SetVars и соберите их в строку. Если сомневаетесь — начните с Return и OUT1: это самый короткий путь увидеть, что реально получается. — — —

Приложение A. SetVars: функции и операции в mode=expr (простым языком)

Где это запрограммировано

  • Логика разбора и разрешённые операции реализованы в SetVarsNode._safe_eval_expr(). Если коротко: там «белый список» безопасных функций и операций, всё остальное запрещено.
  • Подстановка макросов внутри строк (для функций from_json/jp/jp_text) делает render_template_simple().

Что можно писать в mode=expr

  1. Случайные значения
  • rand() — число от 0 до 1 (типа 0.01.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. Арифметика (как в обычных формулах)
  • Операции: +, -, *, /, //, %

    • / — деление с плавающей точкой
    • // — целочисленное деление (отбрасывает дробную часть)
    • % — остаток от деления
  • Скобки и унарные знаки:

    • (1 + 2) * 3
    • -5, +7

Примеры:

  • 9.99 * 1.2 → 11.988
  • (rand() * 100) // 1 → псевдо‑процент от 0 до 99
  • randint(1, 6) + 10 → 11..16
  1. Логика и сравнения
  • Логические: and, or
  • Сравнения: ==, !=, <, <=, >, >=
  • Можно «цепочкой»: 1 < x <= 10

Примеры:

  • (rand() > 0.5) and (choice([True, False]) or True)
  • 120 <= choice([64,128,256]) and 256 >= 200 → True
  1. Литералы (что можно писать напрямую)
  • Числа: 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.
  1. Макросы внутри 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

Быстрая шпаргалка «на каждый день»

  1. Бросок кубика d6
  • name: DICE
  • mode: expr
  • value: randint(1, 6)
  1. Случайный стиль ответа
  • name: TONE
  • mode: expr
  • value: choice(['официально','дружелюбно','коротко'])
  1. Выбрать лимит токенов
  • name: TOKENS
  • mode: expr
  • value: choice((128, 256, 512))
  1. Наценка 20%
  • name: PRICE_WITH_VAT
  • mode: expr
  • value: price * 1.2 ← если price — ваша строковая переменная, используйте вне expr (в шаблоне) {{ PRICE }}; внутри expr внешние имена не видны
  1. Вытянуть текст из ответа модели 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.
  1. Работа с изображениями (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,<b64>.
    • 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
    • alt
    • alt
  • Во время исполнения 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 v20230601):
    • 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":"<base64>"}} — для 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

Примечания

  • Никакого тримминга больших строк и 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:<mime>;base64,<ваш_base64>

Где это работает

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

Синтаксис подробно

  1. Базовый случай (по умолчанию image/png):
  • img()OUT1 → data:image/png;base64,OUT1 (после разворачивания OUT1 — получится полноценный data URL)
  1. Явный тип по короткому имени:
  • 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
  1. Полный MIME:
  • img(image/heic)... → data:image/heic;base64,...
  1. Динамический 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 <img> в Return прямо в шаблоне

C. Положить data URL в переменную без expr

D. Динамический MIME из ответа провайдера

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

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: <img src="img(png)..."> — потребитель, способный отображать HTML, увидит картинку.

Диагностика и отладка

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

Где в коде реализовано

Короткая памятка (копируйте в нужные места шаблонов)

— — — Приложение B. Что изменилось в парсинге Gemini и OUTx (20250921)

Коротко

  • Что поменяли: теперь текст из ответов Gemini собирается из ВСЕХ частей parts[].text во всех candidates, пустые/пробельные строки игнорируются, результат склеивается через "\n".
  • Где реализовано:
  • Что это даёт: 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(), но это не обязательно для текущей логики OUTx.

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

Где работает

  • Узел If выполняет булево выражение и открывает ветку true/false. Исполнение: IfNode.run()
  • Парсер/оценка выражений: eval_condition_expr()

Что поддерживается

  • Операторы:
    • Логика: &&, ||, ! (вместо "not" используйте "!")
    • Сравнения: ==, !=, <, <=, >, >=
    • Ключевое слово: contains (подстрока для строк или membership для списков)
  • Скобки: (...)
  • Макросы внутри выражения:
  • Строковые литералы: "..." или '...' (следите за закрывающей кавычкой)

Семантика 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

Где ещё посмотреть