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:
Register: https://proxyapi.ru
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.
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.
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:
Create an assistant. Here we send a request to create an assistant with the necessary parameters and receive
assistant_id
.Create a VectorStore and upload files to it. Send a request to create
Vector Store
and we getvector_store_id
. We load each file from the directory and get for themfile_id
. We need to tie eachfile_id
Tovector_store_id
.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_search
which 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:
Creates a Vector Store and receives
vector_store_id
.Scans the directory specified in the configuration and downloads each file.
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_id
which 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_id
which 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
.
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.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.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.
Create thread (
Thread
) and send a message (Message
). Here we initialize a new thread and pass a message from the user to it.Initialize launch (
Run
). Launching involves creating a new conversation with the assistant and receiving a response.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 Stream
then 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/runs
by 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:
Initializing the Telegram bot and setting up the connection.
Receiving and processing incoming messages.
Sending messages to an assistant and receiving a response.
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.MessageConfig
containing 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.yaml
which 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 examplegpt-4-turbo
.tools
: List of tools available to the assistant. Here we only usefile_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 loadConfig
which 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==*****
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 .