Common.AI.* — Работа с AI-сервисами

Пакет плагинов для выполнения запросов к AI-сервисам (LLM, генерация изображений, речь и т.д.). Содержит: * клиентов и запросы к AI-провайдерам; * асинхронную обработку ответов; * таймауты и ошибки; * сохранение и нормализацию результатов. Пакет не содержит логики принятия решений и не является агентным или сценарным слоем. AI здесь — это внешний сервис, а не субъект.

Common.AI.ImageGen — Генерация изображений

Автор: Art Yg
Версия: 1.0

ImageGen — универсальный плагин для асинхронной генерации изображений через внешний Webhook Processor, с сохранением результата в lead (URL и/или base64) и поддержкой сценарного выхода (success/error/timeout).

Плагин спроектирован так, чтобы работать в двухфазном режиме, как LLMQuery:


Зачем он существует

В Metabot-сценариях нельзя считать генерацию изображения “быстрой синхронной функцией”:

ImageGen стандартизирует этот поток:


Что использует внутри

ImageGen — это обёртка, которая опирается на инфраструктурные компоненты:

Важно: RemoteApiCall как транспорт в принципе поддерживает любых провайдеров, но текущая версия ImageGen по умолчанию заточена под OpenAI Images API.


Поддерживаемые провайдеры

На текущий момент поддержан:

Если вам нужен другой провайдер (например, Replicate, Stability, Midjourney proxy, внутренний сервис) — свяжитесь с командой, и мы расширим плагин.

Примечание по архитектуре: сейчас таблица провайдеров (PROVIDERS) находится внутри плагина. При необходимости её можно вынести наружу (в конфиг бота/таблицу/отдельный реестр), чтобы вы могли подключать свои провайдеры без изменения кода плагина.


Что сохраняет

В зависимости от настроек:


Двухфазный протокол выполнения

PHASE 1 — отправка запроса

Когда isFirstImmediateCall = true:

  1. Инициализирует timeout-policy через Common.Platform.AsyncFallback (если задан timeout)

  2. Берёт токен из атрибута бота (по auth.tokenKey)

  3. Собирает итоговый prompt:

    • если задан prompts — собирает system[] + user[]

    • элементы массива могут быть:

      • inline строка
      • ссылка $alias (берётся из таблицы promptTable для agentName)
    • если в prompts используются $alias, то обязательны agentName и promptTable

  4. Формирует request body под /images/generations

  5. Отправляет запрос через RemoteApiCall.send(..., asyncResponse: true)

  6. (опционально) показывает messages.wait

  7. Возвращает false — сценарий “выходит” и ждёт callback


PHASE 2 — обработка callback

Когда isFirstImmediateCall = false:

  1. Проверяет, что это действительно callback от процессора (payload.is_async_response)

  2. Если это не callback (пользователь что-то написал):

    • показывает messages.processing
    • возвращает false
  3. Если callback:

    • снимает таймаут-job через AsyncFallback.unschedule()
    • парсит payload.content
    • извлекает url или b64_json
    • сохраняет в lead
    • если включён requireUrl и url нет → ошибка
  4. Если задан successScript — делает bot.run(successScript), иначе возвращается true


Конфигурация

Ниже описаны ключевые блоки ImageGen.run().


Provider + Auth

provider: "openai",
auth: { tokenKey: "OPENAI_API_KEY" }

Prompts (таблица + массивы)

ImageGen поддерживает интерфейс промптов “на вырост” — как в LLMQuery: массивы промптов и табличные ссылки.

agentName: "orion",
promptTable: "gpt_prompts",

prompts: {
  system: ["$avatar_brief_generator"],
  user: ["...основной запрос..."]
}

Принципы:

Примечание: OpenAI Images API принимает один prompt (строкой), поэтому ImageGen склеивает массивы в итоговый текст (обычно system + пустая строка + user). Это сделано для унификации с LLMQuery и поддержки других провайдеров/форматов в будущем.


Image параметры

image: {
  model: "dall-e-3",
  n: 1,
  size: "1024x1792",
  quality: "standard",
  style: "natural",
  background: null,
  response_format: "url"
}

Практика по форматам результата:


requireUrl

requireUrl: true

Если true, то отсутствие URL считается ошибкой (даже если пришёл b64).

Это удобно для MVP-потока “дай URL, а скачивание/сохранение сделаю потом”.


Таймаут (fallback)

timeout: {
  seconds: 120,
  script: "Orion_Image_Timeout"
}

Если callback не пришёл за указанное время — планировщик Metabot запускает timeout.script.

Таймаут реализован через Common.Platform.AsyncFallback внутри ImageGen.


Namespace для fallback

fallback: {
  namespace: "orion_image_reflection"
}

Нужен, чтобы несколько асинхронных операций не конфликтовали.

Если не задан, будет auto:


UX сообщения

messages: {
  wait: "🜁 Генерируем…",
  processing: "⏳ Подожди, ещё формируется…",
  error: "⚠️ Не удалось получить изображение"
}

Ошибки

error: {
  script: "Orion_Image_Error",
  flagAttr: "orion_image_error",
  reasonAttr: "orion_image_error_reason"
}

Успешный выход

successScript: "Orion_Image_Ready"

Если не указан — ImageGen.run() возвращается в ту же точку (return true), без перехода.


Таймаут: встроенный vs внешний

В большинстве сценариев таймаут проще и чище задавать внутри ImageGen через timeout, потому что плагин сам использует Common.Platform.AsyncFallback.

Внешнее управление таймаутом имеет смысл, если:


Примеры использования

Пример 1 — как используется в Orion: system prompt из таблицы + timeout + error + URL (MVP)

/**
 * orion_profiling_reflection_image
 *
 * Назначение:
 * - асинхронно сгенерировать вертикальный "Operator Reflection" образ
 * - сохранить URL в лид (скачивание/сохранение — отдельным шагом)
 * - timeout + error внутри ImageGen (как у LLMQuery)
 */

const ImageGen = require("Common.AI.ImageGen");

return ImageGen.run({
  lead,
  isFirstImmediateCall,

  code: "OrionActorImage",

  // Provider/Auth
  provider: "openai",
  auth: { tokenKey: "OPENAI_API_KEY" },

  // Prompts из таблицы + inline
  agentName: "orion",
  promptTable: "gpt_prompts",
  prompts: {
    // системный слой (табличный)
    system: ["$avatar_brief_generator"],

    // основной запрос (inline)
    user: [`
Create a vertical codex-grade mythotech Operator icon in Aurum Void aesthetic.

Aurum Void: cold matte gold schematic lines (axes, nodes, ritual UI glyphs) over deep graphite void.
No glossy sci-fi, no neon, no superhero vibe, no humor.

Faceless figure (hood/shadow/mask/void), calm, stable, centered on a strong vertical axis.
Heavy materials: carbon composite armor/robe, matte metal, dense fabric, worn realistic textures.
Subtle fog/particles for depth.

This is NOT a human portrait. It is an operational manifestation of a system entrepreneur / product leader at the scaling stage.
Output: a visual artifact, not an illustration.
    `.trim()]
  },

  // Таймаут (fallback)
  timeout: {
    seconds: 120,
    script: "Orion_Image_Timeout"
  },

  // Ошибки (как в LLMQuery)
  error: {
    script: "Orion_Image_Error",
    flagAttr: "orion_image_error",
    reasonAttr: "orion_image_error_reason"
  },

  // Namespace чтобы не конфликтовать
  fallback: {
    namespace: "orion_image_reflection"
  },

  // MVP: хотим именно URL
  requireUrl: true,

  image: {
    model: "dall-e-3",
    size: "1024x1792",
    quality: "standard",
    style: "natural",
    response_format: "url"
  },

  messages: {
    wait: "🜁 ORION формирует визуальное отражение…",
    processing: "⏳ Подожди, образ ещё куется…",
    error: "⚠️ Не удалось получить изображение"
  },

  save: {
    urlAttr: "orion_actor_image_url",
    b64Attr: "orion_actor_image_b64",
    rawJsonAttr: "orion_actor_image_payload"
  }

  // successScript: "Orion_Image_Ready" // опционально
});

Пример 2 — минимальный сценарий с таблицей промптов, без successScript

Подходит, когда вы хотите вернуться “в ту же точку” и решать дальше в текущем шаге.

const ImageGen = require("Common.AI.ImageGen");

return ImageGen.run({
  lead,
  isFirstImmediateCall,

  code: "OperatorIcon",

  provider: "openai",
  auth: { tokenKey: "OPENAI_API_KEY" },

  agentName: "orion",
  promptTable: "gpt_prompts",
  prompts: {
    system: ["$avatar_brief_generator"],
    user: ["Create a vertical mythotech Operator icon in Aurum Void aesthetic..."]
  },

  timeout: { seconds: 90, script: "Image_Timeout" },
  error: { script: "Image_Error" },

  requireUrl: true,

  image: {
    model: "dall-e-3",
    size: "1024x1792",
    response_format: "url"
  },

  save: {
    urlAttr: "operator_icon_url",
    rawJsonAttr: "operator_icon_payload"
  }
});

Пример 3 — внешний контроль timeout через AsyncFallback (продвинутый режим)

Этот способ полезен, если:

const ImageGen = require("Common.AI.ImageGen");
const AsyncFallback = require("Common.Platform.AsyncFallback");

if (isFirstImmediateCall) {
  AsyncFallback.configure({
    lead,
    namespace: "orion_image_reflection",
    timeout: { seconds: 120, script: "Orion_Image_Timeout" },
    error: { flagAttr: "orion_image_error", reasonAttr: "orion_image_error_reason" }
  }).schedule();
}

const res = ImageGen.run({
  lead,
  isFirstImmediateCall,

  code: "OrionActorImage",
  provider: "openai",
  auth: { tokenKey: "OPENAI_API_KEY" },

  agentName: "orion",
  promptTable: "gpt_prompts",
  prompts: {
    system: ["$avatar_brief_generator"],
    user: ["Create a vertical mythotech Operator icon in Aurum Void aesthetic..."]
  },

  // timeout внутри не задаём, потому что контролируем снаружи
  error: {
    script: "Orion_Image_Error",
    flagAttr: "orion_image_error",
    reasonAttr: "orion_image_error_reason"
  },

  requireUrl: true,

  image: {
    model: "dall-e-3",
    size: "1024x1792",
    response_format: "url"
  },

  save: {
    urlAttr: "orion_actor_image_url",
    rawJsonAttr: "orion_actor_image_payload"
  }
});

if (!isFirstImmediateCall && res === true) {
  AsyncFallback.configure({
    lead,
    namespace: "orion_image_reflection"
  }).unschedule();
}

return res;

Частые сценарии отказов и как реагировать


Итог

Common.AI.ImageGen — стандартизированный способ подключить генерацию изображений в сценарии Metabot:

Common.AI.Prompts — Универсальный резолвер и сборщик промптов

Автор: Art Yg
Версия: 1.0

Prompts — инфраструктурный helper для унифицированной работы с промптами в сценариях Metabot и внутри других плагинов (например, Common.AI.ImageGen, LLMQuery/LMClient).

Он решает типовую боль: не держать промпты в коде, не копировать одно и то же “склеивание”, и иметь единый механизм:


Зачем он существует

В продуктовых сценариях промпты обычно:

Без Prompts это превращается в дублирование:

Prompts стандартизирует это как маленький инфраструктурный слой, который можно подключать где угодно.


Основные принципы


Минимальные требования

Для работы в “inline-режиме” достаточно:

  1. lead
  2. вызвать Prompts.buildText(...)

Для работы с табличными ссылками ($... / @...) дополнительно нужно:

  1. наличие table.find в runtime
  2. таблица промптов (promptTable)
  3. агент (agentName) — обязателен только для $name

Что умеет Prompts

1) Ссылки на промпты из таблицы

Поддерживаются два типа ref:

Пример:


2) Макросы (переменные из lead и bot)

Prompts умеет подставлять значения из атрибутов:

Поведение:


3) Нормализация входов и сборка блоков

На вход можно дать:

Дальше Prompts собирает:

И может вернуть:


Конфигурация

Все методы используют общие опции.

DEFAULTS

{
  promptTable: "gpt_prompts",
  agentName: null,   // обязателен для "$name"
  strict: true,      // если промпт не найден → throw
  applyMacros: true
}

Важные правила


API Prompts

Prompts.toArray(value)

Нормализует значение в массив строк.

Используется внутри, но можно использовать и снаружи.


Prompts.resolveOne(ref, opts)

Резолвит одну строку:


Prompts.resolveMany(list, opts)

Резолвит список refs/строк → массив строк.


Prompts.applyMacros(str, lead)

Применяет:


Prompts.buildBlocks(input, opts)

Собирает блоки по секциям:

{
  system: [],
  user: [],
  last: [],
  all: []
}

Prompts.buildText(input, opts)

Собирает итоговый prompt как строку:


Табличный формат промптов

Prompts ожидает, что таблица (promptTable) содержит хотя бы поля:

Запрос выполняется через:

table.find(promptTable, ["prompt"], [
  ["agent_name", agent],
  ["name", name]
]);

Использование

Ниже примеры именно “для статьи”: чтобы читатель увидел, что есть несколько режимов и зачем это.


Примеры

Пример 1 — Inline: без таблиц, без агента

Подходит для простых сценариев и MVP.

const Prompts = require("Common.AI.Prompts");

const prompt = Prompts.buildText(
  {
    system: [
      "You are a strict image generator. Output must be cinematic, realistic, and calm."
    ],
    user: [
      "Create a vertical mythotech Operator icon in Aurum Void aesthetic."
    ]
  },
  { lead } // agentName/promptTable не нужны
);

// prompt — готовая строка, без обращений к таблицам

Пример 2 — Табличный промпт агента: $alias

Если используешь $..., то agentName обязателен, иначе будет throw.

const Prompts = require("Common.AI.Prompts");

const prompt = Prompts.buildText(
  {
    system: [
      "$avatar_brief_generator" // берём из таблицы для агента orion
    ],
    user: [
      "Output should be a codex-grade artifact. No neon. No superhero vibe."
    ]
  },
  {
    lead,
    agentName: "orion",
    promptTable: "gpt_prompts"
  }
);

Пример 3 — Общий промпт: @alias (agentName не нужен)

@name всегда читается из агента <<common>>.

const Prompts = require("Common.AI.Prompts");

const prompt = Prompts.buildText(
  {
    system: [
      "@safety_rules",
      "@style_aurum_void"
    ],
    user: [
      "Create an abstract Operator icon."
    ]
  },
  {
    lead,
    promptTable: "gpt_prompts"
  }
);

Пример 4 — Макросы: подтягиваем контекст из lead и bot

const Prompts = require("Common.AI.Prompts");

// допустим:
// lead.getAttr("actor_stage") = "scaling"
// bot.getAttr("BRAND_TONE") = "discipline, meaning, depth"

const prompt = Prompts.buildText(
  {
    system: [
      "Tone: {{$$BRAND_TONE}}"
    ],
    user: [
      "Stage: {{$actor_stage}}",
      "Create a vertical Operator artifact."
    ]
  },
  {
    lead,
    applyMacros: true
  }
);

Пример 5 — Сборка blocks отдельно (для отладки/логирования)

Иногда полезно видеть, какие блоки получились до склейки.

const Prompts = require("Common.AI.Prompts");

const blocks = Prompts.buildBlocks(
  {
    system: ["@style_aurum_void", "$avatar_brief_generator"],
    user: ["Generate an icon for current stage: {{$actor_stage}}"]
  },
  {
    lead,
    agentName: "orion",
    promptTable: "gpt_prompts"
  }
);

// blocks.system / blocks.user / blocks.all — можно сохранить в lead или tracer

Пример 6 — Мягкий режим (strict=false)

Иногда нужен режим “не падать”, а вернуть сообщение/заглушку.

const Prompts = require("Common.AI.Prompts");

const prompt = Prompts.buildText(
  {
    system: ["$missing_alias"],
    user: ["Create an icon."]
  },
  {
    lead,
    agentName: "orion",
    promptTable: "gpt_prompts",
    strict: false
  }
);

// В strict=false при отсутствии промпта вернётся текст-предупреждение (а не throw)

Использование внутри других плагинов

Common.AI.Prompts специально сделан как “маленький кусок инфраструктуры”, чтобы:

Где он уже естественно применяется


Частые ошибки и как их избежать

1) Использовали $alias, но не указали agentName

Это ошибка по контракту, будет throw:

Решение: передай agentName.


2) Использовали $alias / @alias, но не указали promptTable

Это тоже ошибка:

Решение: передай promptTable (или оставь дефолт "gpt_prompts").


3) Хотели “просто текст”, но случайно начали строку с $

Если текст реально должен начинаться с $, то сейчас это будет воспринято как ref.

Практический паттерн: не начинай “сырой текст” с $/@. Если прям надо — лучше добавить пробел или явную экранировку на уровне твоего контента.


Итог

Common.AI.Prompts — это базовый инфраструктурный helper, который:

Он может использоваться:

Если в твоей архитектуре дальше появятся новые источники промптов (реестр провайдеров, JSON-конфиги, версии промптов, AB-тесты) — вот этот слой и будет правильным местом для расширения. И это как раз тот случай, где можно внезапно сделать себе ловушку, если начать “подмешивать” сюда бизнес-логику — держи его инфраструктурным, иначе потом будет боль.