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

666 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

НАД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 (premerge 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.
Если ты дошёл досюда ну я не впечатлена. Просто запомни и не ломи моя нервная система, ладно? Хмф.