<repo><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\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\handler\handler.go"><![CDATA[package handler
import (
 "context"
 "fmt"
 "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/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.Text != "" {
 return
 }
 if update.Message.Voice != nil {
 m, _ := b.SendMessage(ctx, &bot.SendMessageParams{
 ChatID: update.Message.Chat.ID,
 Text: "Processing..please wait",
 })
 response, err := handleVoiceMessage(ctx, b, update, processor)
 if err != nil {
 b.EditMessageText(ctx, &bot.EditMessageTextParams{
 ChatID: m.Chat.ID,
 MessageID: m.ID,
 Text: "Can't process voice message request right now",
 })
 return
 }
 switch response.Type {
 case models.ResponseText:
 b.EditMessageText(ctx, &bot.EditMessageTextParams{
 ChatID: m.Chat.ID,
 MessageID: m.ID,
 Text: response.Text,
 })
 case models.ResponseToDo:
 var ToDosText string
 ToDosName := "<b>" + response.ToDo.Name + "</b>"
 for _, todo := range response.ToDo.Items {
 ToDosText += todo.Name + "\n"
 }
 b.EditMessageText(ctx, &bot.EditMessageTextParams{
 ChatID: m.Chat.ID,
 MessageID: m.ID,
 Text: fmt.Sprintf("<b>Here are your ToDo items: </b>\n\n\n%s\n%s", ToDosName, ToDosText),
 ParseMode: tgmodels.ParseModeHTML,
 })
 case models.ResponseReminder:
 case models.ResponseNote:
 }
 }
}
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
 }
 return response, nil
}]]></f><f p="internal\keyboard\todo.go"><![CDATA[package keyboard
import (
 "fmt"
 tgmodels "github.com/go-telegram/bot/models"
 "asdf/tg_assistant_bot/internal/models"
)
func GenerateToDoKeyboard(todo models.ToDo) *tgmodels.InlineKeyboardMarkup {
 var inlineButtons [][]tgmodels.InlineKeyboardButton
 for _, item := range todo.Items {
 statusSymbol := "⛔"
 if item.Completed {
 statusSymbol = "✅"
 }
 inlineButtons = append(inlineButtons, []tgmodels.InlineKeyboardButton{
 {
 Text: fmt.Sprintf("%s %s", item.Name, statusSymbol),
 CallbackData: fmt.Sprintf("todo_toggle_%d_%d", todo.ID, item.ID),
 },
 })
 inlineButtons = append(inlineButtons, []tgmodels.InlineKeyboardButton{
 {
 Text: "✍️ Edit",
 CallbackData: fmt.Sprintf("todo_edit_%d_%d", todo.ID, item.ID),
 },
 {
 Text: "🗑️ Delete",
 CallbackData: fmt.Sprintf("todo_delete_%d_%d", todo.ID, item.ID),
 },
 })
 }
 return &tgmodels.InlineKeyboardMarkup{
 InlineKeyboard: inlineButtons,
 }
}]]></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 `json:"id"`
 Name string `json:"name"`
 Items []ToDoItem `json:"todos"`
}
type ToDoItem struct {
 ID int64 `json:"id"`
 Name string `json:"name"`
 Completed bool `json:"completed"`
}]]></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\openai_processor.go"><![CDATA[package llm
import (
 "context"
 "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/openai"
)
type OpenAIProcessor struct {
 client llms.Model
}
func NewOpenAIProcessor() (*OpenAIProcessor, error) {
 client, err := openai.New(openai.WithToken(config.AppConfig.OpenAIAPIKey))
 if err != nil {
 logger.Log.Errorf("Error initializing OpenAI client: %v", err)
 return nil, err
 }
 return &OpenAIProcessor{
 client: client,
 }, nil
}
func (p *OpenAIProcessor) ProcessAudio(ctx context.Context, audioData []byte) (*models.GenericResponse, error) {
 _, err := p.client.GenerateContent(
 ctx,
 []llms.MessageContent{
 {
 Role: llms.ChatMessageTypeHuman,
 Parts: []llms.ContentPart{
 llms.TextPart("Please transcribe the following audio:"),
 llms.BinaryPart("audio/wav", audioData),
 },
 },
 },
 llms.WithMaxTokens(4000),
 llms.WithTemperature(0.1),
 llms.WithModel(config.AppConfig.OpenAIVoiceModel),
 )
 if err != nil {
 logger.Log.Errorf("Error generating content with OpenAI: %v", err)
 return nil, err
 }
 logger.Log.Infof("OpenAI response received")
 return nil, 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></repo>

Update it to store ToDos into database. Lets use sqlite. Find best way to add this to current structure. Associate a todo with userid( we can get it from     update.Message.From.ID which is int64)