sync: UI animations, select styling, TLS verify flag via proxy second line, brand spacing

This commit is contained in:
2025-09-27 18:46:52 +03:00
parent 135c393eda
commit 2abfbb4b1a
52 changed files with 8029 additions and 1408 deletions

View File

@@ -6,9 +6,9 @@
Файлы, где «живет» эта магия:
- Сервер и конечные точки: [agentui/api/server.py](agentui/api/server.py)
- Исполнитель узлов (сердце конвейера): [PipelineExecutor.run()](agentui/pipeline/executor.py:170)
- Ноды: [ProviderCallNode.run()](agentui/pipeline/executor.py:1631), [RawForwardNode.run()](agentui/pipeline/executor.py:1939), [ReturnNode.run()](agentui/pipeline/executor.py:2256), [IfNode.run()](agentui/pipeline/executor.py:2350), SetVars внутри того же файла
- Шаблонизатор (подстановки [[...]] и {{ ... }}): [render_template_simple()](agentui/pipeline/templating.py:191), булевы выражения If: [eval_condition_expr()](agentui/pipeline/templating.py:336)
- Исполнитель узлов (сердце конвейера): [PipelineExecutor.run()](agentui/pipeline/executor.py:316)
- Ноды: [ProviderCallNode.run()](agentui/pipeline/executor.py:2007), [RawForwardNode.run()](agentui/pipeline/executor.py:2477), [ReturnNode.run()](agentui/pipeline/executor.py:2798), [IfNode.run()](agentui/pipeline/executor.py:2892), SetVars внутри того же файла
- Шаблонизатор (подстановки [[...]] и {{ ... }}): [render_template_simple()](agentui/pipeline/templating.py:196), булевы выражения If: [eval_condition_expr()](agentui/pipeline/templating.py:382)
— — —
@@ -218,10 +218,10 @@ Return сам завернёт текст в правильную структу
Когда нода (ProviderCall или RawForward) получила JSON от провайдера, движок старается «вынуть» из него удобный текст:
- OpenAI: choices[0].message.content
- Gemini: candidates[0].content.parts[0].text
- Gemini: все parts[].text во всех candidates склеиваются через "\n" (пустые/пробельные части игнорируются)
- Claude: content[].text (склейка)
- Если формат неизвестен — идёт «лучший догадчик» по глубине, чтобы найти текст
- Можно задать пресет (JSONPath) в настройках ноды или глобально в «Запуск» → «Пресеты парсинга OUTx»
- Можно задать пресет (JSONPath) в настройках ноды или глобально в «Запуск» → «Пресеты парсинга OUTx»; для Gemini альтернатива авто: json_path="candidates.*.content.parts.*.text", join_sep="\n"
Логика извлечения и пресетов находится в: [ProviderCallNode.run()](agentui/pipeline/executor.py:1631), [RawForwardNode.run()](agentui/pipeline/executor.py:1939)
@@ -275,12 +275,12 @@ Return сам завернёт текст в правильную структу
- Создание приложения/роутов: [create_app()](agentui/api/server.py:270)
- Нормализация payload (OpenAI/Gemini/Claude → единый вид): [normalize_to_unified()](agentui/api/server.py:44)
- Контекст для макросов (incoming/chat/params/…): [build_macro_context()](agentui/api/server.py:143)
- Исполнитель конвейера и «волны»: [PipelineExecutor.run()](agentui/pipeline/executor.py:170)
- Узел вызова провайдера (сборка [[PROMPT]], лог HTTP): [ProviderCallNode.run()](agentui/pipeline/executor.py:1631)
- Прямой форвард запроса: [RawForwardNode.run()](agentui/pipeline/executor.py:1939)
- Финализация ответа под формат клиента: [ReturnNode.run()](agentui/pipeline/executor.py:2256)
- Условия If (contains, &&, ||, ! и макросы): [IfNode.run()](agentui/pipeline/executor.py:2350), [eval_condition_expr()](agentui/pipeline/templating.py:336)
- Шаблонизатор макросов [[...]] и {{ ... }}: [render_template_simple()](agentui/pipeline/templating.py:191)
- Исполнитель конвейера и «волны»: [PipelineExecutor.run()](agentui/pipeline/executor.py:316)
- Узел вызова провайдера (сборка [[PROMPT]], лог HTTP): [ProviderCallNode.run()](agentui/pipeline/executor.py:2007)
- Прямой форвард запроса: [RawForwardNode.run()](agentui/pipeline/executor.py:2477)
- Финализация ответа под формат клиента: [ReturnNode.run()](agentui/pipeline/executor.py:2798)
- Условия If (contains, &&, ||, ! и макросы): [IfNode.run()](agentui/pipeline/executor.py:2892), [eval_condition_expr()](agentui/pipeline/templating.py:382)
- Шаблонизатор макросов [[...]] и {{ ... }}: [render_template_simple()](agentui/pipeline/templating.py:196)
- Определение провайдера по форме JSON: [detect_vendor()](agentui/common/vendors.py:8)
— — —
@@ -431,4 +431,266 @@ Return сам завернёт текст в правильную структу
Где посмотреть действующие значения
- В редакторе нажмите «ПЕРЕМЕННЫЕ»: там видно STORE (включая snapshot OUT_TEXT и алиасы OUT1/OUT2). Клик по строке — копирование макроса для вставки.
Схема работы панели описана в [static/editor.html](static/editor.html) и скриптах [static/js/serialization.js](static/js/serialization.js), [static/js/pm-ui.js](static/js/pm-ui.js).
Схема работы панели описана в [static/editor.html](static/editor.html) и скриптах [static/js/serialization.js](static/js/serialization.js), [static/js/pm-ui.js](static/js/pm-ui.js).
10) Работа с изображениями (ProviderCall + переменные)
Обзор
- Теперь можно:
- хранить изображения в переменных SetVars в виде data URL;
- вставлять изображения в Prompt Blocks через Markdown-нотацию;
- вызывать провайдеров OpenAI / Claude / Gemini с мультимодальными сообщениями.
Как хранить изображения в переменных
- В SetVars (mode=expr) доступны новые безопасные функции:
- file_b64(path) — читает файл и возвращает base64-строку (без префикса).
- data_url(b64, mime) — собирает data URL: data:mime;base64,<b64>.
- file_data_url(path, mime?) — обёртка: читает файл, определяет mime по расширению (если не указан) и возвращает полноценный data URL.
- Примеры SetVars:
- name: IMG1
mode: expr
value: file_data_url('static/samples/cat.png','image/png')
- name: IMG2
mode: expr
value: data_url(file_b64('static/samples/dog.jpg'), 'image/jpeg')
- После этого [[IMG1]] / [[IMG2]] вернут строку-полный data URL, пригодный для мультимодальных LLM.
Как вставлять картинки в Prompt Blocks
- Внутри блока (system/user/assistant) используйте Markdown-нотацию:
- ![alt]([[IMG1]])
- ![alt](https://example.org/pic.png)
- ![alt](data:image/webp;base64,....)
- Во время исполнения ProviderCall блок превращается в список частей:
- текстовые сегменты → {"type":"text","text":"..."}
- изображения → {"type":"image_url","url":"..."}
- Для plainтекста ничего не меняется (обратная совместимость полностью сохранена).
Маппинг для провайдеров
- OpenAI (chat.completions):
- message.content → либо строка, либо массив частей:
- {"type":"text","text":"..."}
- {"type":"image_url","image_url":{"url":"<http(s) или data:...>"}}
- Claude (messages v20230601):
- message.content — массив блоков:
- {"type":"text","text":"..."}
- {"type":"image","source":{"type":"url","url":"..."}} — для http(s)
- {"type":"image","source":{"type":"base64","media_type":"image/png","data":"..."}} — для data URL
- Gemini (generateContent):
- contents[].parts — микс:
- {"text":"..."}
- {"inline_data":{"mime_type":"image/png","data":"&lt;base64&gt;"}} — для data URL
- http/https URL напрямую не инлайнится; либо преобразуйте в data URL, либо реализуйте внешнюю загрузку (не требуется для базовой поддержки).
Мини-рецепт
1) SetVars:
- name: IMG
- mode: expr
- value: file_data_url('static/pictures/cat.png', 'image/png')
2) ProviderCall (OpenAI/Gemini/Claude), Prompt Blocks, user:
- Опиши картинку ниже кратко.
- ![cat]([[IMG]])
Примечания
- Никакого тримминга больших строк и base64 по умолчанию не применяется (по пожеланию). Храните картинки разумного размера.
- JSONтело ProviderCall остаётся валидным JSON после развёртывания [[PROMPT]], т.к. преобразование в «части» выполняется до формирования payload.
- Для OpenAI и Claude можно использовать как http/https URL, так и data URL. Для Gemini предпочтительно data URL (inline_data).
Где реализовано
- Backend преобразование блоков и маппинг под провайдеров — см. файл agentui/pipeline/executor.py.
- Новые функции expr для SetVars — см. тот же файл внутри SetVarsNode._safe_eval_expr().
- Шаблонизатор как и раньше отвечает за развёртку [[...]] / {{ ... }} — см. agentui/pipeline/templating.py.
10.1) Супер‑короткая запись для картинок: img(mime)[[...]]
Задача
- У вас есть base64строка (без префикса data:), например вы её извлекли пресетом JSONPath: candidates.0.content.parts.1.inlineData.data.
- Хотите получить ПОЛНЫЙ data URL без плясок с expr и функциям SetVars — прямо в обычной строке шаблона.
Решение (новый синтаксис)
- Пишите: img(mime)[[МАКРОС_С_BASE64]]
- На выходе получится строка: data:&lt;mime&gt;;base64,&lt;ваш_base64&gt;
Где это работает
- Везде, где используется шаблонизатор [render_template_simple()](agentui/pipeline/templating.py:191):
- Return.text_template
- ProviderCall.template / headers / endpoint
- RawForward.extra_headers / override_path
- SetVars (mode=string)
- Реализовано в препроцессоре шаблонов: [templating.py](agentui/pipeline/templating.py)
Синтаксис подробно
1) Базовый случай (по умолчанию image/png):
- img()[[OUT1]]
→ data:image/png;base64,[[OUT1]] (после разворачивания [[OUT1]] — получится полноценный data URL)
2) Явный тип по короткому имени:
- img(png)[[...]] → image/png
- img(jpeg)[[...]] → image/jpeg (alias: jpg → image/jpeg)
- img(webp)[[...]] → image/webp
- img(gif)[[...]] → image/gif
- img(svg)[[...]] → image/svg+xml
- img(bmp)[[...]] → image/bmp
- img(tif)[[...]] / img(tiff)[[...]] → image/tiff
3) Полный MIME:
- img(image/heic)[[...]] → data:image/heic;base64,...
4) Динамический MIME через фигурные скобки:
- img({{ OUT.n1.result.candidates.0.content.parts.1.inlineData.mimeType|default('image/png') }})[[OUT1]]
- Важно: MIME в круглых скобках можно задавать фигурными {{ ... }} — после сборки строки препроцессор оставит «data:{{ ... }};base64,...», и следующий проход шаблонизатора подставит реальное значение.
Пошаговые примеры (копируйте и вставляйте)
A. Вернуть картинку как строку data URL из Return без SetVars и expr
- У вас пресет JSONPath на base64: candidates.0.content.parts.1.inlineData.data.
- В Return.text_template напишите:
img(png)[[OUT1]]
или, если [[OUT1]] — не base64, а вытащить надо конкретное поле:
img(png)[[VAR:OUT.n1.result.candidates.0.content.parts.1.inlineData.data]]
- В результате Return отдаст текст:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
- Это удобно, когда потребитель умеет работать с data URL строками.
B. Сформировать HTML &lt;img&gt; в Return прямо в шаблоне
- В Return.text_template:
&lt;img src="img(jpeg)[[VAR:OUT.n1.result.candidates.0.content.parts.1.inlineData.data]]" alt="preview"&gt;
- На выходе получится полноценный HTML с src="data:image/jpeg;base64,...".
C. Положить data URL в переменную без expr
- SetVars → переменная IMG (mode=string):
value: img()[[VAR:OUT.n1.result.candidates.0.content.parts.1.inlineData.data]]
- Далее [[IMG]] — готовый data URL (можно вставлять куда угодно).
D. Динамический MIME из ответа провайдера
- Если в JSON есть mimeType:
img({{ OUT.n1.result.candidates.0.content.parts.1.inlineData.mimeType|default('image/png') }})[[VAR:OUT.n1.result.candidates.0.content.parts.1.inlineData.data]]
Часто задаваемые вопросы
Q1: Чем отличается от «старого» способа через SetVars + data_url()?
- Старый (через expr) остаётся и удобен, когда нужно переиспользовать значение в нескольких местах или делать дополнительную логику.
- Новый — мгновенная подстановка «в одну строку» прямо в шаблоне, без expr и функций. Он быстрее для типовой задачи «base64 → data URL».
Q2: Что будет, если внутри [[...]] вернётся не строка?
- Шаблонизатор приведёт значение к строке (для dict/list — сериализует в JSON). Но для data URL ожидается именно base64строка. Следите, чтобы путь/макрос давал строку, не объект.
Q3: Можно ли внутри скобок img(...) использовать [[...]]?
- Нет, внутри круглых скобок лучше использовать {{ ... }} (фигурные). Пример выше с mimeType показывает правильный путь. Квадратные [[...]] в круглых скобках не разбираются этим прелюдом, зато {{ ... }} спокойно подставятся на следующем шаге.
Q4: Нужно ли дописывать data:...;base64, вручную?
- Нет. Именно для этого и сделан синтаксис img(mime)[[...]]. Вы указываете только MIME (или оставляете пустым), а шаблонизатор добавляет префикс сам.
Q5: Что если мне нужен не текст data URL, а отрисованное превью?
- Блок «ЛОГИ → Data» уже показывает мини‑превью изображений для HTTPответов нод ProviderCall и RawForward, независимо от OUT1.
- Если вы хотите «превью прямо в Return», используйте HTML: &lt;img src="img(png)[[...]]"&gt; — потребитель, способный отображать HTML, увидит картинку.
Диагностика и отладка
- Если вы видите «сырую» base64строку вместо data URL, проверьте, что используете новый синтаксис img(...)[[...]] или явно дописали префикс вручную.
- Если «картинки» не видно в Return, удостоверьтесь, что получатель умеет отображать data URL напрямую. Для HTMLполучателей используйте тег &lt;img&gt;.
- Для ответов Gemini в логах панель «Data» показывает превью (мы подаём полные изображения через SSE), а в «Response» структура JSON остаётся читабельной — там триммится только base64, не нарушая остальной структуры.
Где в коде реализовано
- Препроцессор img(mime)[[...]] добавлен в [render_template_simple()](agentui/pipeline/templating.py:191); регулярное выражение и обработчик находятся в [templating.py](agentui/pipeline/templating.py).
- Общая логика развёртки [[...]] и {{ ... }} — тоже в [render_template_simple()](agentui/pipeline/templating.py:191).
- Подсветка и превью в логах — см. [static/editor.html](static/editor.html).
Короткая памятка (копируйте в нужные места шаблонов)
- По умолчанию PNG:
img()[[OUT1]]
- Явный JPEG:
img(jpeg)[[VAR:OUT.n1.result.candidates.0.content.parts.1.inlineData.data]]
- Динамический MIME:
img({{ OUT.n1.result.candidates.0.content.parts.1.inlineData.mimeType|default('image/png') }})[[OUT1]]
- HTML превью:
&lt;img src="img(png)[[OUT1]]" alt="image"&gt;
— — —
Приложение B. Что изменилось в парсинге Gemini и [[OUTx]] (20250921)
Коротко
- Что поменяли: теперь текст из ответов Gemini собирается из ВСЕХ частей parts[].text во всех candidates, пустые/пробельные строки игнорируются, результат склеивается через "\n".
- Где реализовано:
- Алгоритм besteffort для [[OUTx]]/алиасов: [\_best_text_from_outputs()](agentui/pipeline/templating.py:133)
- Явная стратегия "gemini" для нод: [\_extract_text_for_out()](agentui/pipeline/executor.py:1590)
- Что это даёт: [[OUT1]]/[[OUT2]]… и {{ OUT.nX.response_text }} больше не «пустеют», когда первая часть ответа — "\n". Возвращается весь человекочитаемый текст.
Что было и что стало
- Было: брали только первый элемент parts[0].text → если там "\n", получали пустоту.
- Стало: проходим все parts, берём непустые .text (strip) и склеиваем. Если частей нет — работает прежний «глубокий поиск текста» (fallback).
Примеры
- Ответ Gemini (упрощённо):
{
"candidates": [{
"content": { "parts": [{ "text": "\n" }, { "text": "Полезный ответ" }] }
}]}
- Раньше [[OUT…]] → "" (или "\n")
- Теперь [[OUT…]] → "Полезный ответ"
Альтернатива через пресет/JSONPath (без правок кода)
- Если хотите явно контролировать извлечение текста:
- strategy="jsonpath"
- json_path="candidates.*.content.parts.*.text"
- join_sep="\n"
- Это эквивалент новой авто‑логики для Gemini и годится, когда нужна строгая предсказуемость.
Подсказка: RawForward и auto
- Для RawForward мы иногда используем «подсказку» провайдера (auto). Сейчас этого достаточно. При желании можно расширить авто‑детекцию для ответов Gemini по ключу "candidates" в [detect_vendor()](agentui/common/vendors.py:8), но это не обязательно для текущей логики [[OUTx]].
— — —
Приложение C. Как правильно писать условия If (шпаргалка)
Где работает
- Узел If выполняет булево выражение и открывает ветку true/false. Исполнение: [IfNode.run()](agentui/pipeline/executor.py:2892)
- Парсер/оценка выражений: [eval_condition_expr()](agentui/pipeline/templating.py:382)
Что поддерживается
- Операторы:
- Логика: &&, ||, ! (вместо "not" используйте "!")
- Сравнения: ==, !=, <, <=, >, >=
- Ключевое слово: contains (подстрока для строк или membership для списков)
- Скобки: (...)
- Макросы внутри выражения:
- [[OUT1]], [[OUT:n2.result...]], [[NAME]]
- {{ OUT.nX.response_text|default('') }}, {{ params.temperature|default(0.7) }}
- Строковые литералы: "..." или '...' (следите за закрывающей кавычкой)
Семантика contains простым языком
- Строки: A contains B → строка B входит в строку A (по подстроке).
- Списки/множества: A contains B → элемент B присутствует в коллекции A.
Частые шаблоны (скопируйте)
- Проверка фразы в тексте провайдера:
[[OUT3]] contains "Stream failed to"
- Объединение условий:
([[OUT3]] contains "Stream failed to") || ([[OUT3]] contains "gemini-2.5-pro")
- Числовые сравнения с запасным значением:
{{ params.temperature|default(0.7) }} >= 0.3 && {{ params.temperature|default(0.7) }} <= 1
- Проверка «не пусто»:
{{ OUT.n2.response_text|default('') }} != ""
- Негативная проверка:
!([[OUT3]] contains "error")
- Сложная проверка с несколькими ветками:
({{ params.max_tokens|default(256) }} >= 128) && !([[OUT1]] contains "retry")
Советы по устойчивости
- Используйте |default(...) у {{ ... }}, чтобы не падать на None/пустых значениях.
- Для длинных/непредсказуемых строк проверяйте наличие ключевого фрагмента через contains.
- Оборачивайте группы условий в (...) — так легче читать и поддерживать.
- Внимание к кавычкам: "..." и '...' должны закрываться; это самая частая причина синтаксических ошибок.
Антишаблоны (чего избегать)
- Незакрытые кавычки в строковых литералах.
- Путаница с not: используйте "!" (а не слово not).
- Попытка вызывать произвольные функции — в If разрешён только contains(a, b) (под капотом), всё остальное запрещено.
Проверка на практике
- Пример из рабочего пайплайна (идея):
If.expr: (([[OUT3]] contains "Stream failed to") || ([[OUT3]] contains "gemini-2.5-pro"))
Trueветка → Return "[[OUT3]]"
Falseветка → RawForward или другой ProviderCall
Где ещё посмотреть
- Развёртка макросов [[...]] и {{ ... }}: [render_template_simple()](agentui/pipeline/templating.py:196)
- Логи If с развернутым выражением (для отладки): [IfNode.run()](agentui/pipeline/executor.py:2892)