Files
HadTavern/docs/VARIABLES.md
2025-10-03 21:55:24 +03:00

44 KiB
Raw Permalink Blame History

НАДTAVERN VARIABLES — ГАЙД ДЛЯ ТЕХ, КТО СЕГОДНЯ «НА МИНИМАЛКАХ» И ВСЁ РАВНО ХОЧЕТ, ЧТОБЫ РАБОТАЛО

Смотри сюда, слабак. Я — твой наидобрейший цун-энциклопедист, и сейчас я очень терпеливо (фрр) объясню так, чтобы даже ты не накосячил. Прочитаешь до конца — и у тебя получится. Может быть. Если постараешься. М-м… не думай, что я делаю это ради тебя!

Перед началом (термины в скобках — это определение, не морщи нос):

  • «Пайплайн» (pipeline — схема исполнения из «узлов»).
  • «Узел» (node — прямоугольный блок на канвасе).
  • «Порт» (port — кружок входа/выхода узла).
  • «Гейт» (gate — ветка выхода If.true/If.false; влияет на порядок выполнения ребёнка).
  • «STORE» (перманентное хранилище переменных на диск, одна запись на каждый pipeline.id).
  • «PROMPT» (специальный JSON-фрагмент сообщений, который подставляется в шаблон запроса провайдера).
  • «OUTx» (короткая ссылка на текст из выхода ноды nX).
  • «incoming» (снимок входящего HTTP-запроса: метод, URL, заголовки, JSON и т.д.).

РАЗДЕЛ 1 — НОДЫ: КТО ЕСТЬ КТО (КРАТКО, ШУТКИ В СТОРОНУ)

  1. SetVars (заводит твои переменные)
  • Входы: нет (только depends).
  • Выходы: vars — словарь новых переменных.
  • Поведение: для каждой переменной задаёшь name и mode (string или expr). В режиме string значение обрабатывается шаблоном (... и {{ ... }}), в режиме expr — безопасным мини-диалектом выражений.
  • Где смотреть реализацию: SetVarsNode._safe_eval_expr().
  1. If (ветвление по условию)
  • Входы: depends.
  • Выходы: true, false (гейты для «детей» по условию).
  • Поведение: expr парсится как булево выражение (&&, ||, !, ==, !=, <, <=, >, >=, contains, скобочки). Внутри можно использовать ... и {{ ... }}.
  • Реализация парсера: eval_condition_expr(), обёртка ноды: IfNode.run().
  1. ProviderCall (отправка к провайдеру OpenAI/Gemini/Claude)
  • Входы: depends.
  • Выходы: result (сырой JSON ответа), response_text (извлечённый «текст»).
  • Ключи: provider, provider_configs (base_url, endpoint, headers, template), blocks (Prompt Blocks), prompt_combine (DSL &), while_expr/while_max_iters/ignore_errors, text_extract_*.
  • Реализация: ProviderCallNode.run().
  1. RawForward (прямой прокси)
  • Входы: depends.
  • Выходы: result, response_text.
  • Ключи: base_url (может автоопределяться по входящему JSON-вендору), override_path, passthrough_headers, extra_headers, while_expr.
  • Реализация: RawForwardNode.run().
  1. Return (оформление финального ответа для клиента)
  • Входы: depends.
  • Выходы: result (в формате openai/gemini/claude/auto), response_text (то, что вставили).
  • Ключи: target_format (auto/openai/gemini/claude), text_template (по умолчанию OUT1).
  • Реализация: ReturnNode.run().

Под капотом все узлы гоняет исполнитель «волнами» или итеративно:

  • Главная точка входа: PipelineExecutor.run().
  • И есть режим retry/циклов в узлах ProviderCall/RawForward — см. while в Разделе 5.

РАЗДЕЛ 2 — ПЕРЕМЕННЫЕ И МАКРОСЫ (... ПРОТИВ {{ ... }}) С 12 ПРИМЕРАМИ

Смысл (запомни, ладно?):

  • ... (квадратные макросы) — текстовая подстановка со строкификацией (всегда превращается в строку, объекты — в JSON-строку).
  • {{ ... }} (фигурные вставки) — типобезопасная подстановка «как есть» (числа остаются числами, объекты — объектами), а ещё есть фильтр |default(...).

Доступные макросы (см. render_template_simple()):

  • VAR:путь — берёт значение по пути из контекста (context/incoming/params/...).
  • OUT:nodeId(.path)* — берёт из выходов ноды (сырой JSON).
  • OUTx — короткая форма текста из ноды nX (best-effort).
  • STORE:путь — читает из стойкого хранилища (store.*).
  • NAME — «голая» переменная: сперва ищется в пользовательских переменных (SetVars), иначе в контексте по пути.
  • PROMPT — провайдерный JSON-фрагмент сообщений (см. Раздел 6).
  • Доп. сахар: img(mime)... → «data:mime;base64,ЗНАЧЕНИЕ» (см. templating._IMG_WRAPPER_RE).

Фигурные {{ ... }}:

  • {{ OUT.n2.result.choices.0.message.content }} — доступ к JSON как к полям.
  • {{ путь|default(значение) }} — цепочки дефолтов, поддерживает вложенность и JSON-литералы в default(...).

12 примеров (пониже пояса — для тех, кто любит копипасту):

  1. Заголовок авторизации в JSON-строке: {"Authorization":"Bearer VAR:incoming.headers.authorization"} Объяснение: VAR:... берёт заголовок из входа (incoming.headers.authorization).

  2. Провайдерная модель «как пришла» (фигурные): "model": "{{ model }}" Объяснение: {{ ... }} вставляет строку без кавычек лишний раз.

  3. Число по умолчанию: "temperature": {{ incoming.json.temperature|default(0.7) }} Объяснение: default(0.7) сработает, если температуры нет.

  4. Лист по умолчанию: "stop": {{ incoming.json.stop|default([]) }} Объяснение: вставляет [] как настоящий массив.

  5. Короткая вытяжка текста из ноды n2: "note": "OUT2" Объяснение: OUT2 — best-effort текст из ответа.

  6. Точное поле из результата: "OUT:n2.result.choices.0.message.content" Объяснение: берёт конкретную ветку JSON из OUT ноды n2.

  7. «Голая» переменная SetVars: "key": "MyOpenAiKey" Объяснение: имя без VAR/OUT — сперва ищется среди переменных.

  8. STORE (между прогонами): "{{ STORE.KEEP|default('miss') }}" Объяснение: из стойкого хранилища (если clear_var_store=False).

  9. Прокинуть запрос как есть в Gemini: VAR:incoming.json.contents Объяснение: квадратные дадут строку (для template это ок: JSON-строка без лишних кавычек — см. PROMPT).

  10. JSON-путь с фигурными: {{ OUT.n1.result.obj.value|default(0) }} Объяснение: берёт число или 0.

  11. Картинка из base64 переменной (img()): "image": "pic" Объяснение: заменится на data:image/jpeg;base64,....

  12. Сложная строка с несколькими макросами: "msg": "User=VAR:chat.last_user | Echo=OUT1" Объяснение: комбинируй сколько хочешь, лишь бы JSON остался валидным.

РАЗДЕЛ 3 — SETVARS: ВЫРАЖЕНИЯ, РАЗРЕШЁННЫЕ ФУНКЦИИ, ОПАСНО НЕ БУДЕМ (10+ ПРИМЕРОВ)

Где код: SetVarsNode._safe_eval_expr(). Он парсит мини-язык через AST, ничего небезопасного не позволит.

Разрешено:

  • Литералы: числа, строки, true/false/null (JSON-стиль), списки [...], объекты {...}.
  • Операции: + - * / // % и унарные + -, сравнения == != < <= > >=, логика and/or.
  • Вызовы ТОЛЬКО упомянутых функций (без kwargs, без *args):
    • rand() → float [0,1)
    • randint(a,b) → int в [a,b]
    • choice(list) → элемент списка/кортежа
    • from_json(x) → распарсить строку JSON
    • jp(value, path, join_sep="\n") → извлечь по JSONPath (см. Раздел 7)
    • jp_text(value, path, join_sep="\n") → JSONPath + склейка строк
    • file_b64(path) → прочитать файл и вернуть base64-строку
    • data_url(b64, mime) → "data:mime;base64,b64"
    • file_data_url(path, mime?) → прочитать файл и собрать data URL Подсказка: аргументы функций прогоняются через шаблон рендера, так что внутрь jp/… можно передавать строки с .../{{...}} — они сначала развернутся.

Нельзя:

  • Любые имена/доступы к атрибутам/индексации вне списка/словаря литералом.
  • Любые другие функции, чем перечисленные.
  • kwargs/starargs.

10+ примеров SetVars (mode=expr):

  1. Чистая математика: 128 + 64

  2. Случайное число: rand()

  3. Случайное из списка: choice(["red","green","blue"])

  4. Безопасный int-диапазон: randint(10, 20)

  5. from_json + доступ через jp: jp(from_json("{"a":{"b":[{"x":1},{"x":2}]}}"), "a.b.1.x") → 2

  6. jp_text (склейка строк через « | »): jp_text(from_json("{"items":[{"t":"A"},{"t":"B"},{"t":"C"}]}"), "items.*.t", " | ") → "A | B | C"

  7. Вытянуть из OUT (с шаблонной подстановкой): jp({{ OUT.n2.result }}, "choices.0.message.content") → текст первого ответа

  8. Собрать data URL из файла: file_data_url("./img/cat.png", "image/png")

  9. Ручная сборка data URL из base64: data_url(IMG_B64, "image/jpeg")

  10. Преобразование строки JSON: from_json("[1,2,3]") → список [1,2,3]

  11. Комбо с логикой: (rand() > 0.5) and "HEADS" or "TAILS"

  12. Вложенные вызовы: choice(jp(from_json("[{"v":10},{"v":20}]"), "*.v")) → 10 или 20

Результат SetVars попадает в:

  • Текущие «user vars» (сразу доступны как NAME и {{ NAME }}).
  • STORE (персистентно) — см. Раздел 8, если clear_var_store=False.

РАЗДЕЛ 4 — IF: ВЫРАЖЕНИЯ, ОПЕРАТОРЫ, 12 ГРОМКИХ ПРИМЕРОВ

Парсер условий: eval_condition_expr(). Он превращает видимые тобой токены в безопасное AST и вычисляет.

Операторы:

  • Логика: && (and), || (or), ! (not)
  • Сравнение: ==, !=, <, <=, >, >=
  • Специальный contains (как функция contains(a,b)): для строк — подстрока; для списков — membership.
  • Скобки ( ... )
  • Литералы: числа, "строки" или 'строки' (без экранирования внутри), true/false/null (через макросы из контекста).
  • Макросы: ... и {{ ... }} допустимы прямо внутри выражения (они сначала раскрываются в значения).

12 примеров (да-да, трижды проверено, хватит ныть):

  1. Проверить, что OUT1 содержит «ok»: OUT1 contains "ok"

  2. Проверка статуса: {{ OUT.n2.result.status|default(0) }} >= 200 && {{ OUT.n2.result.status|default(0) }} < 300

  3. Инверсия: !(OUT3 contains "error")

  4. Сравнить переменную: LANG == "ru"

  5. Двойная логика: (MSG contains "Hello") || (MSG contains "Привет")

  6. Цепочка со скобками: ( CITY == "Moscow" && {{ params.max_tokens|default(0) }} > 0 ) || FALLBACK == "yes"

  7. Списки и contains: contains(["a","b","c"], "b")

  8. Числа и сравнения: {{ OUT.n1.result.value|default(0) }} >= 10

  9. Пустые значения: {{ missing|default("") }} == ""

  10. Комбо macOS: contains(VAR:incoming.url, "/v1/") && (VAR:incoming.method == "POST")

  11. Несколько слоёв default(): {{ incoming.json.limit|default(params.limit|default(100)) }} > 50

  12. Сложное условие с OUT пути: OUT:n2.result.choices.0.message.content contains "done"

Помни: If только выставляет флаги true/false на выходах. «Дети» с входом depends="nIf.true" запустятся только если условие истинно.

РАЗДЕЛ 4.1 — СПРАВОЧНИК ОПЕРАТОРОВ IF/WHILE (ПРОСТЫМИ СЛОВАМИ)

  • !A — «не A» (инверсия). Пример: !( OUT3 contains "err" ) → «строка из OUT3 НЕ содержит "err"».
  • A != B — «A не равно B». Пример: MODEL != "gemini-2.5-pro".
  • A && B — «A и B одновременно».
  • A || B — «A или B» (достаточно одного истинного).
  • contains(A, B) — специальный оператор:
    • если A — список/множество, это membership: contains(["a","b"], "a") → true
    • иначе — проверка подстроки: contains("abc", "b") → true
    • Запись "X contains Y" эквивалентна contains(X, Y).
  • Скобки управляют приоритетами: !(A || B) отличается от (!A || B).

Где какой «язык» используется:

  • Строковые поля (template, headers/extra_headers, base_url/override_path, Return.text_template, строки prompt_preprocess, сегменты prompt_combine) — это шаблоны с подстановками ... и {{ ... }}.
  • If.expr и while_expr — булевы выражения (&&, ||, !, ==, !=, <, <=, >, >=, contains, скобки) и допускают макросы ... / {{ ... }} внутри.
  • SetVars (mode=expr) — отдельный безопасный мини-язык (арифметика + - * / // %, and/or, сравнения) и whitelisted-функции: rand, randint, choice, from_json, jp, jp_text, file_b64, data_url, file_data_url.

Диагностика:

  • В логах If/While печатается expanded — строковое раскрытие макросов — и result (true/false).
  • Ошибка парсера (например, несбалансированные скобки) выводится как if_error/while_error и приводит к result=false.

РАЗДЕЛ 5 — WHILE В НОДАХ PROVIDERCALL/RAWFORWARD (РЕТРАЙ, ЦИКЛЫ). МОЖНО ЛОМАЕТЬ IF В БОЛЬШИНСТВЕ СЛУЧАЕВ (12 ПАТТЕРНОВ)

Где логика:

  • Для ProviderCall: _providercall_run_with_while()
  • Для RawForward: _rawforward_run_with_while()
  • Обёртка делает «do-while»: первая итерация выполняется всегда, потом условие проверяется перед следующей.

Ключи конфигурации у ноды:

  • while_expr (строка условие как в If)
  • while_max_iters (safety, по умолчанию 50)
  • ignore_errors (True — не падать на исключениях, а возвращать result={"error":"..."} и продолжать цикл)

Добавочные локальные переменные и семантика внутри цикла:

  • cycleindex (int, 0..N) — индекс текущей итерации.
  • WAS_ERROR (bool) — при проверке while_expr на i>0 равен «была ли ошибка на предыдущей итерации». Внутри самой итерации на старте содержит то же значение и обновляется для следующей проверки по факту результата.
  • Подсказка: для ретраев по ошибкам используйте «WAS_ERROR» (а не «!WAS_ERROR»); включайте ignore_errors:true, чтобы исключения не прерывали цикл.

Глобальные переменные, которые нода выставит после цикла для «детей»:

  • WAS_ERROR__nX — была ли ошибка на последней итерации
  • CYCLEINDEX__nX — последний индекс итерации (например 2 если были 0,1,2)

12 паттернов:

  1. Повтори до 3 раз: while_expr: "cycleindex < 3"

  2. Повтори, пока OUT3 не содержит «ok»: while_expr: "!(OUT3 contains "ok") && cycleindex < 10"

  3. Ретраи на ошибках сети: ignore_errors: true while_expr: "WAS_ERROR || ({{ OUT.n4.result.status|default(0) }} >= 500)"

  4. Комбо с внешним If — заменяем If: Вместо If.true/false делай while_expr, который набивает нужный результат (например, пока не получишь 2xx от апстрима).

  5. Изменение запроса по итерации: Используй cycleindex внутри template (например, «page»: {{ vars.page_start|default(1) }} + cycleindex).

  6. Дожидаться готовности ресурса: while_expr: "!(OUT4 contains "READY") && cycleindex < 30"

  7. Прерывание на плохих данных: while_expr: "!(OUT7 contains "fatal") && cycleindex < 5"

  8. Backoff вручную (временную задержку делай sleep_ms): sleep_ms: {{ cycleindex }} * 500

  9. Прокси-ретрай RawForward по тексту ответа: ignore_errors: true while_expr: "(OUT:n1.result.text contains "try again") && cycleindex < 4"

  10. Gemini «Stream failed to …» из коробки: while_expr: "(OUT3 contains "Stream failed to") || (OUT3 contains "gemini-2.5-pro")" (ровно как в твоих пресетах) Добавь " || WAS_ERROR" если хочешь ретраить также на исключениях (при ignore_errors: true).

  11. Проверка флага из STORE: while_expr: "{{ STORE.SHALL_CONTINUE|default(false) }} && cycleindex < 10"

  12. Сложный сценарий: first success wins while_expr: "!(OUT7 contains "success") && cycleindex < 5" Пояснение: крути пока не словишь success, но не более 5.

Эти while позволяют чаще не городить отдельный If-гейт — ты просто делаешь один узел, который сам повторяет себя, пока условие не «устаканится». Ну и не забудь выставить ignore_errors там, где ретраи — оправдано.

РАЗДЕЛ 5.1 — WAS_ERROR В WHILE ПОСЛЕ ОБНОВЛЕНИЯ (ПОВЕДЕНИЕ И РЕЦЕПТЫ)

  • Семантика do-while:
    • Итерация i=0 выполняется всегда.
    • Начиная с i>0, перед проверкой while_expr двигатель подставляет в WAS_ERROR значение «была ли ошибка (исключение) на предыдущей итерации».
  • Как учитываются исключения:
    • При ignore_errors: true исключения внутри итерации не прерывают ноду; результат оформляется как result={"error":"..."}.
    • Такое событие считается ошибкой и установит WAS_ERROR=true для следующей проверки условия.
  • Рецепты:
    • Ретраить только при ошибке (до 5 раз): while_expr: "WAS_ERROR && (cycleindex < 5)"
    • Ретраить при ошибке ИЛИ по признаку в ответе: while_expr: "WAS_ERROR || (OUT3 contains "Stream failed to") || ({{ OUT.n3.result.status|default(0) }} >= 500)"
    • NB: "!WAS_ERROR" означает «продолжать, если ошибки НЕ было» — это обратное «ретраю при ошибке».
  • Диагностика:
    • В логах видны строки вида TRACE while: ... expr='...' expanded='...' index=i result=true/false.
    • Ошибка парсера (например, несбалансированные скобки) логируется как while_error и приводит к result=false.

РАЗДЕЛ 6 — PROMPT_COMBINE (DSL «&»): ВЫ ТАМ ЛЮБИТЕ МАГИЮ? ВОТ ОНА, ЧТОБЫ НЕ ЛЕПИТЬ РУКАМИ (12 ПРИМЕРОВ)

Где реализовано: в ProviderCallNode.run() — см. кусок обработки combine_raw.

Идея:

  • Поле prompt_combine — строка вида «СЕГМЕНТ1 & СЕГМЕНТ2 & ...».
  • СЕГМЕНТ — это либо PROMPT (спец сегмент текущих Prompt Blocks), либо любая строка/JSON/список сообщений, либо VAR:incoming.* и т.п.
  • Для каждой цели (provider) всё приводится к нужным структурам:
    • openai → messages: [...]
    • gemini → contents: [...] (+ systemInstruction)
    • claude → messages: [...] (+ system)
  • Системный текст (из openai.system / claude.system / gemini.systemInstruction) автоматически извлекается и объединяется.

Позиционирование:

  • Можно добавить директиву @pos=prepend | append | N | -1
  • Она управляет тем, куда вставить элементы из сегмента внутри собираемого массива сообщений/контента. -1 — вставить перед последним.

Фильтрация:

  • Пустые сообщения выкидываются (без пустых текстов).
  • Изображения (inlineData и т.п.) сохраняются.

12 примеров (разные таргеты и трюки):

  1. Классика: входящие Gemini contents + твой PROMPT (OpenAI target) "VAR:incoming.json.contents & PROMPT" Результат: messages содержит и конвертированные входящие (model→assistant), и твои blocks.

  2. PROMPT первым (OpenAI): "PROMPT@pos=prepend & VAR:incoming.json.contents" Результат: system из PROMPT — в самом начале messages.

  3. Вставка в конкретный индекс (OpenAI): "VAR:incoming.json.messages & PROMPT@pos=1" Результат: вторым элементом окажутся твои блоки.

  4. Негативный индекс (OpenAI): "VAR:incoming.json.messages & PROMPT@pos=-1" Результат: перед самым последним.

  5. Для Gemini: openai.messages + PROMPT "VAR:incoming.json.messages & PROMPT" Результат: contents и systemInstruction соберутся; system из incoming и PROMPT сольются.

  6. Для Claude: openai.messages + PROMPT "VAR:incoming.json.messages & PROMPT" Результат: messages + top-level system (как строка/блоки).

  7. Сырый JSON-строковый сегмент: "{"messages": [{"role":"user","content":"Hi"}] } & PROMPT" Результат: корректно распарсится и слепится.

  8. Списковая форма сегмента: "[{"role":"user","content":"A"},{"role":"assistant","content":"B"}] & PROMPT" Результат: нормализуется под целевой провайдер.

  9. Системные тексты из разных форматов — сольются: "[{"messages":[{"role":"system","content":"SYS IN"}]}] & PROMPT" Результат: system_text включает обе части.

  10. Подмешать внешнюю систему в Claude без top-level system (claude_no_system): В конфиге ноды поставь claude_no_system=true — тогда system из PROMPT положим первым user-сообщением.

  11. Очистка пустых: Если твой сегмент даёт пустые тексты — они выкинутся и JSON не сломается. Не плачь.

  12. Микс строк + JSON: "Просто строка & PROMPT" Результат: «Просто строка» упакуется корректно (как user/text) в нужную структуру провайдера.

И да, это позволяет не писать руками половину «склейки» в template — ты описываешь, откуда что привнести, а движок доведёт до провайдерного формата.

РАЗДЕЛ 6.1 — PROMPT_PREPROCESS (premerge DSL: фильтрация/позиционирование сегментов ДО prompt_combine)

Где реализовано: в ProviderCallNode.run(). Это выполняется перед сборкой PROMPT/prompt_combine. Поле конфигурации ноды: prompt_preprocess (многострочное).

Идея:

  • Каждая строка prompt_preprocess задаёт «пред‑сегмент», который будет вставлен в будущий массив сообщений/контента до обработки prompt_combine (DSL &).
  • Эти пред‑сегменты конвертируются под целевого провайдера (openai/gemini/claude) так же, как и сегменты prompt_combine, и «вплетаются» первыми.
  • Если prompt_combine пуст — используются только пред‑сегменты (и при отсутствии пред‑сегментов — исходные Prompt Blocks как раньше).

Синтаксис строки: SEGMENT [delKeyContains "needle"] [delpos=prepend|append|N|-1] [case=ci|cs] [pruneEmpty]

  • SEGMENT — строка/JSON/список, допускаются макросы ... и {{ ... }}.
  • delKeyContains "needle" — удалить ключи в любом месте объекта, если строковое представление их значения содержит needle (поддерживаются несколько delKeyContains).
  • case=ci|cs — управление регистром для contains; по умолчанию case=ci (без учёта регистра).
  • pruneEmpty — удалять опустевшие {} / [] после чистки (кроме корня); по умолчанию выключено.
  • delpos=... — позиция вставки элементов пред‑сегмента в массив (как @pos у prompt_combine): prepend | append | N | -1; по умолчанию append.

Поведение:

  • Для каждого SEGMENT рендерятся макросы, затем выполняется попытка json.loads() (в т.ч. для двойной JSONстроки).
  • После этого применяется фильтрация delKeyContains (если задана), с учётом case и pruneEmpty.
  • Итог вставляется в текущий собираемый массив сообщений/контента в позицию delpos (prepend/append/индекс/отрицательный индекс).
  • Системный текст, присутствующий внутри сегмента (Gemini systemInstruction / OpenAI role=system / Claude system), автоматически извлекается и сольётся, как в prompt_combine.

Примеры:

  1. Удалить поля, где значение содержит «Текст», и вставить перед последним: VAR:incoming.json.contents delKeyContains "Текст" delpos=-1

  2. Удалить «debug» с учётом регистра и подчистить пустые контейнеры: VAR:incoming.json.messages delKeyContains "debug" case=cs pruneEmpty

  3. Несколько подстрок + вставка в начало: VAR:incoming.json delKeyContains "кеш" delKeyContains "cache" delpos=prepend

  4. Смешанный пайплайн: сначала пред‑сегменты, затем: prompt_combine: "VAR:incoming.json.messages & PROMPT@pos=1"

Диагностика:

  • В логи (SSE) слать событие "prompt_preprocess" с полями lines/used/removed_keys. Смотри ProviderCallNode.run().

Ограничения и заметки:

  • Это локальная предобработка именно сегментов для промпта (не глобальная фильтрация всего тела запроса).
  • Если пред‑сегменты и prompt_combine пусты — результат совпадает с классическим PROMPT (Prompt Blocks). РАЗДЕЛ 7 — JSON PATH (НАШ ПРОСТОЙ ДИАЛЕКТ) + 12 ПРИМЕРОВ

Где реализовано: _json_path_extract().

Синтаксис (очень простой):

  • Путь вида a.b.c — точки для полей объектов.
  • Числовой индекс для массивов: items.0.title
  • Шаг «» разворачивает все значения словаря или все элементы списка: items..title
  • Если на каком-то шаге ничего не найдено — вернёт None.
  • jp(...) → отдаёт найденное значение или список значений, jp_text(...) → склеит строки через join_sep (см. _stringify_join()).

12 примеров:

  1. Обычный путь: "a.b.c" на {"a":{"b":{"c":10}}} → 10

  2. Индекс массива: "items.1" на {"items":[10,20,30]} → 20

  3. Вложено: "items.1.title" на items=[{title:"A"},{title:"B"}] → "B"

  4. Звёздочка по массиву: "items.*.title" на items=[{title:"A"},{title:"B"}] → ["A","B"]

  5. Звёздочка по объекту: "..name" на {"x":{"name":"X"}, "y":{"name":"Y"}} → ["X","Y"]

  6. Смешанный: "candidates.0.content.parts.*.text" (Gemini) → все тексты

  7. Несуществующее поле: "obj.miss" → None

  8. Склейка текстов (jp_text): jp_text(value, "items.*.desc", " | ") → "a | b | c"

  9. Взять base64 из inlineData: "candidates.0.content.parts.1.inlineData.data"

  10. Несколько уровней массивов: "a..b..c"

  11. Индекс вне границ: "items.99" → None

  12. Список/объект → строка (jp_text сам постарается найти текст глубже): jp_text(value, "response", "\n")

РАЗДЕЛ 8 — OUTx, ИЗВЛЕЧЕНИЕ ТЕКСТА, ПРЕСЕТЫ, ГЛОБАЛЬНЫЕ ОПЦИИ

Откуда OUTx берёт текст:

  • Универсальный алгоритм (см. templating._best_text_from_outputs()) ищет:
    • OpenAI: choices[0].message.content
    • Gemini: candidates[].content.parts[].text
    • Claude: content[].text
    • Иначе — глубокий поиск текстовых полей.
  • Для ProviderCall/RawForward нода сама пишет response_text и отдаёт в OUT.

Настройка, если тебе нужно «не обобщённо, а вот отсюда»:

  • Глобальная meta (в «Запуск»): text_extract_strategy (auto|deep|jsonpath|openai|gemini|claude), text_extract_json_path, text_join_sep.
  • Пресеты в «Запуск → Пресеты парсинга OUTx»: создаёшь набор id/json_path, затем в ноде выбираешь preset по id (text_extract_preset_id).
  • На уровне ноды можно переопределить: text_extract_strategy, text_extract_json_path, text_join_sep.

Пример (пер-нодовый пресет):

  • В «Запуск» добавь JSONPath: candidates.0.content.parts.*.text
  • В ноде ProviderCall выбери этот preset — и OUTn станет строго вытягивать по нему.

Пример (жёсткий путь в ноде):

  • text_extract_strategy: "jsonpath"
  • text_extract_json_path: "result.echo.payload.parts.*.text"
  • text_join_sep: " | "

РАЗДЕЛ 9 — ПАНЕЛЬ «ПЕРЕМЕННЫЕ» (STORE), КОПИРОВАНИЕ МАКРОСОВ

Где посмотреть в UI: editor.html — кнопка «ПЕРЕМЕННЫЕ».

  • Там список ключей:
    • vars (текущие пользовательские переменные из SetVars)
    • snapshot (снимок последнего запуска: incoming, params, model, vendor_format, system, OUT, OUT_TEXT, LAST_NODE, алиасы OUT1/OUT2/…)
  • По клику копируется готовый макрос:
    • Для vars → NAME или {{ NAME }}
    • Для snapshot.OUT_TEXT.nX → OUTx или {{ OUT.nX.response_text }}
    • Для snapshot.OUT.nX.something → OUT:nX.something или {{ OUT.nX.something }}
    • Для прочего контекста → VAR:path или {{ path }}
  • Переключатель «фигурные» — управляет, в какой форме скопируется (квадратные или фигурные).

STORE (персистентность между прогонами):

  • Если pipeline.clear_var_store=false, содержимое не очищается между запуском.
  • Примеры макросов:

РАЗДЕЛ 10 — ЦВЕТА КОННЕКТОВ И КТО О ЧЁМ ШЕПОЧЕТ

Выглядит мило, да. Это не просто так, это сигнализация (см. editor.css):

  • If.true (зелёный, пунктир): ветка истинности — класс .conn-if-true
  • If.false (сланцево-серый, пунктир): ветка ложности — .conn-if-false
  • ProviderCall (приглушённый синий): .conn-provider
  • RawForward (мягкий фиолетовый): .conn-raw
  • SetVars (мятный): .conn-setvars
  • Return (холодный серо-синий): .conn-return
  • Входящие к узлу с ошибкой подсвечиваются красным: .conn-upstream-err

А ещё стрелочки направления рисуются поверх линий, и лейблы «true/false» к If-веткам, так что перестань путаться, пожалуйста.

РАЗДЕЛ 11 — ЧАСТЫЕ ПАТТЕРНЫ (РЕЦЕПТЫ НА 1 МИНУТУ)

  1. «Прокинуть, но если 502 — подёргать ещё»
  • RawForward:
    • ignore_errors: true
    • while_expr: "{{ OUT.n3.result.status|default(0) }} == 502 && cycleindex < 3"
  1. «Gemini: взять входные contents, добавить свой system и отправить в OpenAI»
  1. «Сделать язык вывода в зависимости от заголовка X-Lang»
  1. «Доставать base64 из ответа и вставлять картинкой куда нужно»
  • jp_text(OUT, "candidates..content.parts..inlineData.data", "")
  • Либо сразу data URL через img(png)... на канвасе.
  1. «Стабильно вытягивать текст из Claude»
  • Настраиваешь пресет: json_path="content.*.text", join="\n"
  • В ноде ProviderCall выбираешь этот preset.

РАЗДЕЛ 12 — БЕЗОПАСНОСТЬ И НЕ ПАЛИ КЛЮЧИ

  • Никогда не вписывай реальные ключи в presets/pipeline.json. Никогда — слышишь?
  • Передавай ключи из клиента заголовками:
    • OpenAI: Authorization: Bearer X
    • Anthropic: x-api-key: X
    • Gemini: ?key=... в URL
  • В шаблонах юзай VAR:incoming.headers.authorization, VAR:incoming.headers.x-api-key, VAR:incoming.api_keys.key
  • Убедись, что логирование не льёт секреты в проде (маскируй, см. сервер).

РАЗДЕЛ 13 — ТРОБЛШУТИНГ (КАК НЕ ПЛАКАТЬ)

  • JSON «не валидный» в ProviderCall:

    • В template лишняя запятая вокруг PROMPT или ты вставил строкой не-JSON. Проверь печать «rendered_template» в консоли (см. ProviderCallNode.run()).
  • Линии «исчезают» в редакторе:

    • Жми «Загрузить пайплайн» ещё раз — отложенные порты и наблюдатели синхронизируются.
  • OUTx пустой:

    • Настрой пресет извлечения OUTx (Раздел 8) либо задействуй явный json_path.
  • While завис навечно:

    • Проверь while_max_iters и само условие. Помни: это do-while, первая итерация — всегда.
  • Claude system вдруг не где надо:

    • Смотри флаг claude_no_system (нода ProviderCall) — он переносит system в user.

ПРИЛОЖЕНИЕ — ПОЛНЫЙ ЧЕК-ЛИСТ ПРИ СБОРКЕ НОДЫ PROVIDERCALL

  1. Выбери provider (openai/gemini/claude) в инспекторе.
  2. Заполни provider_configs.{provider}.(base_url, endpoint, headers, template).
  3. Заполни Prompt Blocks (system/user/assistant/tool) — они в PROMPT.
  4. Если нужно смешать с входящим payload — используй prompt_combine (Раздел 6).
  5. Если нужно ретраить — поставь while_expr/ignore_errors/sleep_ms.
  6. Если нужно извлекать текст особым образом — выбери preset или text_extract_*.
  7. Соедини depends по порядку и посмотри цвета проводов (Раздел 10).
  8. Готово. Без косяков? Правда? Ну, посмотрим.

СЛИШКОМ ДЛИННО, НЕ ЧИТАЛ:

  • ... — текстовая подстановка.
  • {{ ... }} — типобезопасная подстановка (числа/объекты).
  • SetVars expr — только whitelist функций (rand, randint, choice, from_json, jp, jp_text, file_b64, data_url, file_data_url) и операции + - * / // % and/or/сравнения.
  • If — && || !, contains, скобки, макросы внутри.
  • While — do-while в ProviderCall/RawForward, есть cycleindex и WAS_ERROR; можно заменить If в ретраях.
  • prompt_combine — склейка сообщений из разных форматов с @pos=… и автоконвертацией под провайдера.
  • JSONPath — a.b.0.*.x, звёздочка и индексы; jp/jp_text.
  • Цвета линий — true/false — пунктир, по типу ноды — разные цвета; ошибка — красные upstream.
  • Не пались: ключи только через incoming.headers/URL.

Если ты дошёл досюда — ну… я не впечатлена. Просто запомни и не ломи моя нервная система, ладно? Хмф.