Версия: 6.x
burger close
Использование AI платформы ReadyScript

ReadyScript в своих продуктах предоставляет возможность генерации различного текстового контента с помощью LLM-моделей. Однако любой разработчик может также использовать генерацию в своих модулях.

Генерация происходит через специальный сервер ReadyScript, где у каждого пользователя есть свой стартовый баланс. Тарификация происходит в запросах (а не в токенах, как это может быть на некоторых других сервисах). В случае исчерпания стартового баланса, необходимо приобрести пакет запросов, чтобы продолжить выполнять запросы.

При необходимости, ReadyScript предоставляет возможность приобрести дополнительный модуль в маркетплейсе, который позволяет напрямую подключаться к различным GPT-сервисам. В этом случае пополнять баланс за доступ к API нужно будет непосредственно на том сервисе, который вы желаете использовать и тарификация там уже будет своя (обычно в токенах).

Для разработчика не будет иметь значения, какой GPT-сервис используется для генерации, так как мы создали обособленный от конкретного сервиса абстрактный набор классов и методов для генерации.

Важно отметить, что в ReadyScript мы предполагаем, что вы будете использовать stream режим генерации, так как генерацию в этом режиме очень удобно визуализировать, передавая частички текста в клиентскую часть. Но даже и для скрытой серверной генерации stream режим никак не мешает, так как в целом генерирует текст за сравнительно то же время, что и в синхронном режиме.

Переходим к конкретике...

Вызов генерации текста в любом месте PHP кода

В этом разделе речь пойдет о генерации через LLM ответа для произвольного запроса. Начнем с примера кода, который мы опишем дальше. Давайте создадим файл test.php в корне сайта.

<?php
require('setup.inc.php');
try {
$service = \Ai\Model\ServiceApi::getDefaultService(); //Получаем профиль GPT-сервиса для автогенерации по умолчанию
$service_type = $service->getServiceTypeObject(); //Получаем объект модуля(типа) сервиса
//Здесь формируются параметры согласно документации OpenAI.
//https://platform.openai.com/docs/api-reference/chat/create
$params = [
'messages' => [
[
'role' => 'user',
'content' => 'Привет! Как дела?'
]
]
];
/**
* @var $stream \Ai\Model\ServiceType\ServiceChatResponse[] Здесь будут кусочки ответа в виде объектов ServiceChatResponse
*/
$stream = $service_type
->setStatisticParams([ //Устанавливаем параметры для статистики использования запросов
'type' => \Ai\Model\Orm\Statistic::TYPE_OTHER, //Отмечаем, что это не системный запрос, его тип "Другой"
'user_id' => \RS\Application\Auth::getCurrentUser()->id, //Передаем ID пользователя, который выполняет запрос
]) //Информация записывается в статистику после выполнения запроса
->createChatStream($params); //Формируем запрос на генерацию и получаем в ответ \Generator, в котором будут приходить кусочки ответа
foreach ($stream as $service_chat_response) {
//$service_chat_response->getDeltaText() //Здесь будет доступен кусок текста, если он нужен
if ($service_chat_response->isFinish()) {
echo $service_chat_response->getFullText(); //Здесь будет доступен полный текст ответа
// "Здравствуйте! Чем я могу вам помочь?"
//.. тут любые ваши действия с полным ответом.
}
}
} catch (Exception $e) {
//Если мы тут, значит что-то пошло не так.
echo "Ошибка: ".$e->getMessage();
}

Сперва нам необходимо получить объект профиля GPT-сервиса \Ai\Model\Orm\Service, через который мы будем выполнять запрос. Получить установленный в системе по умолчанию сервис можно с помощью метода \Ai\Model\ServiceApi::getDefaultService().

Получить конкретный профиль GPT-сервиса можно было бы с помощью следующего кода:

$service = \Ai\Model\ServiceApi::getServiceById(-1); //Вернет сервис ReadyScript
$service = \Ai\Model\ServiceApi::getServiceById(0); //Вернет сервис по умолчанию, выбранный в настройках модуля AI-ассистент
$service = \Ai\Model\ServiceApi::getServiceById($id); //Для $id > 0, вернет профиль с переданным $id

После получения профиля GPT-сервиса, нужно получить объект Типа сервиса(протокола) в помощью метода getServiceTypeObject() у объекта профиля.

Так как любой запрос должен корректно попадать в статистику, необходимо установить вспомогательные для статистики данные через метод setStatisticParams. По факту мы таким образом помогаем заполнить объект \Ai\Model\Orm\Statistic, поэтому возможные ключи можно посмотреть в описании полей этого ORM-объекта.

Далее происходит вызов основного метода, который запускает генерацию и возвращает Генератор (\Generator), т.е. итерируемый объект, состоящий из объектов \Ai\Model\ServiceType\ServiceChatResponse.

Рассмотрим подробнее интерфейс класса \Ai\Model\ServiceType\ServiceChatResponse.

<?php
namespace Ai\Model\ServiceType;
/**
* Объект содержит в себе сведения о результате выполнения одной итерации получения данных из потока (stream) GPT-сервиса.
* Так как, каждый GPT-сервис может возвращать поток в разном формате. Его необходимно унифицировать для ReadyScript.
* Все GPT сервисы в ReadyScript, должны возвращать поток объектов ServiceChatResponse.
*/
class ServiceChatResponse
{
/**
* Добавляет произвольные данные к данному сообщению
*
* @param string $key Произвольный ключ
* @param mixed $value Произвольное значение
* @return $this
*/
public function addExtraData(string $key, $value);
/**
* Устанавливает Произвольные данные
*
* @param array $data Произвольные данные
* @return $this
*/
public function setExtraData(array $data);
/**
* Возвращает установленные произвольные данные
*
* @return array
*/
public function getExtraData();
/**
* Возвращает текст, полученный от последней итерации
*
* @return string
*/
public function getDeltaText();
/**
* Устанавливает текст, полученный от последней итерации
*
* @param string $delta_text Часть текста
* @return $this
*/
public function setDeltaText(string $delta_text);
/**
* Возвращает полный текст, полученный с начала потока
*
* @return string
*/
public function getFullText();
/**
* Устанавливает полный текст, полученный с начала потока
*
* @param string $full_text Полный текст
* @return $this
*/
public function setFullText(string $full_text);
/**
* Возвращает true, если это последний элемент потока
*
* @return bool
*/
public function isFinish();
/**
* Устанавливает, является ли это последним элементом потока
*
* @param bool $is_finish
* @return $this
*/
public function setIsFinish(bool $is_finish);
}

Таким образом, вы можете получить результат генерации, проверив, что это последняя часть и вызвав метод getFullText().

if ($service_chat_response->isFinish()) {
echo $service_chat_response->getFullText(); //Здесь будет доступен полный текст ответа
//.. тут любые действия с полным ответом.
}

Добавление кнопки быстрой генерации для поля по промпту

ReadyScript предоставляет готовые инструменты для быстрого и простого добавления кнопок генерации к любым текстовым полям ORM-объектов на основе шаблонов запросов к ИИ (промптов). Предполагается, что такая кнопка будет генерировать текст для выбранного поля на основании данных из других полей.

Например, товару можно заполнить краткое описание, опираясь на названия товара. А полное описание можно заполнить на основе названия и краткого описания.

field-completion.jpg
Диалог массового заполнения полей через ИИ

В любом случае нажатие на кнопку генерации просто направляет в GPT-сервис запрос, сформированный на базе шаблона промпта, который задается в разделе Разное -> AI - ассистент -> Шаблоны запросов к ИИ. А уже внутри шаблона прописываются переменные, на основании которых нужно сгенерировать текст.

Технически, чтобы к какому-либо ORM-объекту в ReadyScript можно было заполнять через ИИ поля, нужно описать класс Трансформер, наследник от \Ai\Model\Transformer\AbstractTransformer, затем зарегистрировать данный класс через обработку события ai.getTransformers, а затем обработать событие orm.init.... для необходимого объекта, применив к нему метод трансформера addAiToFields($ormObject).

Рассмотрим пример класса трансформера для объекта Товар \Ai\Model\Object\ProductTransformer.

namespace Ai\Model\Transformer\Object;
/**
* Класс обеспечивает автоматическое заполнение полей товара с помощью ИИ
*/
class ProductTransformer extends AbstractTransformer
{
/**
* Возвращает идентификатор транформера
*
* @return string
*/
public static function getId()
{
return 'catalog-product';
}
/**
* Возвращает название транформера
*
* @return string
*/
public static function getTitle()
{
return t('Товары');
}
/**
* Возвращает список полей, которые могут автоматически заполняться с помощью ИИ
*
* @return array
*/
protected function initFields()
{
return [
new TextField($this, 'short_description', t('Короткое описание')),
new HtmlField($this, 'description', t('Полное описание')),
new StringField($this, 'meta_title', t('Мета-заголовок')),
new StringField($this, 'meta_keywords', t('Мета-ключевые слова')),
new StringField($this, 'meta_description', t('Мета-описание')),
];
}
/**
* Возвращает объект главного поля, которое может быть источником генерации всех остальных полей
*
* @return MainField
*/
public function getMainField()
{
return (new MainField($this, 'title', t('Короткое название')))
//Устанавливаем порядок последовательных и одновременных генераций, при нажатии на кнопку "Сгенерировать все"
->setGenerateFieldsOrder([
['short_description'], //Это будет генерироваться последовательно
['description'], //Это будет генерироваться последовательно
['meta_title', 'meta_keywords', 'meta_description'] //Это будет генерироваться одновременно
]);
}
/**
* Возвращает список объектов, содержащих идентификатор, название и значение переменной
*
* @return ReplaceVariable[]
*/
public function getVariables()
{
return $this->getVariablesFromOrmObject($this->source_object ?? new Product(), [
'sale_status', 'format', 'last_id', 'group_id', 'xml_id', 'import_hash', 'offers_json', 'video_link',
'market_sku', 'payment_subject', 'payment_method', 'tax_ids', 'marked_class', 'country_maker', 'tn_ved_codes',
'gtd', 'offer_caption'
]);
}
/**
* Заполняет объект товара данными из POST.
* Данный товар будет необходим для подстановки переменных в запрос к ИИ
*
* @param array $post_array
* @return void
*/
public function fillSourceObjectPromPost(array $post_array)
{
$product = new Product();
$product->getFromArray($post_array);
$this->setSourceData($product);
}
/**
* Устанавливает исходный объект, из которого будут добываться переменные и/или который нужно обновлять
*
* @param Product $object
* @return void
*/
public function setSourceData($object)
{
$this->source_object = $object;
}
/**
* Загружает по ID исходный объект, из которого будут добываться переменные и/или который нужно обновлять
*
* @param integer $id
* @return void
*/
public function setSourceDataById($id)
{
$this->source_object = new Product($id);
}
/**
* Возвращает название исходного объекта
*
* @return string
*/
public function getSourceObjectTitle()
{
return $this->source_object['title'];
}
/**
* Возвращает ссылку на просмотр/редактирование объекта в административной панели
*
* @return string
*/
public function getSourceObjectAdminUrl()
{
$router = Manager::obj();
return $router->getAdminUrl('edit', [
'id' => $this->source_object['id']
], 'catalog-ctrl');
}
/**
* Возвращает исходный объект, из которого будут добываться переменные и/или который нужно обновлять
*
* @return Product
*/
public function getSourceObject()
{
}
/**
* Возвращает объект класса для выборки объектов, заполняемых данным трансформером
*
* @return EntityList
*/
public function getDaoObject()
{
return new Api();
}
/**
* Возвращает список ID объектов с учетом установленных в административной панели фильтров.
*
* @param array $ids
* @param bool $is_select_all_pages Если true, то означает, что был выбран флаг "Выбрать элементы на всех страницах",
* и нужно выбрать элементы с учетом всех установленных фильтров
*
* @return array
*/
public function modifySelectAll($ids, $is_select_all_pages)
{
$api = $this->getDaoObject();
$request_object = $api->getSavedRequest(Ctrl::class . '_list');
if ($is_select_all_pages && $request_object !== null) {
return $api->getIdsByRequest($request_object);
}
return $ids;
}
}

Данный класс регистрировался бы следующим образом:

namespace Custom\Config;
class Handlers extends HandlerAbstract
{
function init()
{
$this->bind('ai.getTransformers');
$this->bind('orm.init.catalog-product');
}
/**
* Регистрирует в системе новый трансформер ORM-объектов
*/
public static function aiGetTransformers($list)
{
$list[] = ProductTransformer::class;
return $list;
}
/**
* Добавляет метки к полям, которые должны будут заполняться с помощью ИИ
*
* @param Product $product
* @return void
*/
public static function ormInitCatalogProduct(Product $product)
{
$transformer = new ProductTransformer();
$transformer->addAiToFields($product);
}
}

При разработке класса для вашего трансформера по аналогии нужно заменить объект Product из примера выше на ваш ORM-объект. Сегодня существует 3 типа поля, массив из которых должен возвращать метод initFields.

  • \Ai\Model\Transformer\Field\HtmlField - для генерации значений для поля \RS\Orm\Type\RichText
  • \Ai\Model\Transformer\Field\StringField - для генерации значений для поля \RS\Orm\Type\Varchar
  • \Ai\Model\Transformer\Field\TextField - для генерации значений для поля \RS\Orm\Type\Text

Технически, каждый тип поля описывает в системном промпте в каком формате вернуть результат.

Создание кнопок массового заполнения полей ORM-объектов по трансформеру

Если у вас уже есть класс трансформера ваших объектов, то очень просто можно реализовать кнопку "Заполнить через ИИ" для выбранного списка объектов массово.

multi-completion.jpg
Генерация значения для поля через ИИ

Покажем на примере страницы Товары -> Каталог товаров.

Для добавления кнопки необходимо обработать событие, которое вызывается сразу после формирования Помощника формирования скелета страницы административной панели. В случае со страницей из контроллера \Catalog\Controller\Admin\Ctrl, метода actionIndex, название события будет выглядеть так: controller.exec.catalog-admin-ctrl.index, где:

  • controller.exec - общий префикс
  • catalog-admin-ctrl - короткий идентификатор контроллера
  • index - имя действия контроллера
namespace Custom\Config;
class Handlers extends HandlerAbstract
{
function init()
{
//... тут предыдущие подписки
$this->bind('controller.exec.catalog-admin-ctrl.index');
}
/**
* Расширяет страницу со списком товаров админ.панели.
*
* @param CrudCollection $helper
* @return void
*/
public static function controllerExecCatalogAdminCtrlIndex(CrudCollection $helper)
{
$router = Manager::obj(); //Получаем объект менеджера машрутов (роутов)
//Формируем адрес, мастера мульти-заполнения данных
$url = $router->getAdminUrl('wizardWelcome', //Действие контроллера, обязательно такое
[
'transformer_id' => 'catalog-product', //Тут передаем ID трансформера из метода getId()
'skip' => 'auto' //Этот параметр нужен, чтобы корректно работал флажок "Пропустить первый шаг в мастере"
],
'ai-taskctrl' //Короткий идентификатор контроллера
);
$helper->getBottomToolbar()->addItem(new Button(
$url,
t('Заполнить через ИИ'), //Текст кнопки
[
'attr' => [
'class' => 'crud-multiaction' //Обязательный класс для кнопки
]
]),
'ai-fill', //Произвольный идентификатор кнопки
1 //Позиция вставки кнопки
);
}
//...
}