НАДTAVERN VARIABLES — ГАЙД ДЛЯ ТЕХ, КТО СЕГОДНЯ «НА МИНИМАЛКАХ» И ВСЁ РАВНО ХОЧЕТ, ЧТОБЫ РАБОТАЛО Смотри сюда, слабак. Я — твой наидобрейший цун-энциклопедист, и сейчас я очень терпеливо (фрр) объясню так, чтобы даже ты не накосячил. Прочитаешь до конца — и у тебя получится. Может быть. Если постараешься. М-м… не думай, что я делаю это ради тебя! - Источники истины (это значит «код, который реально решает», а не чаты): - Исполнение пайплайна: [PipelineExecutor.run()](agentui/pipeline/executor.py:402) - Нода SetVars — выражения и функции: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1290) - Нода ProviderCall — вызов провайдера и PROMPT: [ProviderCallNode.run()](agentui/pipeline/executor.py:2084) - Нода RawForward — прямой прокси: [RawForwardNode.run()](agentui/pipeline/executor.py:3547) - Нода Return — формат финального ответа: [ReturnNode.run()](agentui/pipeline/executor.py:3930) - Нода If — парсер условий: [IfNode.run()](agentui/pipeline/executor.py:4024) - While-обёртка для ProviderCall: [_providercall_run_with_while()](agentui/pipeline/executor.py:4075) - While-обёртка для RawForward: [_rawforward_run_with_while()](agentui/pipeline/executor.py:4243) - Шаблоны: [[...]] и {{ ... }} здесь: [render_template_simple()](agentui/pipeline/templating.py:205) - Условия if/while (&&, ||, contains, скобочки): [eval_condition_expr()](agentui/pipeline/templating.py:391) - JSONPath (упрощённый, но хватит): [_json_path_extract()](agentui/pipeline/executor.py:1569) - Склейка текста при JSONPath: [_stringify_join()](agentui/pipeline/executor.py:1610) - UI инспектор Prompt Blocks: [PM.setupProviderCallPMUI()](static/js/pm-ui.js:9) - Экспорт/импорт пайплайна в редакторе: [AgentUISer.toPipelineJSON()](static/js/serialization.js:104), [AgentUISer.fromPipelineJSON()](static/js/serialization.js:286) - Веб-редактор: [editor.html](static/editor.html) Перед началом (термины в скобках — это определение, не морщи нос): - «Пайплайн» (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()](agentui/pipeline/executor.py:1197). 2) If (ветвление по условию) - Входы: depends. - Выходы: true, false (гейты для «детей» по условию). - Поведение: expr парсится как булево выражение (&&, ||, !, ==, !=, <, <=, >, >=, contains, скобочки). Внутри можно использовать [[...]] и {{ ... }}. - Реализация парсера: [eval_condition_expr()](agentui/pipeline/templating.py:391), обёртка ноды: [IfNode.run()](agentui/pipeline/executor.py:3538). 3) 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()](agentui/pipeline/executor.py:1991). 4) RawForward (прямой прокси) - Входы: depends. - Выходы: result, response_text. - Ключи: base_url (может автоопределяться по входящему JSON-вендору), override_path, passthrough_headers, extra_headers, while_expr. - Реализация: [RawForwardNode.run()](agentui/pipeline/executor.py:3105). 5) Return (оформление финального ответа для клиента) - Входы: depends. - Выходы: result (в формате openai/gemini/claude/auto), response_text (то, что вставили). - Ключи: target_format (auto/openai/gemini/claude), text_template (по умолчанию [[OUT1]]). - Реализация: [ReturnNode.run()](agentui/pipeline/executor.py:3444). Под капотом все узлы гоняет исполнитель «волнами» или итеративно: - Главная точка входа: [PipelineExecutor.run()](agentui/pipeline/executor.py:316). - И есть режим retry/циклов в узлах ProviderCall/RawForward — см. while в Разделе 5. РАЗДЕЛ 2 — ПЕРЕМЕННЫЕ И МАКРОСЫ ([[...]] ПРОТИВ {{ ... }}) С 12 ПРИМЕРАМИ Смысл (запомни, ладно?): - [[...]] (квадратные макросы) — текстовая подстановка со строкификацией (всегда превращается в строку, объекты — в JSON-строку). - {{ ... }} (фигурные вставки) — типобезопасная подстановка «как есть» (числа остаются числами, объекты — объектами), а ещё есть фильтр |default(...). Доступные макросы (см. [render_template_simple()](agentui/pipeline/templating.py:205)): - [[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](agentui/pipeline/templating.py:41)). Фигурные {{ ... }}: - {{ 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](img(jpeg)[[IMG_B64]])" Объяснение: заменится на data:image/jpeg;base64,.... 12) Сложная строка с несколькими макросами: "msg": "User=[[VAR:chat.last_user]] | Echo=[[OUT1]]" Объяснение: комбинируй сколько хочешь, лишь бы JSON остался валидным. РАЗДЕЛ 3 — SETVARS: ВЫРАЖЕНИЯ, РАЗРЕШЁННЫЕ ФУНКЦИИ, ОПАСНО НЕ БУДЕМ (10+ ПРИМЕРОВ) Где код: [SetVarsNode._safe_eval_expr()](agentui/pipeline/executor.py:1197). Он парсит мини-язык через 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()](agentui/pipeline/templating.py:391). Он превращает видимые тобой токены в безопасное 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()](agentui/pipeline/executor.py:3589) - Для RawForward: [_rawforward_run_with_while()](agentui/pipeline/executor.py:3741) - Обёртка делает «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()](agentui/pipeline/executor.py:1991) — см. кусок обработки 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 (pre‑merge DSL: фильтрация/позиционирование сегментов ДО prompt_combine) Где реализовано: в [ProviderCallNode.run()](agentui/pipeline/executor.py:2083). Это выполняется перед сборкой [[PROMPT]]/prompt_combine. Поле конфигурации ноды: prompt_preprocess (многострочное). Идея: - Каждая строка prompt_preprocess задаёт «пред‑сегмент», который будет вставлен в будущий массив сообщений/контента до обработки [prompt_combine (DSL &)](agentui/pipeline/executor.py:2230). - Эти пред‑сегменты конвертируются под целевого провайдера (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()](agentui/pipeline/executor.py:2211). Ограничения и заметки: - Это локальная предобработка именно сегментов для промпта (не глобальная фильтрация всего тела запроса). - Если пред‑сегменты и prompt_combine пусты — результат совпадает с классическим [[PROMPT]] (Prompt Blocks). РАЗДЕЛ 7 — JSON PATH (НАШ ПРОСТОЙ ДИАЛЕКТ) + 12 ПРИМЕРОВ Где реализовано: [_json_path_extract()](agentui/pipeline/executor.py:1475). Синтаксис (очень простой): - Путь вида a.b.c — точки для полей объектов. - Числовой индекс для массивов: items.0.title - Шаг «*» разворачивает все значения словаря или все элементы списка: items.*.title - Если на каком-то шаге ничего не найдено — вернёт None. - jp(...) → отдаёт найденное значение или список значений, jp_text(...) → склеит строки через join_sep (см. [_stringify_join()](agentui/pipeline/executor.py:1517)). 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()](agentui/pipeline/templating.py:133)) ищет: - 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](static/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](static/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" 2) «Gemini: взять входные contents, добавить свой system и отправить в OpenAI» - ProviderCall (openai): - prompt_combine: "[[VAR:incoming.json.contents]] & [[PROMPT]]@pos=prepend" 3) «Сделать язык вывода в зависимости от заголовка X-Lang» - SetVars: - LANG (string): "[[VAR:incoming.headers.X-Lang|default('en')]]" - If: - expr: "[[LANG]] == 'ru'" - Return: - text_template: "[[OUT2]]" (где n2 — твоя ветка для RU) 4) «Доставать base64 из ответа и вставлять картинкой куда нужно» - jp_text(OUT, "candidates.*.content.parts.*.inlineData.data", "") - Либо сразу data URL через img(png)[[...]] на канвасе. 5) «Стабильно вытягивать текст из 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()](agentui/pipeline/executor.py:2055)). - Линии «исчезают» в редакторе: - Жми «Загрузить пайплайн» ещё раз — отложенные порты и наблюдатели синхронизируются. - [[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). - Подстановки в headers/template — через [[...]] / {{ ... }} (см. [render_template_simple()](agentui/pipeline/templating.py:205)) 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. Если ты дошёл досюда — ну… я не впечатлена. Просто запомни и не ломи моя нервная система, ладно? Хмф.