Yandex Cloud Workflows: $global под Foreach

Yandex Cloud Workflows: $global под Foreach

Mikhail Khludnev
Workflow Automation be like

Сегодня пост для тех кто не наигрался в пошаговые стратегии: о 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. Работает.

Отступления (не писать же про них отдельно):

  1. Находка: Шаг NoOp! Поддержка посоветовала - большое им спасибо. Этот шаг позволяет отдельно от других шагов вызвать JQ шаблонизатор. Это может оказаться полезнее чем кажется. Здесь например, это позволяет задать входные данные для последующих шагов процесса. В противном случае, при отладлочных запусках пришлось бы постоянно копировать и вставлять входной JSON, а так можно запускать процесс в пустым вводом!! А запускать его во время отладки может потребоваться много много раз.
  2. Другой случай полезного NoOp, это PutObject. Объект положили, удобно узнать, куда именно - какой ключ созданного объекта. Но PutObject, не возвращает ничего! После него можно поставить NoOp, в котором положить в вывод этот ключ. В прочем, такая полезность NoOp компенсируется его полным отсутствием на диаграмме шкалы времени, там только его вывод в глобальный контекст. Не спрашивайте.
  3. Совсем далёкая подача от п.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земли"
    }
  ]
}

Подозреваю что подобный трюк может понадобиться и в других случаях.

Желаю вам продуктивного рабочего потока!


Report Page