Я сделал небольшой клиент для Wolfram Language, который умеет вызывать OpenAI API и другие API, которые на него похожи. Сам активно пользуюсь им и хочу рассказать о том, как легко создать ассистента на основе OpenAI API и добавить в него свои собственные плагины.
Зачем я это делаю?
Во-первых, я не так часто вижу на Хабре утилитарные статьи, где рассказывается о том, как использовать нейросеть с примерами кода. И особенно мало таких статей, где речь идет про конкретные плагины. Чаще всего мы видим здесь рекламу, ссылочки на телеграм и восхваление очередных достижений нейросете-строения.
Во-вторых, у Wolfram Language есть фантастически крутой блокнотный пользовательский интерфейс. Речь конечно же про Mathematica и про наш родной отечественный WLJS Notebook. Формат интерактивного блокнота как нельзя лучше подходит для работы с чат-ботами, LLM и нейросетями.
В-третьих, в пакете AILink есть киллер-фича WL из коробки, которая доступна всем пользователям Wolfram Language - это Cloud Evaluate. С его помощью вам не потребуется VPN для обхода блокировки по региону со стороны OpenAI. То есть AILink в Wolfram Language работает в РФ без использования прокси!
В-четвертых, я как фанат Wolfram Language просто в очередной раз хочу про него рассказать.
Будем считать, что среда выполнения когда уже установлена, но если нет, то всегда можно получить бесплатное ядро Wolfram Engine с регистрацией, но без смс. Там очень простые инструкции, нужно всего-то зарегистрировать учетную запись и нажать кнопку "получить лицензию".
Установить ядро на Windows можно так:
winget install WolframEngine
На MacOS так:
brew cask install wolfram-engine
На Linux можно только скачать файл-установщик по ссылке, но зато можно в одну строчку получить образ для docker:
docker pull wolframresearch/wolframengine
После выполнения шагов выше и получения лицензии создадим где-нибудь директорию LLMBot, а в ней новый файл llmbot.nb:
Затем откроем файл при помощи Mathematica и приступим к написанию кода.
Первое, что мы добавим в наш скрипт - это установка и импорт зависимостей. Нам понадобится несколько пакетов из Paclet Repository:
(*function like apt update*)
Map[PacletSiteUpdate] @ PacletSites[];
(*install paclets*)
PacletInstall["KirillBelov/Internal"];
PacletInstall["KirillBelov/Objects"];
PacletInstall["KirillBelov/AILink"];
(*paclets importing*)
Get["KirillBelov`AILink`"];
После чего пользователю станут доступны функции из пакета AILink:
AIModels[] - список моделей OpenAI
AIImageGenerate["prompt"] - создает картинку в DALL-E
AISpeech["text"] - превращает текст в аудио в TTS
AITranscription[audio] - преобразует звук в текст в Whisper
AIChatObject[<>] - представление объекта чата в языке
AIChatComplete[chat, message] - дополнение чата в GPT
AIChatCompleteAync[chat, message, callback] - асинхронное дополнение
Для того чтобы ими всеми пользоваться необходимо задать ключ для доступа к API. Сделать это можно выполнив команду:
(*set api key*)
SystemCredential["OPENAI_API_KEY"] = "your openai api key";
Готово! Проверить что все работает можно добавим в скрипт вот такую строчку:
(*print models*)
AIModels[]
Если все примерно как на скриншоте - то можно продолжать. Следующим шагом проверим, что работают другие функции:
Можно передать в качестве аргументов скрипта текстовую строку и OpenAI модель TTS создаст для него аудио файл:
hiAudio = AISpeech["Привет!"]
Export["hi.mp3", hiAudio]
В качестве опциональных аргументов функция AISpeech[] принимает имя голоса. с помощью которого нужно озвучить текст. Доступные голоса можно посмотреть в документации OpenAI. Вот как будут отличаться разные голоса:
hiAlloy = AISpeech["Привет Хабр!", "Voice" -> "alloy"]
hiNova = AISpeech["Привет Хабр!", "Voice" -> "nova"]
AudioPlot[{hiAlloy, hiNova}]
Все тоже самое можно проделать и в обратную сторону:
AITranscription[hiAlloy]
AITranscription[hiNova]
Последней доп-функцией в обзоре будет генерация изображений. Все очень просто:
AIImageGenerate["wolf and ram in the village"]
Теперь перейдем к главной части статьи! Чат бот! Чтобы создать пустой объект чата используем функцию:
chat = AIChatObject[]
Отправим первое сообщение в OpenAI. После выполнения запроса оно сохранится в чат и чат же вернется как результат выполнения функции:
AIChatComplete[chat, "Привет!"];
chat["Messages"]
Ну и дальше вызывая функцию AIChatComplete можно продолжать общаться с LLM. Но это слишком просто!
У функции AIChatComplete есть несколько важных опций:
"Model" - позволяет указать конкретную модель. По умолчанию "gpt-4o"
"Temperature" - позволяет указать температуру - не все модели ее поддерживают
"Tools" - позволяет использовать функции-плагины, опять же не все модели их поддерживают
Можно передать как в функцию дополнения так и в качестве параметров конструктора чата:
chat = AIChatObject["Tools" -> {tool1, tool2}];
AIChatComplete[chat, "Model" -> "gpt-4o"];
С выбором модели все еще более менее понятно.
"gpt-3.5-turbo" - быстрая и не самая умная
"gpt-4o" - умнее и медленнее
"o1-preview" - умеет рассуждать и тулы не поддерживает
Но что передавать в качестве tool1, tool2, ...? Это должны быть функции! Функции, которые создаются с некоторыми ограничениями, но очень простыми:
Они возвращают в качестве ответа строку
Они имеют описание в usage
Типы аргументов явно указаны и могут быть String, Real или Integer
Создадим очень простую такую функцию:
time::usage = "time[] returns current time in string format.";
time[] := DateString[]
Создадим еще функцию с параметрами. Например получение текущей температуры в указанном населенном пункте. Для того чтобы узнать температуру я буду использовать WeatherAPI.
В процессе написания статьи я зарегистрировался там и посмотрел как выполняется запрос в документации. На Wolfram Language это будет вот так:
SystemCredential["WEATHERAPI_KEY"] = "<api key>";
wheather::usage = "wheather[lat, lon] returns info about current wheathe for specific geo coordinates. Result is JSON object with coordinates, text description, temperature, wind speed and etc.";
wheather[lat_Real, lon_Real] :=
Module[{
endpoint = "http://api.weatherapi.com/v1/current.json",
apiKey = SystemCredential["WEATHERAPI_KEY"],
request, response
},
request = URLBuild[endpoint, {
"q" -> StringTemplate["``,``"][lat, lon],
"key" -> apiKey
}];
response = URLRead[request];
response["Body"]
]
Плюс еще одна функция мне потребуется для определения координат населенного пункта по названию. Ее я сделаю с использованием Wolfram Alpha:
geoPosition::usage = "geoPosition[city] geo position of the specific city. city parameter - name of the city only in English.";
geoPosition[city_String] :=
StringTemplate["lat = ``, lon = ``"] @@
First @ WolframAlpha[city <> " GeoPosition", "WolframResult"]
Теперь я просто добавлю эти функции в список функций LLM и посмотрю что получится:
Чтобы информация поместилась в окне блокнота я многое удалил, но если коротко. то произошло следующее:
Пользователь спросил у ГПТ текущую погоду в Саратове
ГПТ вернул сообщение, где попросил вызвать функцию geoPosition["Saratov"]
Функция отправила в ГПТ координаты
ГПТ снова вернул вызов функции и параметры - теперь weather[lat, lon]
Функция отправила в ГПТ информацию о погоде
ГПТ вернул пользователю отформатированный читаемый текст.
Да, пока я пишу эту статью в Саратове и правда около 6-7 градусов и моросит дождь - вот он на фото ниже
Чат-бот знает теперь три функции, с помощью которых он может получить доступ к внешнему миру. Но они довольно узконаправленные: время. координаты и погода. Но что если дать боту более общий инструмент для общения с миром? Например, поиск в интернете? На самом деле сделать это довольно легко! Пусть с первого раза реализация будет не лучшей и не оптимальной, но я потратил на это буквально несколько минут. Сначала я вспомнил, что DuckDuckGo невозбранно дает использовать свой поиск, а потом посмотрел какие там есть варианты и нашел что там есть поиск lite. И вот как выглядит инструмент поиска в сети:
duckDuckGoSearch::usage = "duckDuckGoSearch[query] call DuckDuckGo search engine. ..";
duckDuckGoSearch[query_String] :=
Module[{url = "https://lite.duckduckgo.com/lite/", request,
response, responseBody},
request = HTTPRequest[url,
<|
Method -> "POST",
"Body" -> {
"q" -> query
},
"ContentType" -> "application/x-www-form-urlencoded"
|>
];
response = URLRead[request];
responseBody = response["Body"];
ImportString[ExportString[responseBody, "String"], "HTML"] <>
"\nhttps://duckduckgo.com?" <> URLQueryEncode[{"q" -> query}]
];
Ну и опять попробуем что получится:
AIChatComplete[
"Try to search in the web what the last Donald Trump speech",
"Tools" -> {time, geoPosition, wheather, duckDuckGoSearch},
"Model" -> "gpt-4o"
]["Messages"][[-1, "content"]]
Хм... там есть ссылки. А что есть просто взять и научить читать LLM страницы по ссылкам! Это очень просто и лежит на поверхности. Просто добавим urlRead!
urlRead::usage = "urlRead[url] read text from html page with address == url. Use this function when you need to get data from the specific address or site in the internet. result is a imple text.";
urlRead[url_String] := ImportString[URLRead[url]["Body"], "HTML"];
А теперь попросим LLM прочитать подробнее что написано в статье по первой ссылке!
Да, это вполне реальная статья: https://time.com/7026339/donald-trump-speech-app-comment-public-reaction-kamala-harris-campaign/, а не сгенерированный URL, который просто похож на настоящий.
В принципе, продолжать добавлять плагины можно еще очень долго и можно реализовать самые смелые и интересные идеи. Кроме показанных примеров я так же делал для себя:
Поиск по локальным файлам в директории
Составление кратного описание большого файла чтобы иметь "каталог"
Сделать свой CoPilot
Дать доступ к ядру и возможность создавать и редактировать ячейки блокнота
Выполнять код
Использовать другие полезные сервисы - курсы валют, интернет магазины, новости и др.
Добавить в групповой чат и дать доступ ко всем сообщениям и поиску по ним
и т.д.
То, что сделано на коленке в рамках статьи - я очень часто вижу в виде товара, но нечасто в виде реализации. Самые разные инфлюэнсеры, криптовалютные эксперты и тарологи рассказывают о новой невероятной нейросети с самыми крутыми возможностями - главное купить подписку именно на их телеграм. Может быть я не там смотрю и нужно идти не в магазин, а на завод. Но в любом случае я хотел поделиться своими лучшими практиками по программному использованию OpenAI API и созданию плагинов для него. Всем спасибо за внимание!