diff --git a/bot/bot.go b/bot/bot.go new file mode 100644 index 0000000..e75dc17 --- /dev/null +++ b/bot/bot.go @@ -0,0 +1,224 @@ +package bot + +import ( + "errors" + "github.com/mymmrac/telego" + th "github.com/mymmrac/telego/telegohandler" + tu "github.com/mymmrac/telego/telegoutil" + "log/slog" + "net/url" + "strings" + "telegram-ollama-reply-bot/extractor" + "telegram-ollama-reply-bot/llm" +) + +var ( + ErrGetMe = errors.New("cannot retrieve api user") + ErrUpdatesChannel = errors.New("cannot get updates channel") + ErrHandlerInit = errors.New("cannot initialize handler") +) + +const contextUserKey = "user" + +type Bot struct { + api *telego.Bot + llm *llm.LlmConnector + extractor *extractor.Extractor +} + +func NewBot(api *telego.Bot, llm *llm.LlmConnector, extractor *extractor.Extractor) *Bot { + return &Bot{ + api: api, + llm: llm, + extractor: extractor, + } +} + +func (b *Bot) Run() error { + botUser, err := b.api.GetMe() + if err != nil { + slog.Error("Cannot retrieve api user", err) + + return ErrGetMe + } + + slog.Info("Running api as", map[string]any{ + "id": botUser.ID, + "username": botUser.Username, + "name": botUser.FirstName, + "is_bot": botUser.IsBot, + }) + + updates, err := b.api.UpdatesViaLongPolling(nil) + if err != nil { + slog.Error("Cannot get update channel", err) + + return ErrUpdatesChannel + } + + bh, err := th.NewBotHandler(b.api, updates) + if err != nil { + slog.Error("Cannot initialize bot handler", err) + + return ErrHandlerInit + } + + defer bh.Stop() + defer b.api.StopLongPolling() + + bh.Handle(b.startHandler, th.CommandEqual("start")) + bh.Handle(b.heyHandler, th.CommandEqual("hey")) + bh.Handle(b.summarizeHandler, th.CommandEqual("summarize")) + bh.Handle(b.helpHandler, th.CommandEqual("help")) + + bh.Start() + + return nil +} + +func (b *Bot) heyHandler(bot *telego.Bot, update telego.Update) { + slog.Info("/hey") + + chatID := tu.ID(update.Message.Chat.ID) + + b.sendTyping(chatID) + + llmReply, err := b.llm.HandleSingleRequest(update.Message.Text, llm.ModelMistralUncensored) + if err != nil { + slog.Error("Cannot get reply from LLM connector") + + _, _ = b.api.SendMessage(b.reply(update.Message, tu.Message( + chatID, + "LLM request error. Try again later.", + ))) + + return + } + + slog.Debug("Got completion. Going to send.", llmReply) + + message := tu.Message( + chatID, + llmReply, + ).WithParseMode("Markdown") + + _, err = bot.SendMessage(b.reply(update.Message, message)) + + if err != nil { + slog.Error("Can't send reply message", err) + } +} + +func (b *Bot) summarizeHandler(bot *telego.Bot, update telego.Update) { + slog.Info("/summarize", update.Message.Text) + + chatID := tu.ID(update.Message.Chat.ID) + + b.sendTyping(chatID) + + args := strings.Split(update.Message.Text, " ") + + if len(args) < 2 { + _, _ = bot.SendMessage(tu.Message( + tu.ID(update.Message.Chat.ID), + "Usage: /summarize \r\n\r\n"+ + "Example:\r\n"+ + "/summarize https://kernel.org/get-notifications-for-your-patches.html", + )) + + return + } + + _, err := url.ParseRequestURI(args[1]) + if err != nil { + slog.Error("Provided URL is not valid", args[1]) + + _, _ = b.api.SendMessage(b.reply(update.Message, tu.Message( + chatID, + "URL is not valid.", + ))) + + return + } + + article, err := b.extractor.GetArticleFromUrl(args[1]) + if err != nil { + slog.Error("Cannot retrieve an article using extractor", err) + } + + llmReply, err := b.llm.Summarize(article.Text, llm.ModelMistralUncensored) + if err != nil { + slog.Error("Cannot get reply from LLM connector") + + _, _ = b.api.SendMessage(b.reply(update.Message, tu.Message( + chatID, + "LLM request error. Try again later.", + ))) + + return + } + + slog.Debug("Got completion. Going to send.", llmReply) + + message := tu.Message( + chatID, + llmReply, + ).WithParseMode("Markdown") + + _, err = bot.SendMessage(b.reply(update.Message, message)) + + if err != nil { + slog.Error("Can't send reply message", err) + } +} + +func (b *Bot) helpHandler(bot *telego.Bot, update telego.Update) { + slog.Info("/help") + + chatID := tu.ID(update.Message.Chat.ID) + + b.sendTyping(chatID) + + _, err := bot.SendMessage(b.reply(update.Message, tu.Messagef( + chatID, + "Instructions:\r\n"+ + "/hey - Ask something from LLM\r\n"+ + "/summarize - Summarize text from the provided link\r\n"+ + "/help - Show this help", + ))) + if err != nil { + slog.Error("Cannot send a message", err) + } +} + +func (b *Bot) startHandler(bot *telego.Bot, update telego.Update) { + slog.Info("/start") + + chatID := tu.ID(update.Message.Chat.ID) + + b.sendTyping(chatID) + + _, err := bot.SendMessage(b.reply(update.Message, tu.Message( + chatID, + "Hey!\r\n"+ + "Check out /help to learn how to use this bot.", + ))) + if err != nil { + slog.Error("Cannot send a message", err) + } +} + +func (b *Bot) reply(originalMessage *telego.Message, newMessage *telego.SendMessageParams) *telego.SendMessageParams { + return newMessage.WithReplyParameters(&telego.ReplyParameters{ + MessageID: originalMessage.MessageID, + }) +} + +func (b *Bot) sendTyping(chatId telego.ChatID) { + slog.Info("Setting 'typing' chat action") + + err := b.api.SendChatAction(tu.ChatAction(chatId, "typing")) + if err != nil { + slog.Error("Cannot set chat action", err) + } +} diff --git a/extractor/extractor.go b/extractor/extractor.go new file mode 100644 index 0000000..82f7fdf --- /dev/null +++ b/extractor/extractor.go @@ -0,0 +1,42 @@ +package extractor + +import ( + "errors" + "github.com/advancedlogic/GoOse" +) + +var ( + ErrExtractFailed = errors.New("extraction failed") +) + +type Extractor struct { + goose *goose.Goose +} + +func NewExtractor() *Extractor { + gooseExtractor := goose.New() + + return &Extractor{ + goose: &gooseExtractor, + } +} + +type Article struct { + Title string + Text string + Url string +} + +func (e *Extractor) GetArticleFromUrl(url string) (Article, error) { + article, err := e.goose.ExtractFromURL(url) + + if err != nil { + return Article{}, ErrExtractFailed + } + + return Article{ + Title: article.Title, + Text: article.CleanedText, + Url: article.FinalURL, + }, nil +} diff --git a/go.mod b/go.mod index 519724e..1eedf95 100644 --- a/go.mod +++ b/go.mod @@ -3,24 +3,38 @@ module telegram-ollama-reply-bot go 1.22.0 require ( + github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275 github.com/mymmrac/telego v0.29.1 github.com/sashabaranov/go-openai v1.20.2 ) require ( + github.com/PuerkitoBio/goquery v1.4.1 // indirect github.com/andybalholm/brotli v1.1.0 // indirect + github.com/andybalholm/cascadia v1.0.0 // indirect + github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e // indirect github.com/bytedance/sonic v1.10.2 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/fasthttp/router v1.4.22 // indirect + github.com/fatih/set v0.2.1 // indirect + github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // indirect + github.com/go-resty/resty/v2 v2.0.0 // indirect github.com/grbit/go-json v0.11.0 // indirect + github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 // indirect github.com/klauspost/compress v1.17.6 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/mattn/go-runewidth v0.0.3 // indirect + github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 // indirect + github.com/pkg/errors v0.8.1 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect + github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.52.0 // indirect github.com/valyala/fastjson v1.6.4 // indirect golang.org/x/arch v0.6.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index b3906a5..4a01d62 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ +github.com/PuerkitoBio/goquery v1.4.1 h1:smcIRGdYm/w7JSbcdeLHEMzxmsBQvl8lhf0dSw2nzMI= +github.com/PuerkitoBio/goquery v1.4.1/go.mod h1:T9ezsOHcCrDCgA8aF1Cqr3sSYbO/xgdy8/R/XiIMAhA= +github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275 h1:Kuhf+w+ilOGoXaR4O4nZ6Dp+ZS83LdANUjwyMXsPGX4= +github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275/go.mod h1:98NztIIMIntZGtQVIs8H85Q5b88fTbwWFbLz/lM9/xU= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= +github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e h1:s05JG2GwtJMHaPcXDpo4V35TFgyYZzNsmBlSkHPEbeg= +github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= @@ -16,25 +24,44 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fasthttp/router v1.4.22 h1:qwWcYBbndVDwts4dKaz+A2ehsnbKilmiP6pUhXBfYKo= github.com/fasthttp/router v1.4.22/go.mod h1:KeMvHLqhlB9vyDWD5TSvTccl9qeWrjSSiTJrJALHKV0= +github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= +github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= +github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 h1:u8AQ9bPa9oC+8/A/jlWouakhIvkFfuxgIIRjiy8av7I= +github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573/go.mod h1:eBvb3i++NHDH4Ugo9qCvMw8t0mTSctaEa5blJbWcNxs= +github.com/go-resty/resty/v2 v2.0.0 h1:9Nq/U+V4xsoDnDa/iTrABDWUCuk3Ne92XFHPe6dKWUc= +github.com/go-resty/resty/v2 v2.0.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc= github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek= +github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ= +github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mymmrac/telego v0.29.1 h1:nsNnK0mS18OL+unoDjDI6BVfafJBbT8Wtj7rCzEWoM8= github.com/mymmrac/telego v0.29.1/go.mod h1:ZLD1+L2TQRr97NPOCoN1V2w8y9kmFov33OfZ3qT8cF4= +github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 h1:fiKJgB4JDUd43CApkmCeTSQlWjtTtABrU2qsgbuP0BI= +github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sashabaranov/go-openai v1.20.2 h1:nilzF2EKzaHyK4Rk2Dbu/aJEZbtIvskDIXvfS4yx+6M= github.com/sashabaranov/go-openai v1.20.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= +github.com/simplereach/timeutils v1.2.0 h1:btgOAlu9RW6de2r2qQiONhjgxdAG7BL6je0G6J/yPnA= +github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -49,15 +76,62 @@ github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7g github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc= golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/llm/llm.go b/llm/llm.go new file mode 100644 index 0000000..483a36e --- /dev/null +++ b/llm/llm.go @@ -0,0 +1,100 @@ +package llm + +import ( + "context" + "errors" + "github.com/sashabaranov/go-openai" + "log/slog" +) + +var ( + ErrLlmBackendRequestFailed = errors.New("llm back-end request failed") + ErrNoChoices = errors.New("no choices in LLM response") + + ModelMistralUncensored = "dolphin-mistral" +) + +type LlmConnector struct { + client *openai.Client +} + +func NewConnector(baseUrl string, token string) *LlmConnector { + config := openai.DefaultConfig(token) + config.BaseURL = baseUrl + + client := openai.NewClientWithConfig(config) + + return &LlmConnector{ + client: client, + } +} + +func (l *LlmConnector) HandleSingleRequest(text string, model string) (string, error) { + req := openai.ChatCompletionRequest{ + Model: model, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleSystem, + Content: "You're a bot in the Telegram chat. You are replying to questions directed to you.", + }, + }, + } + + req.Messages = append(req.Messages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: text, + }) + + resp, err := l.client.CreateChatCompletion(context.Background(), req) + if err != nil { + slog.Error("LLM back-end request failed", err) + + return "", ErrLlmBackendRequestFailed + } + + slog.Debug("Received LLM back-end response", resp) + + if len(resp.Choices) < 1 { + slog.Error("LLM back-end reply has no choices") + + return "", ErrNoChoices + } + + return resp.Choices[0].Message.Content, nil +} + +func (l *LlmConnector) Summarize(text string, model string) (string, error) { + req := openai.ChatCompletionRequest{ + Model: model, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleSystem, + Content: "You are a short digest editor. Summarize the text you received " + + "as a list of bullet points with most important facts from the text. " + + "If possible, use the same language as the original text.", + }, + }, + } + + req.Messages = append(req.Messages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: text, + }) + + resp, err := l.client.CreateChatCompletion(context.Background(), req) + if err != nil { + slog.Error("LLM back-end request failed", err) + + return "", ErrLlmBackendRequestFailed + } + + slog.Debug("Received LLM back-end response", resp) + + if len(resp.Choices) < 1 { + slog.Error("LLM back-end reply has no choices") + + return "", ErrNoChoices + } + + return resp.Choices[0].Message.Content, nil +} diff --git a/main.go b/main.go index b720c5d..b2f72a6 100644 --- a/main.go +++ b/main.go @@ -1,20 +1,14 @@ package main import ( - "context" "fmt" "log/slog" "os" - - openai "github.com/sashabaranov/go-openai" + "telegram-ollama-reply-bot/bot" + "telegram-ollama-reply-bot/extractor" + "telegram-ollama-reply-bot/llm" tg "github.com/mymmrac/telego" - tu "github.com/mymmrac/telego/telegoutil" -) - -const ( - ModelMistral = "mistral" - ModelMistralUncensored = "dolphin-mistral" ) func main() { @@ -23,68 +17,20 @@ func main() { telegramToken := os.Getenv("TELEGRAM_TOKEN") - config := openai.DefaultConfig(ollamaToken) - config.BaseURL = ollamaBaseUrl + llmc := llm.NewConnector(ollamaBaseUrl, ollamaToken) + ext := extractor.NewExtractor() - client := openai.NewClientWithConfig(config) - - bot, err := tg.NewBot(telegramToken, tg.WithDefaultLogger(false, true)) + telegramApi, err := tg.NewBot(telegramToken, tg.WithDefaultLogger(false, true)) if err != nil { fmt.Println(err) os.Exit(1) } - // Get updates channel - updates, _ := bot.UpdatesViaLongPolling(nil) + botService := bot.NewBot(telegramApi, llmc, ext) - // Stop reviving updates from update channel - defer bot.StopLongPolling() - - // Loop through all updates when they came - for update := range updates { - // Check if update contains a message - if update.Message != nil { - slog.Info("Update with message received", update.Message.Chat, update.Message.From, update.Message.Text) - - chatID := tu.ID(update.Message.Chat.ID) - - req := openai.ChatCompletionRequest{ - Model: ModelMistralUncensored, - Messages: []openai.ChatCompletionMessage{ - { - Role: openai.ChatMessageRoleSystem, - Content: "You're a bot in the Telegram chat. You are replying to questions directed to you.", - }, - }, - } - - req.Messages = append(req.Messages, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleUser, - Content: update.Message.Text, - }) - resp, err := client.CreateChatCompletion(context.Background(), req) - if err != nil { - slog.Error("ChatCompletion error", err) - - continue - } - - slog.Info("Got completion. Going to send.", resp.Choices[0]) - - message := tu.Message( - chatID, - resp.Choices[0].Message.Content, - ) - message = message.WithReplyParameters(&tg.ReplyParameters{MessageID: update.Message.MessageID}) - message = message.WithParseMode("Markdown") - - _, err = bot.SendMessage(message) - - if err != nil { - slog.Error("Can't send reply message", err) - } - - //req.Messages = append(req.Messages, resp.Choices[0].Message) - } + err = botService.Run() + if err != nil { + slog.Error("Running bot finished with an error", err) + os.Exit(1) } }