Compare commits

..

4 commits

Author SHA1 Message Date
Alexey Skobkin c2a30464d1 Merge pull request 'Chat history and context improvements' (#35) from fix_quoted_messages_usernames_context into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #35
2024-11-03 22:28:39 +00:00
Alexey Skobkin 541c1d3bbf
Storing /summarize replies in history too (#32).
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-11-04 01:24:40 +03:00
Alexey Skobkin a783a84faa
Renaming MessageRingBuffer to MessageHistory (#30). 2024-11-04 01:18:50 +03:00
Alexey Skobkin 093372dd91
Adding usernames to the message context (#34). Adding text of reply-quoted message to the context (#33). Refactoring context creation and presentation. 2024-11-04 01:18:42 +03:00
5 changed files with 161 additions and 68 deletions

View file

@ -30,7 +30,7 @@ type Bot struct {
extractor *extractor.Extractor extractor *extractor.Extractor
stats *stats.Stats stats *stats.Stats
models ModelSelection models ModelSelection
history map[int64]*MessageRingBuffer history map[int64]*MessageHistory
profile BotInfo profile BotInfo
markdownV1Replacer *strings.Replacer markdownV1Replacer *strings.Replacer
@ -48,7 +48,7 @@ func NewBot(
extractor: extractor, extractor: extractor,
stats: stats.NewStats(), stats: stats.NewStats(),
models: models, models: models,
history: make(map[int64]*MessageRingBuffer), history: make(map[int64]*MessageHistory),
profile: BotInfo{0, "", ""}, profile: BotInfo{0, "", ""},
markdownV1Replacer: strings.NewReplacer( markdownV1Replacer: strings.NewReplacer(
@ -129,7 +129,7 @@ func (b *Bot) textMessageHandler(bot *telego.Bot, update telego.Update) {
slog.Info("/any-message", "type", "private") slog.Info("/any-message", "type", "private")
b.processMention(message) b.processMention(message)
default: default:
slog.Debug("/any-message", "info", "Message is not mention, reply or private chat. Skipping.") slog.Debug("/any-message", "info", "MessageData is not mention, reply or private chat. Skipping.")
} }
} }
@ -144,7 +144,13 @@ func (b *Bot) processMention(message *telego.Message) {
requestContext := b.createLlmRequestContextFromMessage(message) requestContext := b.createLlmRequestContextFromMessage(message)
llmReply, err := b.llm.HandleChatMessage(message.Text, b.models.TextRequestModel, requestContext) userMessageData := tgUserMessageToMessageData(message)
llmReply, err := b.llm.HandleChatMessage(
messageDataToLlmMessage(userMessageData),
b.models.TextRequestModel,
requestContext,
)
if err != nil { if err != nil {
slog.Error("Cannot get reply from LLM connector") slog.Error("Cannot get reply from LLM connector")
@ -227,9 +233,11 @@ func (b *Bot) summarizeHandler(bot *telego.Bot, update telego.Update) {
slog.Debug("Got completion. Going to send.", "llm-completion", llmReply) slog.Debug("Got completion. Going to send.", "llm-completion", llmReply)
replyMarkdown := b.escapeMarkdownV1Symbols(llmReply)
message := tu.Message( message := tu.Message(
chatID, chatID,
b.escapeMarkdownV1Symbols(llmReply), replyMarkdown,
).WithParseMode("Markdown") ).WithParseMode("Markdown")
_, err = bot.SendMessage(b.reply(update.Message, message)) _, err = bot.SendMessage(b.reply(update.Message, message))
@ -239,6 +247,8 @@ func (b *Bot) summarizeHandler(bot *telego.Bot, update telego.Update) {
b.trySendReplyError(update.Message) b.trySendReplyError(update.Message)
} }
b.saveBotReplyToHistory(update.Message, replyMarkdown)
} }
func (b *Bot) helpHandler(bot *telego.Bot, update telego.Update) { func (b *Bot) helpHandler(bot *telego.Bot, update telego.Update) {

View file

@ -7,25 +7,28 @@ import (
const HistoryLength = 150 const HistoryLength = 150
type Message struct { type MessageData struct {
Name string Name string
Username string
Text string Text string
IsMe bool IsMe bool
IsUserRequest bool
ReplyTo *MessageData
} }
type MessageRingBuffer struct { type MessageHistory struct {
messages []Message messages []MessageData
capacity int capacity int
} }
func NewMessageBuffer(capacity int) *MessageRingBuffer { func NewMessageHistory(capacity int) *MessageHistory {
return &MessageRingBuffer{ return &MessageHistory{
messages: make([]Message, 0, capacity), messages: make([]MessageData, 0, capacity),
capacity: capacity, capacity: capacity,
} }
} }
func (b *MessageRingBuffer) Push(element Message) { func (b *MessageHistory) Push(element MessageData) {
if len(b.messages) >= b.capacity { if len(b.messages) >= b.capacity {
b.messages = b.messages[1:] b.messages = b.messages[1:]
} }
@ -33,7 +36,7 @@ func (b *MessageRingBuffer) Push(element Message) {
b.messages = append(b.messages, element) b.messages = append(b.messages, element)
} }
func (b *MessageRingBuffer) GetAll() []Message { func (b *MessageHistory) GetAll() []MessageData {
return b.messages return b.messages
} }
@ -50,43 +53,72 @@ func (b *Bot) saveChatMessageToHistory(message *telego.Message) {
_, ok := b.history[chatId] _, ok := b.history[chatId]
if !ok { if !ok {
b.history[chatId] = NewMessageBuffer(HistoryLength) b.history[chatId] = NewMessageHistory(HistoryLength)
} }
b.history[chatId].Push(Message{ msgData := tgUserMessageToMessageData(message)
Name: message.From.FirstName,
Text: message.Text, b.history[chatId].Push(msgData)
IsMe: false,
})
} }
func (b *Bot) saveBotReplyToHistory(message *telego.Message, reply string) { func (b *Bot) saveBotReplyToHistory(replyTo *telego.Message, text string) {
chatId := message.Chat.ID chatId := replyTo.Chat.ID
slog.Info( slog.Info(
"history-reply-save", "history-reply-save",
"chat", chatId, "chat", chatId,
"to_id", message.From.ID, "to_id", replyTo.From.ID,
"to_name", message.From.FirstName, "to_name", replyTo.From.FirstName,
"text", reply, "text", text,
) )
_, ok := b.history[chatId] _, ok := b.history[chatId]
if !ok { if !ok {
b.history[chatId] = NewMessageBuffer(HistoryLength) b.history[chatId] = NewMessageHistory(HistoryLength)
} }
b.history[chatId].Push(Message{ msgData := MessageData{
Name: b.profile.Username, Name: b.profile.Name,
Text: reply, Username: b.profile.Username,
Text: text,
IsMe: true, IsMe: true,
})
} }
func (b *Bot) getChatHistory(chatId int64) []Message { if replyTo.ReplyToMessage != nil {
replyMessage := replyTo.ReplyToMessage
msgData.ReplyTo = &MessageData{
Name: replyMessage.From.FirstName,
Username: replyMessage.From.Username,
Text: replyMessage.Text,
IsMe: false,
ReplyTo: nil,
}
}
b.history[chatId].Push(msgData)
}
func tgUserMessageToMessageData(message *telego.Message) MessageData {
msgData := MessageData{
Name: message.From.FirstName,
Username: message.From.Username,
Text: message.Text,
IsMe: false,
}
if message.ReplyToMessage != nil {
replyData := tgUserMessageToMessageData(message.ReplyToMessage)
msgData.ReplyTo = &replyData
}
return msgData
}
func (b *Bot) getChatHistory(chatId int64) []MessageData {
_, ok := b.history[chatId] _, ok := b.history[chatId]
if !ok { if !ok {
return make([]Message, 0) return make([]MessageData, 0)
} }
return b.history[chatId].GetAll() return b.history[chatId].GetAll()

View file

@ -48,17 +48,14 @@ func (b *Bot) createLlmRequestContextFromMessage(message *telego.Message) llm.Re
return rc return rc
} }
func historyToLlmMessages(history []Message) []llm.ChatMessage { func historyToLlmMessages(history []MessageData) []llm.ChatMessage {
length := len(history) length := len(history)
if length > 0 { if length > 0 {
result := make([]llm.ChatMessage, 0, length) result := make([]llm.ChatMessage, 0, length)
for _, msg := range history { for _, msg := range history {
result = append(result, llm.ChatMessage{ result = append(result, messageDataToLlmMessage(msg))
Name: msg.Name,
Text: msg.Text,
})
} }
return result return result
@ -66,3 +63,20 @@ func historyToLlmMessages(history []Message) []llm.ChatMessage {
return make([]llm.ChatMessage, 0) return make([]llm.ChatMessage, 0)
} }
func messageDataToLlmMessage(data MessageData) llm.ChatMessage {
llmMessage := llm.ChatMessage{
Name: data.Name,
Username: data.Username,
Text: data.Text,
IsMe: data.IsMe,
IsUserRequest: data.IsUserRequest,
}
if data.ReplyTo != nil {
replyMessage := messageDataToLlmMessage(*data.ReplyTo)
llmMessage.ReplyTo = &replyMessage
}
return llmMessage
}

View file

@ -6,7 +6,6 @@ import (
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
"log/slog" "log/slog"
"strconv" "strconv"
"strings"
) )
var ( var (
@ -29,7 +28,7 @@ func NewConnector(baseUrl string, token string) *LlmConnector {
} }
} }
func (l *LlmConnector) HandleChatMessage(text string, model string, requestContext RequestContext) (string, error) { func (l *LlmConnector) HandleChatMessage(userMessage ChatMessage, model string, requestContext RequestContext) (string, error) {
systemPrompt := "You're a bot in the Telegram chat.\n" + systemPrompt := "You're a bot in the Telegram chat.\n" +
"You're using a free model called \"" + model + "\".\n\n" + "You're using a free model called \"" + model + "\".\n\n" +
requestContext.Prompt() requestContext.Prompt()
@ -52,28 +51,11 @@ func (l *LlmConnector) HandleChatMessage(text string, model string, requestConte
if historyLength > 0 { if historyLength > 0 {
for _, msg := range requestContext.Chat.History { for _, msg := range requestContext.Chat.History {
var msgRole string req.Messages = append(req.Messages, chatMessageToOpenAiChatCompletionMessage(msg))
var msgText string
if msg.IsMe {
msgRole = openai.ChatMessageRoleAssistant
msgText = msg.Text
} else {
msgRole = openai.ChatMessageRoleSystem
msgText = "User " + msg.Name + " said:\n" + msg.Text
}
req.Messages = append(req.Messages, openai.ChatCompletionMessage{
Role: msgRole,
Content: msgText,
})
} }
} }
req.Messages = append(req.Messages, openai.ChatCompletionMessage{ req.Messages = append(req.Messages, chatMessageToOpenAiChatCompletionMessage(userMessage))
Role: openai.ChatMessageRoleUser,
Content: text,
})
resp, err := l.client.CreateChatCompletion(context.Background(), req) resp, err := l.client.CreateChatCompletion(context.Background(), req)
if err != nil { if err != nil {
@ -164,7 +146,3 @@ func (l *LlmConnector) HasModel(id string) bool {
return false return false
} }
func quoteMessage(text string) string {
return "> " + strings.ReplaceAll(text, "\n", "\n> ")
}

View file

@ -1,5 +1,10 @@
package llm package llm
import (
"github.com/sashabaranov/go-openai"
"strings"
)
type RequestContext struct { type RequestContext struct {
Empty bool Empty bool
User UserContext User UserContext
@ -22,8 +27,11 @@ type ChatContext struct {
type ChatMessage struct { type ChatMessage struct {
Name string Name string
Username string
Text string Text string
IsMe bool IsMe bool
IsUserRequest bool
ReplyTo *ChatMessage
} }
func (c RequestContext) Prompt() string { func (c RequestContext) Prompt() string {
@ -62,3 +70,54 @@ func (c RequestContext) Prompt() string {
return prompt return prompt
} }
func chatMessageToOpenAiChatCompletionMessage(message ChatMessage) openai.ChatCompletionMessage {
var msgRole string
var msgText string
switch {
case message.IsMe:
msgRole = openai.ChatMessageRoleAssistant
case message.IsUserRequest:
msgRole = openai.ChatMessageRoleUser
default:
msgRole = openai.ChatMessageRoleSystem
}
if message.IsMe {
msgText = message.Text
} else {
msgText = chatMessageToText(message)
}
return openai.ChatCompletionMessage{
Role: msgRole,
Content: msgText,
}
}
func chatMessageToText(message ChatMessage) string {
var msgText string
if message.ReplyTo == nil {
msgText += "In reply to:"
msgText += quoteText(presentUserMessageAsText(*message.ReplyTo)) + "\n\n"
}
msgText += presentUserMessageAsText(message)
return msgText
}
func presentUserMessageAsText(message ChatMessage) string {
result := message.Name
if message.Username != "" {
result += " (@" + message.Username + ")"
}
result += " wrote:\n" + message.Text
return result
}
func quoteText(text string) string {
return "> " + strings.ReplaceAll(text, "\n", "\n> ")
}