Создание AI-ассистента с OpenAI Assistants API на Go
Всем привет!
Меня зовут Дмитрий, я занимаюсь развитием отношений с клиентами и партнерами в IT-компании StecPoint.
Недавно возникла необходимость создать AI-ассистента, обученного искать и выдавать информацию из базы знаний, предоставленных заказчиком.
В этой статье мы рассмотрим процесс создания MVP такого ассистента. Мы загрузим в него файлы, зададим инструкции, привяжем все к Telegram-боту и будем обрабатывать запросы пользователей. Для создания ассистента будем использовать функциональность OpenAI Assistants API. К сожалению, сервисы OpenAI недоступны из РФ без VPN, но мы решим эту проблему с помощью сторонних прокси-сервисов.
Все это напишем на Go. Код, готовый к запуску и компиляции, доступен в конце статьи.
Дисклеймер
Здесь все будет просто. Весь код мы напишем в main
. Мы не будем как-то готовить инфраструктуру, использовать базу данных, заморачиваться с логгированием и тестированием. Единственное - вынесем некоторые параметры в config
.
Наша задача – научиться создавать ассистентов по API и получать от них ответы на запросы пользователей.
Тем не менее, код будет полностью рабочим, и если сервис запустить на компе и не выключать – все будет чудесно работать :-)
Как получить доступ из РФ
Для доступа к ChatGPT из веб-браузера мы используем различные VPN. Для доступа к OpenAI Assistants API нам, во-первых, потребуется каким-то образом пополнить баланс аккаунта для генерации ответов, во-вторых, прогонять через VPN трафик нашего сервиса.
Оба этих пункта усложняют весь процесс, поэтому мы воспользуемся прокси-сервисом. На рынке есть сервисы, которые берут на себя все эти заботы пользователей об оплате и VPN. Вы регистрируетесь в их личном кабинете, пополняете баланс российской картой, интегрируетесь так, словно пользуетесь конечным API, только URL используете данного сервиса.
Для целей нашего проекта я воспользуюсь ProxyApi.ru. Особых предпочтений нет, но это первый же сервис, который выдал мне поиск.
Последовательность шагов следующая:
Регистрируемся: https://proxyapi.ru
В личном кабинете генерируем себе ключ: https://console.proxyapi.ru/keys. Ключ надо где-нибудь сохранить в надежном месте, так как сервис его отображает только один раз в момент генерации.
Оформляем подписку Pro для доступа к функциональности Assistant API: https://console.proxyapi.ru/pro. Для доступа к обычной функциональности чатов, если б мы создавали такой сервис, подписка не нужна, но для работы с Assistants API, векторным хранилищем и файлами она обязательна.
Пополняем баланс для будущей генерации ответов: https://console.proxyapi.ru/billing
Регистрация бесплатна, подписка Pro стоит 1500 руб./мес., а баланс я пополнил на 1000 руб. Для теста должно хватить с головой.
Далее мы будем писать сервис по документации OpenAI, но стучаться будем не напрямую в https://api.openai.com/v1/, а по https://api.proxyapi.ru/openai/v1/. В остальном интеграция будет идентична.
При таком подходе не получится получить доступ к ассистентам, которые вы могли уже ранее создать в личном кабинете OpenAI - сервис ProxyAPI их просто не увидит и доступа у него не будет.
Регистрируем бота в Telegram
Так как наши пользователи будут взаимодействовать с нашим ассистентом через Telegram-бота, его, судя по всему, нам тоже надо создать.
Для этого идем к «отцу ботов»: https://t.me/BotFather.
Отправляем сообщение или выбираем в меню /newbot
и заполняем необходимые поля. По итогу нас поздравят с созданием бота и напишут token для доступа. Он-то нам и понадобится в будущем.
Немного про OpenAI Assistants API
OpenAI Assistants API предоставляет разработчикам возможность создавать кастомных AI-ассистентов на базе моделей GPT. API позволяет:
Настраивать поведение ассистента через инструкции и параметры.
Добавлять специальные инструменты - поиск по файлам, веб-поиск.
Интегрировать ассистента с различными платформами и сервисами.
Используя этот API, вы можете создавать ассистентов, которые не только понимают естественный язык, но и выполняют конкретные задачи, используя предоставленные им ресурсы.
Работа с OpenAI Assistants API начинается с создания и настройки ассистента, который будет взаимодействовать с пользователями и использовать предоставленные ему ресурсы для поиска в них информации.
Создаем ассистента
Для успешного создания ассистента нам потребуется выполнить следующую последовательность шагов:
Создать ассистента. Здесь мы отправляем запрос на создание ассистента с необходимыми параметрами и получаем
assistant_id
.Создать VectorStore и загрузить в него файлы. Отправляем запрос на создание
Vector Store
и получаемvector_store_id
. Загружаем каждый файл из директории и получаем для нихfile_id
. Нам нужно привязать каждыйfile_id
кvector_store_id
.Обновить ассистента. Нам нужно прокинуть
vector_store_id
в конфигурацию ассистента.
Теперь подробнее.
Шаг 1. Создание ассистента
Ассистент в OpenAI — это сущность, которая объединяет модель (например, GPT-4) и инструменты (например, поиск по файлам) и настраивается с помощью инструкций и параметров, определяющих его поведение. Чтобы создать ассистента, необходимо отправить запрос на создание с указанием следующих параметров:
Name — имя нашего ассистента.
Instructions — текстовые инструкции, определяющие, как ассистент должен себя вести и на какие темы отвечать. Здесь мы указываем что-то наподобие «ты - налоговый консультант, вежливый, отвечаешь на русском языке».
Model — используемая модель. Мы будем использовать
gpt-4-turbo
.Tools — список инструментов, которые ассистент будет использовать. В нашем случае это инструмент
file_search
, который позволяет искать информацию в загруженных файлах.
Наша структура данных для ассистента:
type AssistantCreateRequest struct {
Name string `json:"name"`
Instructions string `json:"instructions"`
Model string `json:"model"`
Tools []Tool `json:"tools"`
}
type Tool struct {
Type string `json:"type"`
}
Теперь создадим ассистента. Параметры для ассистента мы будем читать из config, метод для которого напишем позже. Также OpenAI нам говорит, что для доступа к функциональности ассистентов мы должны добавлять в header`ы параметр OpenAI-Beta:assistants=v2
.
Наш метод создания ассистента:
func createAssistant() (string, error) {
tools := []Tool{{Type: "file_search"}}
requestBody := AssistantCreateRequest{
Name: config.Name,
Instructions: config.Instructions,
Model: config.Model,
Tools: tools,
}
reqBody, err := json.Marshal(requestBody)
if err != nil {
return "", err
}
req, err := http.NewRequest("POST", config.ApiURL+"assistants", bytes.NewBuffer(reqBody))
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+config.APIKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("OpenAI-Beta", "assistants=v2")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var assistantResponse AssistantCreateResponse
if err := json.Unmarshal(body, &assistantResponse); err != nil {
return "", err
}
return assistantResponse.ID, nil
}
В main
добавим следующий код:
func main() {
// Создаем ассистента
assistantID, err := createAssistant()
if err != nil {
os.Exit(1)
}
}
Результатом успешного выполнения функции является получение ID ассистента (assistant_id
), который будет использоваться в дальнейшем.
Шаг 2. Создание Vector Store и загрузка файлов
Теперь нам нужно создать Vector Store — хранилище, в которое мы загрузим наши файлы для поиска информации ассистентом. В нашем случае мы создаём Vector Store и сразу же загружаем в него файлы из указанной директории.
Функция createVectorStoreAndUploadFiles()
выполняет следующие действия:
Создаёт Vector Store и получает
vector_store_id
.Сканирует директорию, указанную в конфигурации, и загружает каждый файл.
Регистрирует каждый загруженный файл в созданном Vector Store.
Создание Vector Store
Запрос на создание Vector Store не требует никаких параметров. Мы получаем vector_store_id
, который будем использовать для добавления файлов и обновления ассистента:
func createVectorStoreAndUploadFiles() (string, error) {
// Создаем Vector Store
req, err := http.NewRequest("POST", config.ApiURL+"vector_stores", nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+config.APIKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("OpenAI-Beta", "assistants=v2")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var vectorStoreResponse VectorStoreCreateResponse
if err := json.Unmarshal(body, &vectorStoreResponse); err != nil {
return "", err
}
vectorStoreID := vectorStoreResponse.ID
// Загрузим файлы из директории, указанной в конфигурации
files, err := os.ReadDir(config.FilesPath)
if err != nil {
return "", err
}
for _, file := range files {
if !file.IsDir() {
filePath := filepath.Join(config.FilesPath, file.Name())
// Загружаем файл и получаем его file_id
fileID, err := uploadFile(filePath)
if err != nil {
continue
}
// Регистрируем файл в Vector Store
if err := registerFileInVectorStore(vectorStoreID, fileID); err != nil {
continue
}
}
}
return vectorStoreID, nil
}
Загрузка файлов
Функция uploadFile()
загружает файл в API и возвращает его file_id
. Мы будем забирать файлы из папки, указанной в конфиге. Ассистент поддерживает работу со многими форматами, включая docx
, pdf
, html
, xlsx
.
func uploadFile(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
var b bytes.Buffer
w := multipart.NewWriter(&b)
// Добавляем файл в запрос
fw, err := w.CreateFormFile("file", filepath.Base(filePath))
if err != nil {
return "", err
}
_, err = io.Copy(fw, file)
if err != nil {
return "", err
}
// Добавляем параметр 'purpose' в запрос
err = w.WriteField("purpose", "assistants")
if err != nil {
return "", err
}
w.Close()
req, err := http.NewRequest("POST", config.ApiURL+"files", &b)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+config.APIKey)
req.Header.Set("Content-Type", w.FormDataContentType())
req.Header.Set("OpenAI-Beta", "assistants=v2")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("Ошибка загрузки файла: %s", string(body))
}
// Получаем file_id
var response map[string]interface{}
if err := json.Unmarshal(body, &response); err != nil {
return "", err
}
fileID, ok := response["id"].(string)
if !ok {
return "", fmt.Errorf("Не удалось получить file_id для файла %s", filePath)
}
return fileID, nil
}
Регистрация файлов в Vector Store
После загрузки файла мы получаем его file_id
, который необходимо зарегистрировать в нашем Vector Store. Это связывает файлы с Vector Store, что позволит ассистенту использовать их для поиска информации.
func registerFileInVectorStore(vectorStoreID, fileID string) error {
requestBody := map[string]string{
"file_id": fileID,
}
reqBody, err := json.Marshal(requestBody)
if err != nil {
return fmt.Errorf("Ошибка формирования тела запроса для регистрации файла: %v", err)
}
req, err := http.NewRequest("POST", config.ApiURL+"vector_stores/"+vectorStoreID+"/files", bytes.NewBuffer(reqBody))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+config.APIKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("OpenAI-Beta", "assistants=v2")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Ошибка регистрации файла: %s", string(body))
}
return nil
}
В main
добавим следующий код:
func main() {
vectorStoreID, err := createVectorStoreAndUploadFiles()
if err != nil {
os.Exit(1)
}
}
Шаг 3. Обновление ассистента с новым Vector Store
Последним шагом является обновление ассистента, чтобы он знал, что теперь у него есть доступ к Vector Store
и помещенным в него файлам. Мы указываем vector_store_id
для инструмента file_search
в конфигурации ассистента.
func updateAssistantWithVectorStore(assistantID, vectorStoreID string) error {
updateBody := map[string]interface{}{
"tool_resources": map[string]interface{}{
"file_search": map[string]interface{}{
"vector_store_ids": []string{vectorStoreID},
},
},
}
reqBody, err := json.Marshal(updateBody)
if err != nil {
return err
}
req, err := http.NewRequest("POST", config.ApiURL+"assistants/"+assistantID, bytes.NewBuffer(reqBody))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+config.APIKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("OpenAI-Beta", "assistants=v2")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Ошибка обновления ассистента: %v", resp.Status)
}
return nil
}
В main
добавим следующий код:
func main() {
// Привязываем Vector Store к ассистенту
if err := updateAssistantWithVectorStore(assistantID, vectorStoreID); err != nil {
os.Exit(1)
}
}
Мы создали готового к работе ассистента, который может искать и выдавать информацию из загруженных файлов, используя инструменты и модель OpenAI. Основным артефактом для нас здесь является assistantID, который мы будем в дальнейшем использовать.
Работа с сообщениями
OpenAI Assistants API использует несколько ключевых сущностей для обработки и управления сообщениями, отправляемыми пользователями: Threads
, Messages
и Runs
.
Threads
(Потоки). Поток представляет собой контекст или, другими словами, диалоговую сессию. Он хранит историю сообщений, отправленных пользователем и ассистентом. Поток создается для каждого нового запроса или взаимодействия.Messages
(Сообщения). Сообщение — это отдельный блок информации, отправленный пользователем или ассистентом. Каждое сообщение имеет роль (user
,assistant
,system
), которая указывает, кто отправил сообщение.Run
(Запуск). Запуск — это процесс выполнения одного или нескольких сообщений в рамках определенного потока. Запуск инициализируется, когда нужно выполнить действия или получить ответ от ассистента, опираясь на текущий контекст. Каждому запуску присваивается уникальный идентификатор, и он обрабатывается сервером в реальном времени.
Эти три сущности работают вместе для обеспечения корректной обработки запросов и предоставления ответов пользователям.
Создадим структуры данных:
type Message struct {
Role string `json:"role"` // Роль сообщения (например, "user", "assistant", "system").
Content string `json:"content"` // Содержимое сообщения.
}
type Thread struct {
Messages []Message `json:"messages"` // Список сообщений в потоке.
}
type RunRequest struct {
AssistantID string `json:"assistant_id"` // Идентификатор ассистента.
Thread Thread `json:"thread"` // Поток, в котором выполняется запрос.
ToolResources map[string]interface{} `json:"tool_resources"` // Дополнительные ресурсы (например, vector_store_ids).
Temperature float64 `json:"temperature"` // Параметр генерации (опционально).
TopP float64 `json:"top_p"` // Параметр генерации (опционально).
Stream bool `json:"stream"` // Указывает, активировать ли потоковую передачу данных.
}
Основной алгоритм взаимодействия с ассистентом выглядит следующим образом.
Создать поток (
Thread
) и отправить сообщение (Message
). Здесь мы инициализируем новый поток, передаем в него сообщение от пользователя.Инициализировать запуск (
Run
). Запуск включает создание нового сеанса общения с ассистентом и получение ответа.Обработать и вывести результат. Ассистент возвращает ответ после обработки запроса.
Теперь подробнее.
Шаг 1. Создание Thread и отправка Message
Для взаимодействия с ассистентом нужно создать поток и передать в него сообщения. Поток создается автоматически при отправке первого сообщения. В запросе указывается идентификатор ассистента (assistant_id
), контекст текущего потока (Thread
) и параметры, влияющие на генерацию ответа (например, temperature
и top_p
). В нашем примере эти параметры будут равны 1.0.
func createAndRunAssistantWithStreaming(assistantID, query, vectorStoreID string) (string, error) {
requestBody := map[string]interface{}{
"assistant_id": assistantID,
"thread": map[string]interface{}{
"messages": []map[string]interface{}{
{"role": "user", "content": query},
},
},
"tool_resources": map[string]interface{}{
"file_search": map[string]interface{}{
"vector_store_ids": []string{vectorStoreID},
},
},
"temperature": 1.0,
"top_p": 1.0,
"stream": true, // Активируем поток
}
reqBody, err := json.Marshal(requestBody)
if err != nil {
return "", fmt.Errorf("Ошибка создания тела запроса: %v", err)
}
req, err := http.NewRequest("POST", config.ApiURL+"threads/runs", bytes.NewBuffer(reqBody))
if err != nil {
return "", fmt.Errorf("Ошибка создания HTTP-запроса: %v", err)
}
req.Header.Set("Authorization", "Bearer "+config.APIKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("OpenAI-Beta", "assistants=v2")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("Ошибка выполнения HTTP-запроса: %v", err)
}
return listenToSSEStream(resp)
}
В приведенном выше коде создается поток с сообщением от пользователя (user
), инициализируется запуск (Run
), и отправляется запрос к API. В запросе указываются:
AssistantID — идентификатор ассистента.
Thread — контекст текущего потока.
ToolResources — используемые ресурсы (наш
vector_store_ids
).Stream — определяет, использовать ли потоковую передачу данных или нет.
Шаг 2. Получение и обработка сообщений
После отправки запроса ассистент возвращает результат в виде ответа, который мы можем обрабатывать и отображать пользователю. Если активирован режим Stream
, то ответы поступают по частям, и для их обработки используется механизм Server-Sent Events (SSE).
Server-Sent Events (SSE) — это механизм, который позволяет серверу отправлять обновления клиенту в реальном времени. Он используется для потоковой передачи данных, когда необходимо получать ответ по мере его генерации. Для пользователя это выглядит так, словно сервис печатает сообщение. SSE обеспечивает одностороннее соединение от сервера к клиенту, что позволяет минимизировать задержки и загруженность сети.
func listenToSSEStream(resp *http.Response) (string, error) {
defer resp.Body.Close()
reader := bufio.NewReader(resp.Body)
var finalMessage string
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
break
}
return "", fmt.Errorf("Ошибка чтения события: %v", err)
}
line = strings.TrimSpace(line)
if len(line) == 0 {
continue
}
// Проверяем, начинается ли строка с 'data: '
if strings.HasPrefix(line, "data: ") {
eventData := line[6:] // Убираем "data: "
if eventData == "[DONE]" {
break
}
var event map[string]interface{}
if err := json.Unmarshal([]byte(eventData), &event); err != nil {
continue
}
// Если это событие завершения сообщения (thread.message.delta)
if obj, ok := event["object"].(string); ok && obj == "thread.message.delta" {
if delta, ok := event["delta"].(map[string]interface{}); ok {
if content, ok := delta["content"].([]interface{}); ok {
for _, part := range content {
if textPart, ok := part.(map[string]interface{}); ok {
if text, ok := textPart["text"].(map[string]interface{}); ok {
if value, ok := text["value"].(string); ok {
finalMessage += value
}
}
}
}
}
}
}
// Проверка на завершение сообщения
if obj, ok := event["object"].(string); ok && obj == "thread.message.completed" {
break
}
}
}
return finalMessage, nil
}
Функция listenToSSEStream
:
Открывает поток для чтения данных из ответа сервера.
Обрабатывает каждую строку, проверяя, является ли она частью события с префиксом
data:
.Извлекает текстовые фрагменты из событий
thread.message.delta
и добавляет их в конечное сообщение.Завершает чтение при получении события
thread.message.completed
или метки[DONE]
.Собирает полный текст ответа, поступающего по частям, и возвращает его в виде строки.
Чтобы лучше понять, как работает SSE и в каком формате приходит ответ, попробуйте сделать POST-запрос через Postman к методу /v1/threads/runs
, указав ваш токен авторизации, добавив в header параметр OpenAI-Beta:assistants=v2
и указав следующее тело запроса:
{
"assistant_id": ["ID вашего ассистента"],
"thread": {
"messages": [
{
"role": "user",
"content": "Привет"
}
]
},
"tool_resources": {
"file_search": {
"vector_store_ids": ["ID вашего Vector Store"]
}
},
"temperature": 1.0,
"top_p": 1.0,
"stream": true
}
Итак, мы создали ассистента и научились отправлять в него пользовательские сообщения и получать ответы. Осталось совсем немного до нашей итоговой реализации.
Интеграция ассистента с Telegram и взаимодействие с пользователями
Теперь создадим Telegram-бота для взаимодействия с реальными пользователями. Telegram предоставляет удобный API для создания ботов, которые могут принимать и обрабатывать сообщения, отправляемые пользователями.
Основные шаги:
Инициализация Telegram-бота и настройка соединения.
Получение и обработка входящих сообщений.
Передача сообщений ассистенту и получение ответа.
Отправка ответа обратно пользователю.
Шаг 1. Инициализация Telegram-бота
Для начала нужно создать бота в Telegram, используя @BotFather. Он предоставит уникальный токен, который используется для аутентификации запросов к Telegram API.
Инициализация бота выглядит следующим образом:
func main() {
// Инициализируем Telegram бота
bot, err := tgbotapi.NewBotAPI(config.TelegramBotToken)
if err != nil {
os.Exit(1)
}
bot.Debug = false // Отключаем отладку самого бота
}
Шаг 2. Обработка входящих сообщений
Для обработки входящих сообщений используется канал обновлений Telegram. Бот подписывается на все новые сообщения, и каждое сообщение обрабатывается в отдельной горутине.
func handleTelegramUpdates(bot *tgbotapi.BotAPI, assistantID, vectorStoreID string) {
// Настройка обновлений
u := tgbotapi.NewUpdate(0)
u.Timeout = 60 // Задаем таймаут ожидания обновлений
updates := bot.GetUpdatesChan(u) // Получаем канал обновлений
for update := range updates {
if update.Message != nil && update.Message.Text != "" {
// Локальные копии переменных
localUpdate := update
query := localUpdate.Message.Text
// Обрабатываем каждый запрос в отдельной горутине
go func(update tgbotapi.Update, query string) {
response, err := createAndRunAssistantWithStreaming(assistantID, query, vectorStoreID)
if err != nil {
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Ошибка обработки запроса.")
bot.Send(msg)
return
}
if response == "" {
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Ассистент не смог предоставить ответ.")
bot.Send(msg)
return
}
// Отправляем ответ пользователю
msg := tgbotapi.NewMessage(update.Message.Chat.ID, response)
bot.Send(msg)
}(localUpdate, query) // Передаем параметры в горутину
}
}
}
Здесь у нас:
Создается канал обновлений
updates
с таймаутом в 60 секунд.В цикле
for update := range updates
обрабатываются все новые сообщения.Для каждого сообщения запускается отдельная горутина с анонимной функцией, чтобы асинхронно обрабатывать запросы.
Сообщение отправляется ассистенту с помощью функции
createAndRunAssistantWithStreaming
.Полученный ответ отправляется обратно пользователю в виде нового сообщения.
В main
добавим следующий код:
func main() {
// Запускаем обработку запросов от пользователей
handleTelegramUpdates(bot, assistantID, vectorStoreID)
}
Шаг 3. Передача сообщений ассистенту
Сообщение, полученное от пользователя, передается ассистенту для выполнения в createAndRunAssistantWithStreaming.
Шаг 4. Отправка ответа пользователю
Получив ответ от ассистента, мы формируем новое сообщение и отправляем его пользователю:
msg := tgbotapi.NewMessage(update.Message.Chat.ID, response)
bot.Send(msg)
Сообщение отправляется с помощью метода bot.Send()
, который принимает объект tgbotapi.MessageConfig
, содержащий идентификатор чата и текст сообщения.
На этом работа с Telegram у нас заканчивается. Остались мелочи.
Работа с конфигурационным файлом config.yaml
Теперь нам осталось считать все параметры из config-файла. В данном проекте используется файл config.yaml
, который содержит все необходимые данные для работы ассистента и интеграции с Telegram. Конфигурационный файл предоставляет удобный способ управления параметрами без необходимости вносить изменения в код.
Содержимое config.yaml
В файле config.yaml
хранятся следующие параметры:
api_url: https://api.proxyapi.ru/openai/v1/
api_key: [YOUR-API-KEY]
telegram_bot_token: [YOUR-TELEGRAM-BOT-TOKEN]
files_path: upload
name: [YOUR-ASSISTANT-NAME]
instructions: [YOUR-ASSISTANT-INSTRUCTIONS]
model: gpt-4-turbo
tools:
- file_search
Каждое поле в этом файле соответствует определенному параметру:
api_url
: URL-адрес, по которому отправляются запросы к OpenAI API. Он зависит от выбранного сервиса. Мы используем ProxyApi, но можно указать прямое использование OpenAI API или любой другой прокси-сервис.api_key
: Ключ доступа к API, который нужен для авторизации запросов.telegram_bot_token
: Токен Telegram бота, который позволяет взаимодействовать с Telegram API и получать/отправлять сообщения.files_path
: Путь к директории, где хранятся файлы, которые будут загружены в ассистента и использоваться для поиска информации. По умолчанию upload.name
: Имя ассистента, которое используется для его идентификации в системе.instructions
: Инструкции для ассистента, определяющие его поведение. Инструкции задаются в виде многострочного текста, в котором описываются правила и ограничения.model
: Модель, на базе которой будет работать ассистент. В нашем примереgpt-4-turbo
.tools
: Список инструментов, доступных для ассистента. Здесь мы используем толькоfile_search
для поиска по загруженным файлам, но при желании можно добавить и другие инструменты.
Структура данных config
Создадим структуру данных, которая повторяет все параметры, указанные в config.yaml
.
// Структура для хранения настроек из config.yaml
type Config struct {
ApiURL string `yaml:"api_url"` // URL-адрес для доступа к OpenAI API
APIKey string `yaml:"api_key"` // Ключ API
TelegramBotToken string `yaml:"telegram_bot_token"`// Токен Telegram бота
FilesPath string `yaml:"files_path"` // Путь к директории с файлами
Name string `yaml:"name"` // Имя ассистента
Instructions string `yaml:"instructions"` // Инструкции для ассистента
Model string `yaml:"model"` // Модель для работы ассистента
Tools []string `yaml:"tools"` // Список инструментов для ассистента
}
Каждое поле в этой структуре связывается с параметрами из config.yaml
с помощью тегов yaml
. Например, параметр api_url
в конфигурации будет автоматически привязан к полю ApiURL
в структуре Config
.
Считывание конфигурационного файла
Теперь считаем конфигурацию. Создадим функцию loadConfig
, которая считает файл config.yaml
и загрузит данные в структуру Config.
Для этого будем пользоваться библиотекой gopkg.in/yaml.v2
.
import (
yaml "gopkg.in/yaml.v2"
)
var config Config
// Функция для чтения конфигурационного файла
func loadConfig(configPath string) error {
// Считываем файл конфигурации
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("Ошибка чтения файла конфигурации: %v", err)
}
// Парсим YAML и заполняем структуру Config
err = yaml.Unmarshal(data, &config)
if err != nil {
return fmt.Errorf("Ошибка разбора файла конфигурации: %v", err)
}
return nil
}
В наш main
добавится следующий код:
func main() {
// Загружаем конфигурацию из файла
err := loadConfig("config.yaml")
if err != nil {
os.Exit(1)
}
}
Использование конфига – единственное усложнение нашей программы, хотя оно и призвано упростить создание ассистента.
На этом наши работы закончены. Можем переходить к запуску программы.
Итог
Тестируем
Я добавил немного логгирования после основных событий, а также положил в папку четыре файла по налоговому законодательству, а в инструкции ассистента указал, что он выступает в качестве налогового консультанта и должен помогать пользователям.
При запуске программы в консоль выводятся следующие логи:
go run main.go
time=2024-10-06T14:18:10.223+03:00 level=INFO msg="Telegram бот авторизован" username=******
time=2024-10-06T14:18:11.097+03:00 level=INFO msg="Ассистент создан" assistant_id=******
time=2024-10-06T14:18:11.606+03:00 level=INFO msg="Vector Store создан" vector_store_id=******
time=2024-10-06T14:18:22.463+03:00 level=INFO msg="Файл успешно зарегистрирован в Vector Store" file_id=******
time=2024-10-06T14:18:24.613+03:00 level=INFO msg="Файл успешно зарегистрирован в Vector Store" file_id=******
time=2024-10-06T14:18:27.566+03:00 level=INFO msg="Файл успешно зарегистрирован в Vector Store" file_id=******
time=2024-10-06T14:18:36.156+03:00 level=INFO msg="Файл успешно зарегистрирован в Vector Store" file_id=******
time=2024-10-06T14:18:37.618+03:00 level=INFO msg="Ассистент успешно обновлен" assistant_id=******
time=2024-10-06T14:18:37.618+03:00 level=INFO msg="Ассистент готов к работе" assistant_id=******
Как видно, мы соблюдаем нашу последовательность шагов: авторизуемся в Telegram-боте, создаем ассистента, создаем Vector store, загружаем и регистрируем наши четыре файла и обновляем ассистента. Теперь мы готовы работать с пользователями.
time=2024-10-06T14:18:52.794+03:00 level=INFO msg="Получен запрос от пользователя" user_id=***** query="Что ты умеешь?"
time=2024-10-06T14:18:59.541+03:00 level=INFO msg="Ответ отправлен пользователю" user_id==*****
Достаточно расточительно каждый раз при запуске сервиса заново создавать ассистента и загружать в него файлы. Также при таком подходе при перезапуске сервиса пользователь будет, фактически, общаться с новым ассистентом, хотя в его истории сообщений будут сообщения с предыдущими ассистентами. Для исправления этой ситуации нам нужно где-то хранить ID ассистентов, пользователей, thread`ов. Для этого можно использовать файловой хранилище или подключить базу данных.
Код программы
Код программы доступен по ссылке: https://github.com/kochetovdv/proxyapi-bot .