Chat history and context improvements #35
20
bot/bot.go
20
bot/bot.go
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
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 (b *Bot) getChatHistory(chatId int64) []Message {
|
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()
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
28
llm/llm.go
28
llm/llm.go
|
@ -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> ")
|
|
||||||
}
|
|
||||||
|
|
|
@ -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> ")
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue