<repo><f p="pkg\logger\logger.go"><![CDATA[package logger
import (
 "io"
 "os"
 "github.com/sirupsen/logrus"
)
var Log *logrus.Logger
func Init() {
 Log = logrus.New()
 Log.SetOutput(os.Stdout)
 Log.SetLevel(logrus.InfoLevel)
 Log.SetFormatter(&logrus.TextFormatter{
 FullTimestamp: true,
 })
}
func SetOutput(w io.Writer) {
 Log.SetOutput(w)
}]]></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\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\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 (
 "context"
 "github.com/go-telegram/bot"
 tgmodels "github.com/go-telegram/bot/models"
 "github.com/go-telegram/ui/keyboard/inline"
)
var demoInlineKeyboard *inline.Keyboard
func initInlineKeyboard(b *bot.Bot) {
 demoInlineKeyboard = inline.New(b, inline.WithPrefix("inline")).
 Row().
 Button("Row 1, Btn 1", []byte("1-1"), onInlineKeyboardSelect).
 Button("Row 1, Btn 2", []byte("1-2"), onInlineKeyboardSelect).
 Row().
 Button("Row 2, Btn 1", []byte("2-1"), onInlineKeyboardSelect).
 Button("Row 2, Btn 2", []byte("2-2"), onInlineKeyboardSelect).
 Button("Row 2, Btn 3", []byte("2-3"), onInlineKeyboardSelect).
 Row().
 Button("Row 3, Btn 1", []byte("3-1"), onInlineKeyboardSelect).
 Button("Row 3, Btn 2", []byte("3-2"), onInlineKeyboardSelect).
 Button("Row 3, Btn 3", []byte("3-3"), onInlineKeyboardSelect).
 Button("Row 3, Btn 4", []byte("3-4"), onInlineKeyboardSelect).
 Row().
 Button("Cancel", []byte("cancel"), onInlineKeyboardSelect)
}
func handlerInlineKeyboard(ctx context.Context, b *bot.Bot, update *tgmodels.Update) {
 b.SendMessage(ctx, &bot.SendMessageParams{
 ChatID: update.Message.Chat.ID,
 Text: "Select the variant",
 ReplyMarkup: demoInlineKeyboard,
 })
}
func onInlineKeyboardSelect(ctx context.Context, b *bot.Bot, mes tgmodels.MaybeInaccessibleMessage, data []byte) {
 b.SendMessage(ctx, &bot.SendMessageParams{
 ChatID: mes.Message.Chat.ID,
 Text: "You selected: " + string(data),
 })
}]]></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)
 todo, err := createToDo(args)
 }
 }
 if err != nil {
 return "", err
 }
 return answer.Choices[0].Content, nil
}]]></f><f p="internal\llm\openai_processor.go"><![CDATA[package llm
import (
 "context"
 "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/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) (string, error) {
 answer, 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 "", err
 }
 logger.Log.Infof("OpenAI response received")
 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 (
 "errors"
 "github.com/tmc/langchaingo/llms"
)
type ToDo struct {
 Name string `json:"name"`
 Items []string `json:"todos"`
}
func createNewToDo(name string, items []string) (ToDo, error) {
 if name == "" {
 return ToDo{}, errors.New("name cannot be empty")
 }
 return ToDo{
 Name: name,
 Items: items,
 }, nil
}
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/pkg/logger"
)
func createToDo(s string) (*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 &ToDo{
 Name: name,
 Items: todos,
 }, nil
}]]></f><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></repo>

We need to modify it:
first of all lets figure out a way to handle voice messages and what we return from there better. Right now we can only return TODOs, but we will have more options (a new reminder or a note).
Also I want to implement a way to send back inline keyboard with todos on each row and it will also have edit and delete button for said todo