<repo><f p="cmd\bot\main.go"><![CDATA[package main
import (
 "context"
 "os"
 "os/signal"
 "github.com/go-telegram/bot"
 "github.com/go-telegram/bot/models"
 "github.com/rakett1337/tg_assistant_bot/internal/config"
 "github.com/rakett1337/tg_assistant_bot/internal/handler"
 "github.com/rakett1337/tg_assistant_bot/internal/llm"
 "github.com/rakett1337/tg_assistant_bot/pkg/logger"
)
func init() {
 logger.Init()
 config.LoadConfig()
}
func main() {
 ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
 defer cancel()
 llmProcessor, err := llm.NewProcessor()
 if err != nil {
 logger.Log.Fatalf("Error initializing LLM Processor: %v", err)
 }
 defaultHandler := func(ctx context.Context, b *bot.Bot, update *models.Update) {
 handler.DefaultHandler(ctx, b, update, llmProcessor)
 }
 opts := []bot.Option{
 bot.WithDefaultHandler(defaultHandler),
 }
 b, err := bot.New(config.AppConfig.TelegramAPIKey, opts...)
 if err != nil {
 logger.Log.Fatalf("Failed to create bot: %v", err)
 }
 b.Start(ctx)
}]]></f><f p="internal\audio\audio.go"><![CDATA[package audio
import (
 "fmt"
 "os/exec"
 "github.com/rakett1337/tg_assistant_bot/pkg/logger"
)
func ConvertAudio(inputPath, outputPath string) error {
 cmd := exec.Command("ffmpeg", "-i", inputPath, outputPath)
 output, err := cmd.CombinedOutput()
 if err != nil {
 logger.Log.Errorf("FFmpeg conversion error: %v\nOutput: %s", err, string(output))
 return fmt.Errorf("FFmpeg conversion error: %v\nOutput: %s", err, string(output))
 }
 logger.Log.Infof("Audio converted from %s to %s", inputPath, outputPath)
 return nil
}]]></f><f p="internal\config\config.go"><![CDATA[package config
import (
 "os"
 "github.com/joho/godotenv"
 "github.com/kelseyhightower/envconfig"
 "github.com/rakett1337/tg_assistant_bot/pkg/logger"
)
type Config struct {
 TelegramAPIKey string `envconfig:"TELEGRAM_API_KEY" required:"true"`
 AIProvider string `envconfig:"AI_PROVIDER" default:"googleai" required:"true"`
 OpenAIAPIKey string `envconfig:"OPENAI_API_KEY"`
 OpenAITextModel string `envconfig:"OPENAI_TEXT_MODEL" default:"gpt-3.5-turbo"`
 OpenAIVoiceModel string `envconfig:"OPENAI_VOICE_MODEL" default:"whisper-1"`
 GoogleAPIKey string `envconfig:"GOOGLE_API_KEY"`
 GoogleTextModel string `envconfig:"GOOGLE_TEXT_MODEL" default:"gemini-1.5-pro"`
 GoogleVoiceModel string `envconfig:"GOOGLE_VOICE_MODEL" default:"gemini-1.5-pro"`
}
var AppConfig Config
func LoadConfig() {
 if err := godotenv.Load(); err != nil && !os.IsNotExist(err) {
 logger.Log.Fatalf("Error loading .env file: %v", err)
 }
 err := envconfig.Process("", &AppConfig)
 if err != nil {
 logger.Log.Fatalf("Error loading config: %v", err)
 }
 logger.Log.Info("Configuration loaded successfully")
}]]></f><f p="internal\handler\handler.go"><![CDATA[package handler
import (
 "context"
 "io"
 "net/http"
 "os"
 "strings"
 "github.com/go-telegram/bot"
 tgmodels "github.com/go-telegram/bot/models"
 "github.com/rakett1337/tg_assistant_bot/internal/audio"
 "github.com/rakett1337/tg_assistant_bot/internal/llm"
 "github.com/rakett1337/tg_assistant_bot/pkg/logger"
)
func DefaultHandler(ctx context.Context, b *bot.Bot, update *tgmodels.Update, processor llm.LLMProcessor) {
 if update.Message == nil {
 return
 }
 if update.Message.Voice != nil {
 err := handleVoiceMessage(ctx, b, update, processor)
 if err != nil {
 b.SendMessage(ctx, &bot.SendMessageParams{
 ChatID: update.Message.Chat.ID,
 Text: "Can't process voice message request",
 })
 }
 return
 }
 if update.Message.Audio != nil {
 }
}
func handleVoiceMessage(ctx context.Context, b *bot.Bot, update *tgmodels.Update, processor llm.LLMProcessor) error {
 logger.Log.Infof("Handling voice message from user %s", update.Message.From.Username)
 file, err := b.GetFile(ctx, &bot.GetFileParams{
 FileID: update.Message.Voice.FileID,
 })
 if err != nil {
 logger.Log.Errorf("Error getting file: %v", err)
 return err
 }
 downloadURL := b.FileDownloadLink(file)
 resp, err := http.Get(downloadURL)
 if err != nil {
 logger.Log.Errorf("Error downloading file: %v", err)
 return err
 }
 defer resp.Body.Close()
 fileData, err := io.ReadAll(resp.Body)
 if err != nil {
 logger.Log.Errorf("Error reading file data: %v", err)
 return err
 }
 inputFileName := file.FileUniqueID + ".oga"
 err = os.WriteFile(inputFileName, fileData, 0644)
 if err != nil {
 logger.Log.Errorf("Error saving file: %v", err)
 return err
 }
 defer os.Remove(inputFileName)
 outputFileName := strings.TrimSuffix(inputFileName, ".oga") + ".wav"
 err = audio.ConvertAudio(inputFileName, outputFileName)
 if err != nil {
 logger.Log.Errorf("Error converting audio: %v", err)
 return err
 }
 defer os.Remove(outputFileName)
 finalFile, err := os.ReadFile(outputFileName)
 if err != nil {
 logger.Log.Errorf("Error reading converted audio file: %v", err)
 return err
 }
 response, err := processor.ProcessAudio(ctx, finalFile)
 if err != nil {
 logger.Log.Errorf("Error processing audio with LLM: %v", err)
 return err
 }
 _, err = b.SendMessage(ctx, &bot.SendMessageParams{
 ChatID: update.Message.Chat.ID,
 Text: response,
 ParseMode: tgmodels.ParseModeHTML,
 })
 if err != nil {
 logger.Log.Errorf("Error sending message: %v", err)
 return err
 }
 return nil
}]]></f><f p="internal\keyboard\todo.go"><![CDATA[package keyboard
import (
 "strconv"
 "github.com/go-telegram/bot"
 tgmodels "github.com/go-telegram/bot/models"
 "github.com/rakett1337/tg_assistant_bot/internal/models"
)
func GenerateToDoKeyboard(b *bot.Bot, todos models.ToDo) *tgmodels.InlineKeyboardMarkup {
 var rows [][]tgmodels.InlineKeyboardButton
 for _, todo := range todos {
 todoButton := tgmodels.InlineKeyboardButton{
 Text: todo.Name,
 CallbackData: "view_todo_" + strconv.Itoa(todo.ID),
 }
 editButton := tgmodels.InlineKeyboardButton{
 Text: "Edit",
 CallbackData: "edit_todo_" + strconv.Itoa(todo.ID),
 }
 deleteButton := tgmodels.InlineKeyboardButton{
 Text: "Delete",
 CallbackData: "delete_todo_" + strconv.Itoa(todo.ID),
 }
 row := []tgmodels.InlineKeyboardButton{todoButton, editButton, deleteButton}
 rows = append(rows, row)
 }
 keyboard := &tgmodels.InlineKeyboardMarkup{
 InlineKeyboard: rows,
 }
 return keyboard
}]]></f><f p="internal\llm\googleai_processor.go"><![CDATA[package llm
import (
 "context"
 "strings"
 "github.com/rakett1337/tg_assistant_bot/internal/config"
 "github.com/rakett1337/tg_assistant_bot/pkg/logger"
 "github.com/tmc/langchaingo/llms"
 "github.com/tmc/langchaingo/llms/googleai"
)
type GoogleAIProcessor struct {
 client llms.Model
}
func NewGoogleAIProcessor() (*GoogleAIProcessor, error) {
 client, err := googleai.New(context.Background(), googleai.WithAPIKey(config.AppConfig.GoogleAPIKey))
 if err != nil {
 logger.Log.Errorf("Error initializing Google AI client: %v", err)
 return nil, err
 }
 return &GoogleAIProcessor{
 client: client,
 }, nil
}
func (p *GoogleAIProcessor) ProcessAudio(ctx context.Context, audioData []byte) (string, error) {
 var err error
 answer, err := p.client.GenerateContent(
 ctx,
 []llms.MessageContent{
 {
 Role: llms.ChatMessageTypeSystem,
 Parts: []llms.ContentPart{
 llms.TextPart(strings.TrimSpace(SPEECH_TO_TEXT_SYSTEN_PROMPT)),
 },
 },
 {
 Role: llms.ChatMessageTypeHuman,
 Parts: []llms.ContentPart{
 llms.TextPart("I am from Estonia and likely will speak in that language"),
 llms.BinaryPart("audio/wav", audioData),
 },
 },
 },
 llms.WithMaxTokens(4000),
 llms.WithTemperature(0),
 llms.WithModel(config.AppConfig.GoogleVoiceModel),
 llms.WithTools(availableTools),
 )
 if err != nil {
 logger.Log.Errorf("Error generating content with Google AI: %v", err)
 return "", err
 }
 logger.Log.Infof("Google AI response received")
 if answer.Choices[0].ToolCalls != nil {
 switch answer.Choices[0].ToolCalls[0].FunctionCall.Name {
 case "createNewToDo":
 args := answer.Choices[0].ToolCalls[0].FunctionCall.Arguments
 logger.Log.Infof("createNewToDo function call with args: %v", args)
 todos, _ := createToDo(args)
 }
 }
 if err != nil {
 return "", err
 }
 return answer.Choices[0].Content, nil
}]]></f><f p="internal\llm\processor.go"><![CDATA[package llm
import (
 "context"
 "fmt"
 "github.com/rakett1337/tg_assistant_bot/internal/config"
)
type LLMProcessor interface {
 ProcessAudio(ctx context.Context, audioData []byte) (string, error)
}
func NewProcessor() (LLMProcessor, error) {
 switch config.AppConfig.AIProvider {
 case "googleai":
 return NewGoogleAIProcessor()
 case "openai":
 return NewOpenAIProcessor()
 default:
 return nil, fmt.Errorf("unsupported AI provider: %s", config.AppConfig.AIProvider)
 }
}]]></f><f p="internal\llm\prompt.go"><![CDATA[package llm
const SPEECH_TO_TEXT_SYSTEN_PROMPT = `
You are AI assistant helping the user to create new To-Do lists. 
You are provided with some tools. Do your best to figure out the title and contents of said list
and then return the tool call. If the user speaks to you in another language, keep the name and to-dos in original language.
After transcribing the voice, do your best to figure out if there are typos:
you may hear: tee shopping list: kurk, pelmeenid, talrik.
In this example, you can understand the user is speaking estonian and the word talrik does not exist, they most likely meant taldrik - fix the typo before doing function call. 
If user sends you gibberish, reply with "I do not understand you"
`]]></f><f p="internal\llm\tool.go"><![CDATA[package llm
import (
 "github.com/tmc/langchaingo/llms"
)
var availableTools = []llms.Tool{
 {
 Type: "function",
 Function: &llms.FunctionDefinition{
 Name: "createNewToDo",
 Description: "Create a new To-Do List.",
 Parameters: map[string]any{
 "type": "object",
 "properties": map[string]any{
 "name": map[string]any{
 "type": "string",
 "description": "Name of the To-Do",
 },
 "todos": map[string]any{
 "type": "string",
 "description": "Comma-separated list of todos to add (e.g., 'buy milk, walk the dog, finish report')",
 },
 },
 "required": []string{"name", "todos"},
 },
 },
 },
}]]></f><f p="internal\llm\util.go"><![CDATA[package llm
import (
 "encoding/json"
 "strings"
 "github.com/rakett1337/tg_assistant_bot/internal/models"
 "github.com/rakett1337/tg_assistant_bot/pkg/logger"
)
func createToDo(s string) (*models.ToDo, error) {
 var rawArgs struct {
 Name string `json:"name"`
 Todos string `json:"todos"` 
 }
 if err := json.Unmarshal([]byte(s), &rawArgs); err != nil {
 logger.Log.Errorf("Error parsing arguments: %v", err)
 return nil, err
 }
 todos := strings.Split(rawArgs.Todos, ",")
 for i := range todos {
 todos[i] = strings.TrimSpace(todos[i]) 
 }
 name := rawArgs.Name
 return &models.ToDo{
 Name: name,
 Items: todos,
 }, nil
}]]></f><f p="internal\models\todo.go"><![CDATA[package models
type ToDo struct {
 Name string `json:"name"`
 Items []string `json:"todos"`
}]]></f></repo>
-----------
We need to create few changes:
Deal with this: cannot range over todos (variable of type "github.com/rakett1337/tg_assistant_bot/internal/models".ToDo)
Then, I think our handler(i.e handleVoiceMEssage) should return some generic response, that cna hold either a new todo/new reminder/new note (we will implement these later) so that we can define the final answers in DefaultHandler.