<repo><f p="internal\config\config.go"><![CDATA[package config
import (
 "os"
 "github.com/joho/godotenv"
 "github.com/kelseyhightower/envconfig"
 "asdf/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\audio\audio.go"><![CDATA[package audio
import (
 "fmt"
 "os/exec"
 "asdf/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\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"
 "asdf/tg_assistant_bot/internal/audio"
 "asdf/tg_assistant_bot/internal/keyboard"
 "asdf/tg_assistant_bot/internal/llm"
 "asdf/tg_assistant_bot/internal/models"
 "asdf/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 {
 response, 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
 }
 switch response.Type {
 case models.ResponseText:
 b.SendMessage(ctx, &bot.SendMessageParams{
 ChatID: update.Message.Chat.ID,
 Text: response.Text,
 ParseMode: tgmodels.ParseModeHTML,
 })
 case models.ResponseToDo:
 keyboard := keyboard.GenerateToDoKeyboard(b, *response.ToDo)
 b.SendMessage(ctx, &bot.SendMessageParams{
 ChatID: update.Message.Chat.ID,
 Text: "Here are your ToDo items:",
 ReplyMarkup: keyboard,
 })
 case models.ResponseReminder:
 case models.ResponseNote:
 }
 return
 }
}
func handleVoiceMessage(ctx context.Context, b *bot.Bot, update *tgmodels.Update, processor llm.LLMProcessor) (*models.GenericResponse, 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 nil, err
 }
 downloadURL := b.FileDownloadLink(file)
 resp, err := http.Get(downloadURL)
 if err != nil {
 logger.Log.Errorf("Error downloading file: %v", err)
 return nil, err
 }
 defer resp.Body.Close()
 fileData, err := io.ReadAll(resp.Body)
 if err != nil {
 logger.Log.Errorf("Error reading file data: %v", err)
 return nil, err
 }
 inputFileName := file.FileUniqueID + ".oga"
 err = os.WriteFile(inputFileName, fileData, 0644)
 if err != nil {
 logger.Log.Errorf("Error saving file: %v", err)
 return nil, 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 nil, err
 }
 defer os.Remove(outputFileName)
 finalFile, err := os.ReadFile(outputFileName)
 if err != nil {
 logger.Log.Errorf("Error reading converted audio file: %v", err)
 return nil, err
 }
 response, err := processor.ProcessAudio(ctx, finalFile)
 if err != nil {
 logger.Log.Errorf("Error processing audio with LLM: %v", err)
 return nil, err
 }
 if err != nil {
 logger.Log.Errorf("Error sending message: %v", err)
 return nil, err
 }
 return response, nil
}]]></f><f p="internal\keyboard\todo.go"><![CDATA[package keyboard
import (
 "context"
 "fmt"
 "github.com/go-telegram/bot"
 tgmodels "github.com/go-telegram/bot/models"
 "github.com/go-telegram/ui/keyboard/inline"
 "asdf/tg_assistant_bot/internal/models"
)
func GenerateToDoKeyboard(b *bot.Bot, todo models.ToDo) *inline.Keyboard {
 todoInlineKeyboard := inline.New(b, inline.WithPrefix("todo"))
 for _, item := range todo.Items {
 todoInlineKeyboard.Row().
 Button(item.Name, []byte(fmt.Sprintf("view_%d_%d", todo.ID, item.ID)), onToDoItemSelect).
 Button("✏️", []byte(fmt.Sprintf("edit_%d_%d", todo.ID, item.ID)), onToDoItemEdit).
 Button("🗑️", []byte(fmt.Sprintf("delete_%d_%d", todo.ID, item.ID)), onToDoItemDelete)
 todoInlineKeyboard.Row()
 }
 return todoInlineKeyboard
}
func onToDoItemEdit(ctx context.Context, b *bot.Bot, mes tgmodels.MaybeInaccessibleMessage, data []byte) {
}
func onToDoItemDelete(ctx context.Context, b *bot.Bot, mes tgmodels.MaybeInaccessibleMessage, data []byte) {
}
func onToDoItemSelect(ctx context.Context, b *bot.Bot, mes tgmodels.MaybeInaccessibleMessage, data []byte) {
}]]></f><f p="internal\llm\googleai_processor.go"><![CDATA[package llm
import (
 "context"
 "strings"
 "asdf/tg_assistant_bot/internal/config"
 "asdf/tg_assistant_bot/internal/models"
 "asdf/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) (*models.GenericResponse, 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 {
 return nil, 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)
 todo, _ := createToDo(args)
 return &models.GenericResponse{
 Type: models.ResponseToDo,
 ToDo: todo,
 }, nil
 }
 }
 return &models.GenericResponse{
 Type: models.ResponseText,
 Text: answer.Choices[0].Content,
 }, nil
}]]></f><f p="internal\llm\processor.go"><![CDATA[package llm
import (
 "context"
 "fmt"
 "asdf/tg_assistant_bot/internal/config"
 "asdf/tg_assistant_bot/internal/models"
)
type LLMProcessor interface {
 ProcessAudio(ctx context.Context, audioData []byte) (*models.GenericResponse, 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"
 "asdf/tg_assistant_bot/internal/models"
 "asdf/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
 var items []models.ToDoItem
 var nextID int64 = 1 
 for _, todoName := range todos {
 items = append(items, models.ToDoItem{
 ID: nextID,
 Name: todoName,
 Completed: false,
 })
 nextID++
 }
 return &models.ToDo{
 ID: 0, 
 Name: name,
 Items: items,
 }, nil
}]]></f><f p="internal\models\response.go"><![CDATA[package models
type ResponseType int
const (
 ResponseText ResponseType = iota
 ResponseToDo
 ResponseReminder
 ResponseNote
)
type GenericResponse struct {
 Type ResponseType
 Text string 
 ToDo *ToDo 
}]]></f><f p="internal\models\todo.go"><![CDATA[package models
type ToDo struct {
 ID int64
 Name string `json:"name"`
 Items []ToDoItem `json:"todos"`
}
type ToDoItem struct {
 ID int64
 Name string `json:"name"`
 Completed bool `json:"completed"`
}]]></f></repo>

When clicking on any menu items of todo, the response disappears. Why? Also is there way to make todo name button longer but the delete/edit buttons small