44 KiB
НАДTAVERN VARIABLES — ГАЙД ДЛЯ ТЕХ, КТО СЕГОДНЯ «НА МИНИМАЛКАХ» И ВСЁ РАВНО ХОЧЕТ, ЧТОБЫ РАБОТАЛО
Смотри сюда, слабак. Я — твой наидобрейший цун-энциклопедист, и сейчас я очень терпеливо (фрр) объясню так, чтобы даже ты не накосячил. Прочитаешь до конца — и у тебя получится. Может быть. Если постараешься. М-м… не думай, что я делаю это ради тебя!
- Источники истины (это значит «код, который реально решает», а не чаты):
- Исполнение пайплайна: PipelineExecutor.run()
- Нода SetVars — выражения и функции: SetVarsNode._safe_eval_expr()
- Нода ProviderCall — вызов провайдера и PROMPT: ProviderCallNode.run()
- Нода RawForward — прямой прокси: RawForwardNode.run()
- Нода Return — формат финального ответа: ReturnNode.run()
- Нода If — парсер условий: IfNode.run()
- While-обёртка для ProviderCall: _providercall_run_with_while()
- While-обёртка для RawForward: _rawforward_run_with_while()
- Шаблоны: ... и {{ ... }} здесь: render_template_simple()
- Условия if/while (&&, ||, contains, скобочки): eval_condition_expr()
- JSONPath (упрощённый, но хватит): _json_path_extract()
- Склейка текста при JSONPath: _stringify_join()
- UI инспектор Prompt Blocks: PM.setupProviderCallPMUI()
- Экспорт/импорт пайплайна в редакторе: AgentUISer.toPipelineJSON(), AgentUISer.fromPipelineJSON()
- Веб-редактор: editor.html
Перед началом (термины в скобках — это определение, не морщи нос):
- «Пайплайн» (pipeline — схема исполнения из «узлов»).
- «Узел» (node — прямоугольный блок на канвасе).
- «Порт» (port — кружок входа/выхода узла).
- «Гейт» (gate — ветка выхода If.true/If.false; влияет на порядок выполнения ребёнка).
- «STORE» (перманентное хранилище переменных на диск, одна запись на каждый pipeline.id).
- «PROMPT» (специальный JSON-фрагмент сообщений, который подставляется в шаблон запроса провайдера).
- «OUTx» (короткая ссылка на текст из выхода ноды nX).
- «incoming» (снимок входящего HTTP-запроса: метод, URL, заголовки, JSON и т.д.).
РАЗДЕЛ 1 — НОДЫ: КТО ЕСТЬ КТО (КРАТКО, ШУТКИ В СТОРОНУ)
- SetVars (заводит твои переменные)
- Входы: нет (только depends).
- Выходы: vars — словарь новых переменных.
- Поведение: для каждой переменной задаёшь name и mode (string или expr). В режиме string значение обрабатывается шаблоном (... и {{ ... }}), в режиме expr — безопасным мини-диалектом выражений.
- Где смотреть реализацию: SetVarsNode._safe_eval_expr().
- If (ветвление по условию)
- Входы: depends.
- Выходы: true, false (гейты для «детей» по условию).
- Поведение: expr парсится как булево выражение (&&, ||, !, ==, !=, <, <=, >, >=, contains, скобочки). Внутри можно использовать ... и {{ ... }}.
- Реализация парсера: eval_condition_expr(), обёртка ноды: IfNode.run().
- 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().
- RawForward (прямой прокси)
- Входы: depends.
- Выходы: result, response_text.
- Ключи: base_url (может автоопределяться по входящему JSON-вендору), override_path, passthrough_headers, extra_headers, while_expr.
- Реализация: RawForwardNode.run().
- 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 примеров (пониже пояса — для тех, кто любит копипасту):
-
Заголовок авторизации в JSON-строке: {"Authorization":"Bearer VAR:incoming.headers.authorization"} Объяснение: VAR:... берёт заголовок из входа (incoming.headers.authorization).
-
Провайдерная модель «как пришла» (фигурные): "model": "{{ model }}" Объяснение: {{ ... }} вставляет строку без кавычек лишний раз.
-
Число по умолчанию: "temperature": {{ incoming.json.temperature|default(0.7) }} Объяснение: default(0.7) сработает, если температуры нет.
-
Лист по умолчанию: "stop": {{ incoming.json.stop|default([]) }} Объяснение: вставляет [] как настоящий массив.
-
Короткая вытяжка текста из ноды n2: "note": "OUT2" Объяснение: OUT2 — best-effort текст из ответа.
-
Точное поле из результата: "OUT:n2.result.choices.0.message.content" Объяснение: берёт конкретную ветку JSON из OUT ноды n2.
-
«Голая» переменная SetVars: "key": "MyOpenAiKey" Объяснение: имя без VAR/OUT — сперва ищется среди переменных.
-
STORE (между прогонами): "{{ STORE.KEEP|default('miss') }}" Объяснение: из стойкого хранилища (если clear_var_store=False).
-
Прокинуть запрос как есть в Gemini: VAR:incoming.json.contents Объяснение: квадратные дадут строку (для template это ок: JSON-строка без лишних кавычек — см. PROMPT).
-
JSON-путь с фигурными: {{ OUT.n1.result.obj.value|default(0) }} Объяснение: берёт число или 0.
-
Картинка из base64 переменной (img()): "image": "
" Объяснение: заменится на data:image/jpeg;base64,....
-
Сложная строка с несколькими макросами: "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):
-
Чистая математика: 128 + 64
-
Случайное число: rand()
-
Случайное из списка: choice(["red","green","blue"])
-
Безопасный int-диапазон: randint(10, 20)
-
from_json + доступ через jp: jp(from_json("{"a":{"b":[{"x":1},{"x":2}]}}"), "a.b.1.x") → 2
-
jp_text (склейка строк через « | »): jp_text(from_json("{"items":[{"t":"A"},{"t":"B"},{"t":"C"}]}"), "items.*.t", " | ") → "A | B | C"
-
Вытянуть из OUT (с шаблонной подстановкой): jp({{ OUT.n2.result }}, "choices.0.message.content") → текст первого ответа
-
Собрать data URL из файла: file_data_url("./img/cat.png", "image/png")
-
Ручная сборка data URL из base64: data_url(IMG_B64, "image/jpeg")
-
Преобразование строки JSON: from_json("[1,2,3]") → список [1,2,3]
-
Комбо с логикой: (rand() > 0.5) and "HEADS" or "TAILS"
-
Вложенные вызовы: 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 примеров (да-да, трижды проверено, хватит ныть):
-
Проверка статуса: {{ OUT.n2.result.status|default(0) }} >= 200 && {{ OUT.n2.result.status|default(0) }} < 300
-
Инверсия: !(OUT3 contains "error")
-
Сравнить переменную: LANG == "ru"
-
Двойная логика: (MSG contains "Hello") || (MSG contains "Привет")
-
Цепочка со скобками: ( CITY == "Moscow" && {{ params.max_tokens|default(0) }} > 0 ) || FALLBACK == "yes"
-
Списки и contains: contains(["a","b","c"], "b")
-
Числа и сравнения: {{ OUT.n1.result.value|default(0) }} >= 10
-
Пустые значения: {{ missing|default("") }} == ""
-
Комбо macOS: contains(VAR:incoming.url, "/v1/") && (VAR:incoming.method == "POST")
-
Несколько слоёв default(): {{ incoming.json.limit|default(params.limit|default(100)) }} > 50
-
Сложное условие с 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 паттернов:
-
Повтори до 3 раз: while_expr: "cycleindex < 3"
-
Повтори, пока OUT3 не содержит «ok»: while_expr: "!(OUT3 contains "ok") && cycleindex < 10"
-
Ретраи на ошибках сети: ignore_errors: true while_expr: "WAS_ERROR || ({{ OUT.n4.result.status|default(0) }} >= 500)"
-
Комбо с внешним If — заменяем If: Вместо If.true/false делай while_expr, который набивает нужный результат (например, пока не получишь 2xx от апстрима).
-
Изменение запроса по итерации: Используй cycleindex внутри template (например, «page»: {{ vars.page_start|default(1) }} + cycleindex).
-
Дожидаться готовности ресурса: while_expr: "!(OUT4 contains "READY") && cycleindex < 30"
-
Прерывание на плохих данных: while_expr: "!(OUT7 contains "fatal") && cycleindex < 5"
-
Backoff вручную (временную задержку делай sleep_ms): sleep_ms: {{ cycleindex }} * 500
-
Прокси-ретрай RawForward по тексту ответа: ignore_errors: true while_expr: "(OUT:n1.result.text contains "try again") && cycleindex < 4"
-
Gemini «Stream failed to …» из коробки: while_expr: "(OUT3 contains "Stream failed to") || (OUT3 contains "gemini-2.5-pro")" (ровно как в твоих пресетах) Добавь " || WAS_ERROR" если хочешь ретраить также на исключениях (при ignore_errors: true).
-
Проверка флага из STORE: while_expr: "{{ STORE.SHALL_CONTINUE|default(false) }} && cycleindex < 10"
-
Сложный сценарий: 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 примеров (разные таргеты и трюки):
-
Классика: входящие Gemini contents + твой PROMPT (OpenAI target) "VAR:incoming.json.contents & PROMPT" Результат: messages содержит и конвертированные входящие (model→assistant), и твои blocks.
-
PROMPT первым (OpenAI): "PROMPT@pos=prepend & VAR:incoming.json.contents" Результат: system из PROMPT — в самом начале messages.
-
Вставка в конкретный индекс (OpenAI): "VAR:incoming.json.messages & PROMPT@pos=1" Результат: вторым элементом окажутся твои блоки.
-
Негативный индекс (OpenAI): "VAR:incoming.json.messages & PROMPT@pos=-1" Результат: перед самым последним.
-
Для Gemini: openai.messages + PROMPT "VAR:incoming.json.messages & PROMPT" Результат: contents и systemInstruction соберутся; system из incoming и PROMPT сольются.
-
Для Claude: openai.messages + PROMPT "VAR:incoming.json.messages & PROMPT" Результат: messages + top-level system (как строка/блоки).
-
Сырый JSON-строковый сегмент: "{"messages": [{"role":"user","content":"Hi"}] } & PROMPT" Результат: корректно распарсится и слепится.
-
Списковая форма сегмента: "[{"role":"user","content":"A"},{"role":"assistant","content":"B"}] & PROMPT" Результат: нормализуется под целевой провайдер.
-
Системные тексты из разных форматов — сольются: "[{"messages":[{"role":"system","content":"SYS IN"}]}] & PROMPT" Результат: system_text включает обе части.
-
Подмешать внешнюю систему в Claude без top-level system (claude_no_system): В конфиге ноды поставь claude_no_system=true — тогда system из PROMPT положим первым user-сообщением.
-
Очистка пустых: Если твой сегмент даёт пустые тексты — они выкинутся и JSON не сломается. Не плачь.
-
Микс строк + JSON: "Просто строка & PROMPT" Результат: «Просто строка» упакуется корректно (как user/text) в нужную структуру провайдера.
И да, это позволяет не писать руками половину «склейки» в template — ты описываешь, откуда что привнести, а движок доведёт до провайдерного формата.
РАЗДЕЛ 6.1 — PROMPT_PREPROCESS (pre‑merge 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.
Примеры:
-
Удалить поля, где значение содержит «Текст», и вставить перед последним: VAR:incoming.json.contents delKeyContains "Текст" delpos=-1
-
Удалить «debug» с учётом регистра и подчистить пустые контейнеры: VAR:incoming.json.messages delKeyContains "debug" case=cs pruneEmpty
-
Несколько подстрок + вставка в начало: VAR:incoming.json delKeyContains "кеш" delKeyContains "cache" delpos=prepend
-
Смешанный пайплайн: сначала пред‑сегменты, затем: 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 примеров:
-
Обычный путь: "a.b.c" на {"a":{"b":{"c":10}}} → 10
-
Индекс массива: "items.1" на {"items":[10,20,30]} → 20
-
Вложено: "items.1.title" на items=[{title:"A"},{title:"B"}] → "B"
-
Звёздочка по массиву: "items.*.title" на items=[{title:"A"},{title:"B"}] → ["A","B"]
-
Звёздочка по объекту: "..name" на {"x":{"name":"X"}, "y":{"name":"Y"}} → ["X","Y"]
-
Смешанный: "candidates.0.content.parts.*.text" (Gemini) → все тексты
-
Несуществующее поле: "obj.miss" → None
-
Склейка текстов (jp_text): jp_text(value, "items.*.desc", " | ") → "a | b | c"
-
Взять base64 из inlineData: "candidates.0.content.parts.1.inlineData.data"
-
Несколько уровней массивов: "a..b..c"
-
Индекс вне границ: "items.99" → None
-
Список/объект → строка (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, содержимое не очищается между запуском.
- Примеры макросов:
- STORE:KEEP
- {{ STORE.KEEP|default('none') }}
РАЗДЕЛ 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 МИНУТУ)
- «Прокинуть, но если 502 — подёргать ещё»
- RawForward:
- ignore_errors: true
- while_expr: "{{ OUT.n3.result.status|default(0) }} == 502 && cycleindex < 3"
- «Gemini: взять входные contents, добавить свой system и отправить в OpenAI»
- ProviderCall (openai):
- prompt_combine: "VAR:incoming.json.contents & PROMPT@pos=prepend"
- «Сделать язык вывода в зависимости от заголовка X-Lang»
- SetVars:
- LANG (string): "VAR:incoming.headers.X-Lang"
- If:
- expr: "LANG == 'ru'"
- Return:
- text_template: "OUT2" (где n2 — твоя ветка для RU)
- «Доставать base64 из ответа и вставлять картинкой куда нужно»
- jp_text(OUT, "candidates..content.parts..inlineData.data", "")
- Либо сразу data URL через img(png)... на канвасе.
- «Стабильно вытягивать текст из 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
- Выбери provider (openai/gemini/claude) в инспекторе.
- Заполни provider_configs.{provider}.(base_url, endpoint, headers, template).
- Подстановки в headers/template — через ... / {{ ... }} (см. render_template_simple())
- Заполни Prompt Blocks (system/user/assistant/tool) — они в PROMPT.
- Если нужно смешать с входящим payload — используй prompt_combine (Раздел 6).
- Если нужно ретраить — поставь while_expr/ignore_errors/sleep_ms.
- Если нужно извлекать текст особым образом — выбери preset или text_extract_*.
- Соедини depends по порядку и посмотри цвета проводов (Раздел 10).
- Готово. Без косяков? Правда? Ну, посмотрим.
СЛИШКОМ ДЛИННО, НЕ ЧИТАЛ:
- ... — текстовая подстановка.
- {{ ... }} — типобезопасная подстановка (числа/объекты).
- 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.
Если ты дошёл досюда — ну… я не впечатлена. Просто запомни и не ломи моя нервная система, ладно? Хмф.