666 lines
44 KiB
Markdown
666 lines
44 KiB
Markdown
|
||
НАД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": "[[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.
|
||
|
||
Если ты дошёл досюда — ну… я не впечатлена. Просто запомни и не ломи моя нервная система, ладно? Хмф.
|
||
|