Building an AI assistant with the OpenAI Assistants API in Go

Hi all!

My name is Dmitry, I am engaged in developing relationships with clients and partners in an IT company StecPoint.

Recently, there was a need to create an AI assistant trained to search and provide information from a knowledge base provided by the customer.

In this article we will look at the process of creating an MVP of such an assistant. We will upload files into it, set instructions, link everything to the Telegram bot and process user requests. To create an assistant, we will use the functionality of the OpenAI Assistants API. Unfortunately, OpenAI services are not available from the Russian Federation without a VPN, but we will solve this problem using third-party proxy services.

We will write all this in Go. The code, ready to run and compile, is available at the end of the article.

Disclaimer

Everything will be simple here. We will write all the code in main. We will not somehow prepare the infrastructure, use the database, or bother with logging and testing. The only thing is to move some parameters into config.

Our task is to learn how to create API assistants and receive answers to user requests from them.

However, the code will be fully functional, and if you run the service on your computer and do not turn it off, everything will work wonderfully 🙂

How to get access from the Russian Federation

We use various VPNs to access ChatGPT from a web browser. To access the OpenAI Assistants API, we, firstly, need to somehow top up the account balance to generate responses, and secondly, pass the traffic of our service through a VPN.

Both of these points complicate the whole process, so we will use a proxy service. There are services on the market that take care of all these user worries about payment and VPN. You register in their personal account, top up your balance with a Russian card, integrate as if you were using the final API, only using the URL of this service.

For the purposes of our project, I will use ProxyApi.ru. I don’t have any particular preferences, but this is the first service that came up in my search.

The sequence of steps is as follows:

  1. Register: https://proxyapi.ru

  2. In your personal account we generate a key for ourselves: https://console.proxyapi.ru/keys. The key must be stored somewhere safe, since the service displays it only once at the time of generation.

  3. Subscribe to Pro to access Assistant API functionality: https://console.proxyapi.ru/pro. To access the usual chat functionality, if we were to create such a service, a subscription would not be required, but to work with the Assistants API, vector storage and files, it is required.

  4. We replenish the balance for future generation of answers: https://console.proxyapi.ru/billing

Registration is free, the Pro subscription costs 1,500 rubles/month, and I topped up my balance by 1,000 rubles. It should be enough for the test.

Next, we will write a service based on the OpenAI documentation, but we will not knock directly on https://api.openai.com/v1/and by https://api.proxyapi.ru/openai/v1/. Otherwise the integration will be identical.

With this approach, you will not be able to gain access to assistants that you could have previously created in your OpenAI personal account – the ProxyAPI service simply will not see them and will not have access.

Registering a bot in Telegram

Since our users will interact with our assistant through a Telegram bot, apparently, we also need to create one.

To do this, we go to the “father of bots”: https://t.me/BotFather.

Send a message or select from the menu /newbot and fill in the required fields. As a result, we will be congratulated on creating a bot and will be given a token for access. This is what we will need in the future.

A little about OpenAI Assistants API

The OpenAI Assistants API gives developers the ability to create custom AI assistants based on GPT models. The API allows:

  • Customize the assistant's behavior through instructions and parameters.

  • Add special tools – file search, web search.

  • Integrate the assistant with various platforms and services.

Using this API, you can create assistants that not only understand natural language, but also perform specific tasks using the resources provided to them.

Working with the OpenAI Assistants API begins with creating and configuring an assistant that will interact with users and use the resources provided to it to search for information in them.

Creating an assistant

To successfully create an assistant, we will need to complete the following sequence of steps:

  1. Create an assistant. Here we send a request to create an assistant with the necessary parameters and receive assistant_id.

  2. Create a VectorStore and upload files to it. Send a request to create Vector Store and we get vector_store_id. We load each file from the directory and get for them file_id. We need to tie each file_id To vector_store_id.

  3. Update assistant. We need to throw vector_store_id to the assistant configuration.

Now more details.

Step 1. Create an assistant

An assistant in OpenAI is an entity that combines a model (for example, GPT-4) and tools (for example, file search) and is configured with instructions and parameters that determine its behavior. To create an assistant, you must send a creation request specifying the following parameters:

  • Name – the name of our assistant.

  • Instructions — text instructions defining how the assistant should behave and what topics to answer. Here we indicate something like “you are a tax consultant, polite, answer in Russian.”

  • Model — the model used. We will use gpt-4-turbo.

  • Tools — a list of tools that the assistant will use. In our case this is a tool file_searchwhich allows you to search for information in downloaded files.

Our data structure for the assistant:

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"`
}

Now let's create an assistant. We will read the parameters for the assistant from config, the method for which we will write later. OpenAI also tells us that to access the assistant functionality we must add a parameter to the headers OpenAI-Beta:assistants=v2 .

Our method for creating an assistant:

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
}

IN main let's add the following code:

func main() {
    // Создаем ассистента
    assistantID, err := createAssistant()
    if err != nil {
        os.Exit(1)
    }
}

The result of successful execution of the function is the receipt of an assistant ID (assistant_id), which will be used later.

Step 2: Create a Vector Store and upload files

Now we need to create a Vector Store – a repository into which we will upload our files for the assistant to search for information. In our case, we create a Vector Store and immediately load files from the specified directory into it.

Function createVectorStoreAndUploadFiles() performs the following actions:

  1. Creates a Vector Store and receives vector_store_id.

  2. Scans the directory specified in the configuration and downloads each file.

  3. Registers each uploaded file in the created Vector Store.

Creation of Vector Store

The request to create a Vector Store does not require any parameters. We get vector_store_idwhich we will use to add files and update the assistant:

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
}

Uploading files

Function uploadFile() uploads a file to the API and returns it file_id. We will take files from the folder specified in the config. The assistant supports many formats, including 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
}

Registering files in the Vector Store

After downloading the file we receive it file_idwhich must be registered in our Vector Store. This links the files to the Vector Store, allowing the assistant to use them to find information.

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
}

IN main let's add the following code:

func main() {
    vectorStoreID, err := createVectorStoreAndUploadFiles()
    if err != nil {
        os.Exit(1)
    }
}

Step 3. Update the assistant with the new Vector Store

The last step is to update the assistant so that it knows that it now has access to Vector Store and the files placed in it. We indicate vector_store_id for tool file_search in assistant configuration.

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
}

IN main let's add the following code:

func main() {
    // Привязываем Vector Store к ассистенту
    if err := updateAssistantWithVectorStore(assistantID, vectorStoreID); err != nil {
        os.Exit(1)
    }
}

We've created a ready-to-use assistant that can search and return information from uploaded files using OpenAI tools and the OpenAI model. The main artifact for us here is assistantID, which we will use further.

Working with messages

The OpenAI Assistants API uses several key entities to process and manage messages sent by users: Threads, Messages And Runs.

  1. Threads (Streams). A thread represents a context, or in other words, a conversational session. It stores a history of messages sent by the user and the assistant. A thread is created for each new request or interaction.

  2. Messages (Messages). A message is a single piece of information sent by a user or assistant. Each message has a role (user, assistant, system), which indicates who sent the message.

  3. Run (Launch). Triggering is the process of executing one or more messages within a specific thread. The launch is initiated when you need to perform actions or receive a response from the assistant, based on the current context. Each launch is assigned a unique identifier and is processed by the server in real time.

These three entities work together to ensure that requests are processed correctly and responses are provided to users.

Let's create data structures:

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"`         // Указывает, активировать ли потоковую передачу данных.
}

The basic algorithm for interacting with the assistant is as follows.

  1. Create thread (Thread) and send a message (Message). Here we initialize a new thread and pass a message from the user to it.

  2. Initialize launch (Run). Launching involves creating a new conversation with the assistant and receiving a response.

  3. Process and display the result. The assistant returns a response after processing the request.

Now more details.

Step 1: Create a Thread and send a Message

To interact with the assistant, you need to create a thread and send messages to it. A thread is created automatically when you send your first message. The request specifies the assistant's ID (assistant_id), current thread context (Thread) and parameters affecting response generation (for example, temperature And top_p). In our example, these parameters will be equal to 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)
}

The above code creates a thread with a message from the user (user), launch is initialized (Run), and a request is sent to the API. The request specifies:

  • AssistantID — assistant identifier.

  • Thread — context of the current thread.

  • ToolResources — resources used (our vector_store_ids).

  • Stream — determines whether to use streaming data or not.

Step 2. Receive and process messages

After sending the request, the assistant returns the result in the form of a response, which we can process and display to the user. If the mode is activated Streamthen the responses arrive in parts, and the Server-Sent Events (SSE) mechanism is used to process them.

Server-Sent Events (SSE) is a mechanism that allows the server to send updates to the client in real time. It is used for streaming data when it is necessary to receive a response as it is generated. To the user, it looks like the service is printing a message. SSE provides a one-way connection from server to client, minimizing latency and network congestion.

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
}

Function listenToSSEStream:

  • Opens a stream to read data from the server response.

  • Processes each line, checking if it is part of an event with a prefix data: .

  • Extracts text fragments from events thread.message.delta and adds them to the final message.

  • Stops reading when an event is received thread.message.completed or tags [DONE].

  • Collects the full text of the response received in parts and returns it as a string.

To better understand how SSE works and what format the response comes in, try making a POST request via Postman to the method /v1/threads/runsby specifying your authorization token by adding a parameter to the header OpenAI-Beta:assistants=v2 and specifying the following request body:

{
  "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
}

So, we created an assistant and learned how to send custom messages to it and receive replies. There is very little left until our final implementation.

Integration of the assistant with Telegram and interaction with users

Now let's create a Telegram bot to interact with real users. Telegram provides a convenient API for creating bots that can receive and process messages sent by users.

Basic steps:

  1. Initializing the Telegram bot and setting up the connection.

  2. Receiving and processing incoming messages.

  3. Sending messages to an assistant and receiving a response.

  4. Sending a response back to the user.

Step 1. Initializing the Telegram bot

First you need to create a bot in Telegram using @BotFather. It will provide a unique token that is used to authenticate requests to the Telegram API.

Bot initialization looks like this:

func main() {
    // Инициализируем Telegram бота
    bot, err := tgbotapi.NewBotAPI(config.TelegramBotToken)
    if err != nil {
        os.Exit(1)
    }
    bot.Debug = false // Отключаем отладку самого бота
}

Step 2. Processing incoming messages

The Telegram update channel is used to process incoming messages. The bot subscribes to all new messages, and each message is processed in a separate goroutine.

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) // Передаем параметры в горутину
        }
    }
}

Here we have:

  • An update channel is being created updates with a timeout of 60 seconds.

  • In a loop for update := range updates All new messages are processed.

  • For each message, a separate goroutine is launched with an anonymous function to process requests asynchronously.

  • The message is sent to the assistant using the function createAndRunAssistantWithStreaming.

  • The received response is sent back to the user as a new message.

IN main let's add the following code:

func main() {
    // Запускаем обработку запросов от пользователей
    handleTelegramUpdates(bot, assistantID, vectorStoreID)
}

Step 3: Sending messages to your assistant

The message received from the user is transmitted to the assistant for execution in createAndRunAssistantWithStreaming.

Step 4: Send a response to the user

Having received a response from the assistant, we generate a new message and send it to the user:

msg := tgbotapi.NewMessage(update.Message.Chat.ID, response)
bot.Send(msg)

The message is sent using the method bot.Send()which takes an object tgbotapi.MessageConfigcontaining the chat ID and message text.

This is where our work with Telegram ends. Little things left.

Working with the config.yaml configuration file

Now we just need to read all the parameters from the config file. This project uses the file config.yamlwhich contains all the necessary data for the assistant to work and integration with Telegram. A configuration file provides a convenient way to manage settings without having to make code changes.

Contents of config.yaml

In file config.yaml The following parameters are stored:

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

Each field in this file corresponds to a specific parameter:

  • api_url: The URL where requests to the OpenAI API are made. It depends on the selected service. We use ProxyApi, but you can specify direct use of the OpenAI API or any other proxy service.

  • api_key: API access key, which is needed to authorize requests.

  • telegram_bot_token: Telegram bot token that allows you to interact with the Telegram API and receive/send messages.

  • files_path: Path to the directory where files are stored that will be loaded into the assistant and used to search for information. By default upload.

  • name: The assistant's name, which is used to identify him in the system.

  • instructions: Instructions for the assistant that determine his behavior. Instructions are given in the form of multi-line text that describes the rules and restrictions.

  • model: Model on which the assistant will work. In our example gpt-4-turbo.

  • tools: List of tools available to the assistant. Here we only use file_search to search through downloaded files, but you can add other tools if you wish.

config data structure

Let's create a data structure that repeats all the parameters specified in 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"`             // Список инструментов для ассистента
}

Each field in this structure is associated with parameters from config.yaml using tags yaml. For example, the parameter api_url in the configuration will be automatically linked to the field ApiURL in the structure Config.

Reading a configuration file

Now we consider the configuration. Let's create a function loadConfigwhich reads the file config.yaml and load the data into the structure Config. For this we will use the library 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
}

In our main the following code will be added:

func main() {
    // Загружаем конфигурацию из файла
    err := loadConfig("config.yaml")
    if err != nil {
        os.Exit(1)
    }
}

Using a config is the only complication of our program, although it is intended to simplify the creation of an assistant.

This concludes our work. We can proceed to launching the program.

Bottom line

Testing

I added a little logging after the main events, and also put four files on tax legislation in a folder, and in the assistant's instructions I indicated that he acts as a tax consultant and should help users.

When you run the program, the following logs are output to the console:

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=******

As you can see, we follow our sequence of steps: log in to the Telegram bot, create an assistant, create a Vector store, upload and register our four files and update the assistant. Now we are ready to work with users.

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==*****
An example of our bot's response.

An example of our bot's response.

It is quite wasteful to re-create the assistant and upload files to it every time you start the service. Also, with this approach, when the service is restarted, the user will, in fact, communicate with the new assistant, although his message history will contain messages with previous assistants. To correct this situation, we need to store the IDs of assistants, users, and threads somewhere. To do this, you can use file storage or connect a database.

Program code

The program code is available at the link: https://github.com/kochetovdv/proxyapi-bot .

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *