<repo><f p="internal\audio\audio.go"><![CDATA[package audio
import (
 "fmt"
 "os/exec"
 "test/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"
 "test/tg_assistant_bot/pkg/logger"
)
type Config struct {
 TelegramAPIKey string `envconfig:"TELEGRAM_API_KEY" required:"true"`
 TelegramUserID string `envconfig:"TELEGRAM_USER_ID" 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-flash"`
 GoogleVoiceModel string `envconfig:"GOOGLE_VOICE_MODEL" default:"gemini-1.5-flash"`
}
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\database\database.go"><![CDATA[package database
import (
 "test/tg_assistant_bot/internal/models"
 "test/tg_assistant_bot/pkg/logger"
 "gorm.io/driver/sqlite"
 "gorm.io/gorm"
)
var DB *gorm.DB
func InitDB() {
 var err error
 DB, err = gorm.Open(sqlite.Open("bot.db"), &gorm.Config{})
 if err != nil {
 logger.Log.Fatalf("Failed to connect to database: %v", err)
 }
 err = DB.AutoMigrate(&models.ToDo{}, &models.ToDoItem{})
 if err != nil {
 logger.Log.Fatalf("Failed to migrate database schema: %v", err)
 }
 logger.Log.Info("Database initialized successfully")
}]]></f><f p="internal\handler\handler.go"><![CDATA[package handler
import (
 "context"
 "fmt"
 "io"
 "net/http"
 "os"
 "strconv"
 "strings"
 "github.com/go-telegram/bot"
 tgmodels "github.com/go-telegram/bot/models"
 "test/tg_assistant_bot/internal/audio"
 "test/tg_assistant_bot/internal/config"
 "test/tg_assistant_bot/internal/llm"
 "test/tg_assistant_bot/internal/models"
 "test/tg_assistant_bot/pkg/logger"
)
func DefaultHandler(ctx context.Context, b *bot.Bot, update *tgmodels.Update, processor llm.LLMProcessor) {
 userID := update.Message.From.ID
 adminID, _ := strconv.Atoi(config.AppConfig.TelegramUserID)
 if userID != int64(adminID) {
 return
 }
 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
 }
 userID := update.Message.From.ID
 response, err := processor.ProcessAudio(ctx, finalFile, userID)
 if err != nil {
 logger.Log.Errorf("Error processing audio with LLM: %v", err)
 return nil, err
 }
 return response, nil
}]]></f><f p="internal\llm\googleai_processor.go"><![CDATA[package llm
import (
 "context"
 "strings"
 "test/tg_assistant_bot/internal/config"
 "test/tg_assistant_bot/internal/models"
 "test/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, userID int64) (*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, userID)
 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"
 "test/tg_assistant_bot/internal/config"
 "test/tg_assistant_bot/internal/models"
 "test/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, userID int64) (*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"
 "test/tg_assistant_bot/internal/config"
 "test/tg_assistant_bot/internal/models"
)
type LLMProcessor interface {
 ProcessAudio(ctx context.Context, audioData []byte, userID int64) (*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"
 "test/tg_assistant_bot/internal/database"
 "test/tg_assistant_bot/internal/models"
 "test/tg_assistant_bot/pkg/logger"
)
func createToDo(s string, userID int64) (*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])
 }
 var items []models.ToDoItem
 for _, todoName := range todos {
 items = append(items, models.ToDoItem{
 Name: todoName,
 Completed: false,
 })
 }
 todo := &models.ToDo{
 UserID: userID,
 Name: rawArgs.Name,
 Items: items,
 }
 if err := database.DB.Create(todo).Error; err != nil {
 logger.Log.Errorf("Error saving ToDo to database: %v", err)
 return nil, err
 }
 logger.Log.Infof("ToDo created and saved to database: %v", todo)
 return todo, 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 `gorm:"primaryKey" json:"id"`
 UserID int64 `json:"user_id"`
 Name string `json:"name"`
 Items []ToDoItem `gorm:"foreignKey:ToDoID" json:"items"`
}
type ToDoItem struct {
 ID int64 `gorm:"primaryKey" json:"id"`
 ToDoID int64 `json:"todo_id"`
 Name string `json:"name"`
 Completed bool `json:"completed"`
}]]></f></repo>

Add updatedAt and createdAt to the todos. Also implement this flow:
When we first talk to bot ("/start"), it will ask us timezone and we will save it to database with userid. so we can later create reminders like "remind me 5 hours later to do X" and it will be based on the timezone