Yandex Cloud Workflows: $global под Foreach
Mikhail Khludnev
Сегодня пост для тех кто не наигрался в пошаговые стратегии: о Yandex Cloud Serverless Integration Workflows. Нетрудно догадаться, что это представитель обширнейшего поля Workflow Automation Tools, eg OSS: Apache Airflow/Hop, n8n to name a few. Но YC Wokflows не Open Source, конечно же. Ок, ближайший аналог, скажем AWS Step Functions.
Одна из его характерных особенностей - использование JQ как одного из краеугольных камней. Прямо скажем, не Yandex's vibe 🚲 ⛔. Не могу сказать, что было легко с JQ, нахлынули какие-то воспоминания об XSLT (не кликайте, не надо!). В целом, конечно работает, но у любой абстрации существует критическая точка взаимодействия с реальным миром: по отдельности $global, Foreach, и сложные шаги, например, работают замечательно, но их комбинация, пока является крайним случаем, где всё не совсем очевидно.
Рассмотрим пример, простого вызова языковой модели:
yawl: '0.1'
start: step-no-op706
steps:
step-no-op706:
noOp:
output: '\({"sys_prompt": "соль", "usr_prompt":"земли"})'
next: step-foundationModelsCall770
step-foundationModelsCall770:
foundationModelsCall:
generate:
temperature: 0
maxTokens: 100
messages:
messages:
- role: system
text: \(.sys_prompt)
- role: user
text: \(.usr_prompt)
modelUrl: gpt://yrownfldrid/yandexgpt-lite/latest
output: '\({"step-foundationModelsCall770": .})'
description: ''
Ничего необычного: задаём system&user prompts для inference. Работает.
Отступления (не писать же про них отдельно):
- Находка: Шаг NoOp! Поддержка посоветовала - большое им спасибо. Этот шаг позволяет отдельно от других шагов вызвать JQ шаблонизатор. Это может оказаться полезнее чем кажется. Здесь например, это позволяет задать входные данные для последующих шагов процесса. В противном случае, при отладлочных запусках пришлось бы постоянно копировать и вставлять входной JSON, а так можно запускать процесс в пустым вводом!!
А запускать его во время отладки может потребоваться много много раз. - Другой случай полезного NoOp, это PutObject. Объект положили, удобно узнать, куда именно - какой ключ созданного объекта. Но PutObject, не возвращает ничего! После него можно поставить NoOp, в котором положить в вывод этот ключ. В прочем, такая полезность NoOp компенсируется его полным отсутствием на диаграмме шкалы времени, там только его вывод в глобальный контекст. Не спрашивайте.
- Совсем далёкая подача от п.1. в соседний сервис облачных функций. Если workflow после исправления можно перезапустить скопировав параметры предыдущего запуска, то окошко параметров тестирования функции всегда очищается, и дроп-дауна истори не имеет. Печаль. Оказалось очень удобно отлогировать параметры вызова
print(json.dumps(event)), и потом копировать их из логов, или сохранить какsample_invocation.jsonотдельным файлом в редакторе. Файлов-то может быть много!
И так, простейший inference работает. Теперь допустим у нас список user prompts, и нам надо их всех перебрать (batch inference конечно подойдёт, просто пример на список из одного элемента).
yawl: '0.1'
start: step-no-op706
steps:
step-no-op706:
noOp:
output: '\({"sys_prompt": "соль", "usr_prompt":["земли"]})' # массив на вводе
next: step-foreach780
step-foreach780:
foreach:
input: \(.usr_prompt | map( {key:.})) # for нужен массив объектов а не строк, превращем строки в объекты
output: '\({"step-foreach780": .})'
do:
start: step-foundationModelsCall296
steps:
step-foundationModelsCall296:
foundationModelsCall:
generate:
temperature: 0
maxTokens: 100
messages:
messages:
- role: system
text: \(.sys_prompt) # а вот внешний конекст тут не доступен - null
- role: user
text: \(.key) # переменная цикла - работает
modelUrl: gpt://yrownfldrid/yandexgpt-lite/latest
output: '\({"inference": .alternatives[0].message.text})'
description: ''
Модель отвечает недоумённо:
{
"step-foreach780": [
{
"inference": "Уточните, пожалуйста, что нужно сделать с этим словом?"
}
]
}
Внимательные читатели уже догадались, что одно-то слово user message модель получает, а вот system message - нет. Происходит это потому что на ввод в шаги цикла подаётся только переменная цикла! Как и написано, и наверное, переменная $global поможет нам? Заменим system message на
text: \($global.sys_prompt)
$global кстати, нет в JQ playground! Где его только нет. Хотя в JQ playground много чего нет.
Получаем ошибку, что по-моему лучше чем пустое (null) значение в прошлый раз!
failed to evaluate JQ expression in messages[0] value, inner error: failed to compile JQ expression ("\($global.sys_prompt)"): variable not defined: $global
Это и есть проблема сложного шага со многими JQ полями. $global есть в выражении на вкладке Ввод (yaml input:), а в других выражениях - нет.
Найдено два решения:
- моё: явно передадим
$globalподвинув пелеменную цикла. Теперь у насiкак циклах нормальных языков! А$globalявно передадим какg. Теперь .i,.gдоступны во вложенных шаблонах
foundationModelsCall:
generate:
temperature: 0
maxTokens: 100
messages:
messages:
- role: system
text: "повтори все сообщения пользователя"
- role: user
text: \(.g.sys_prompt)
- role: user
text: \(.i.key)
modelUrl: gpt://yrownfldrid/yandexgpt-litelatest
input: \({i:., g:$global}) # тут мякотка!
output: '\({"inference": .alternatives[0].message.text})'
Теперь .i,.g доступны во вложенных шаблонах. Концепция примера, немного поменялась по ходу поста, но это только подтверждает ...
неправильноеспасибо специалистам поддержки за подсказку: явно подлить$globalили нужное свойство в Foreach.input при создании списка объектов на итерацию.
step-foreach780:
foreach:
input: \(.usr_prompt | map( {key:., sys_prompt_assigned:$global.sys_prompt}))
output: '\({"step-foreach780": .})'
do:
start: step-foundationModelsCall296
steps:
step-foundationModelsCall296:
foundationModelsCall:
generate:
temperature: 0
maxTokens: 100
messages:
messages:
- role: system
text: "повтори все сообщения пользователя"
- role: user
text: \(.sys_prompt_assigned) # явно переданное
- role: user
text: \(.key)
modelUrl: gpt://yrownfldrid/yandexgpt-lite/latest
output: '\({"inference": .alternatives[0].message.text})'
Модель подтверждает:
{
"step-foreach780": [
{
"inference": "соль\nземли"
}
]
}
Подозреваю что подобный трюк может понадобиться и в других случаях.
Желаю вам продуктивного рабочего потока!