lmcli/pkg/tui/views/chat/conversation.go

300 lines
7.9 KiB
Go
Raw Normal View History

2024-06-02 16:40:46 -06:00
package chat
import (
"context"
"errors"
2024-06-02 16:40:46 -06:00
"fmt"
"time"
"git.mlow.ca/mlow/lmcli/pkg/agent"
"git.mlow.ca/mlow/lmcli/pkg/api"
2024-06-02 16:40:46 -06:00
cmdutil "git.mlow.ca/mlow/lmcli/pkg/cmd/util"
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
tea "github.com/charmbracelet/bubbletea"
)
func (m *Model) setMessage(i int, msg api.Message) {
2024-06-02 16:40:46 -06:00
if i >= len(m.messages) {
panic("i out of range")
}
m.messages[i] = msg
m.messageCache[i] = m.renderMessage(i)
}
func (m *Model) addMessage(msg api.Message) {
2024-06-02 16:40:46 -06:00
m.messages = append(m.messages, msg)
m.messageCache = append(m.messageCache, m.renderMessage(len(m.messages)-1))
}
func (m *Model) setMessageContents(i int, content string) {
if i >= len(m.messages) {
panic("i out of range")
}
m.messages[i].Content = content
m.messageCache[i] = m.renderMessage(i)
}
func (m *Model) rebuildMessageCache() {
m.messageCache = make([]string, len(m.messages))
for i := range m.messages {
m.messageCache[i] = m.renderMessage(i)
}
}
func (m *Model) updateContent() {
atBottom := m.content.AtBottom()
m.content.SetContent(m.conversationMessagesView())
if atBottom {
// if we were at bottom before the update, scroll with the output
m.content.GotoBottom()
}
}
func (m *Model) loadConversation(shortname string) tea.Cmd {
return func() tea.Msg {
if shortname == "" {
return nil
}
2024-06-08 16:01:16 -06:00
c, err := m.Shared.Ctx.Store.ConversationByShortName(shortname)
2024-06-02 16:40:46 -06:00
if err != nil {
return shared.MsgError(fmt.Errorf("Could not lookup conversation: %v", err))
}
if c.ID == 0 {
return shared.MsgError(fmt.Errorf("Conversation not found: %s", shortname))
}
2024-06-08 16:01:16 -06:00
rootMessages, err := m.Shared.Ctx.Store.RootMessages(c.ID)
if err != nil {
return shared.MsgError(fmt.Errorf("Could not load conversation root messages: %v\n", err))
}
return msgConversationLoaded{c, rootMessages}
2024-06-02 16:40:46 -06:00
}
}
func (m *Model) loadConversationMessages() tea.Cmd {
2024-06-02 16:40:46 -06:00
return func() tea.Msg {
2024-06-08 16:01:16 -06:00
messages, err := m.Shared.Ctx.Store.PathToLeaf(m.conversation.SelectedRoot)
2024-06-02 16:40:46 -06:00
if err != nil {
return shared.MsgError(fmt.Errorf("Could not load conversation messages: %v\n", err))
}
return msgMessagesLoaded(messages)
}
}
func (m *Model) generateConversationTitle() tea.Cmd {
return func() tea.Msg {
2024-06-08 16:01:16 -06:00
title, err := cmdutil.GenerateTitle(m.Shared.Ctx, m.messages)
2024-06-02 16:40:46 -06:00
if err != nil {
return shared.MsgError(err)
}
return msgConversationTitleGenerated(title)
}
}
func (m *Model) updateConversationTitle(conversation *api.Conversation) tea.Cmd {
return func() tea.Msg {
2024-06-08 16:01:16 -06:00
err := m.Shared.Ctx.Store.UpdateConversation(conversation)
if err != nil {
return shared.WrapError(err)
}
return nil
2024-06-02 16:40:46 -06:00
}
}
// Clones the given message (and its descendents). If selected is true, updates
// either its parent's SelectedReply or its conversation's SelectedRoot to
// point to the new clone
func (m *Model) cloneMessage(message api.Message, selected bool) tea.Cmd {
return func() tea.Msg {
msg, _, err := m.Ctx.Store.CloneBranch(message)
if err != nil {
return shared.WrapError(fmt.Errorf("Could not clone message: %v", err))
}
if selected {
if msg.Parent == nil {
msg.Conversation.SelectedRoot = msg
err = m.Shared.Ctx.Store.UpdateConversation(msg.Conversation)
} else {
msg.Parent.SelectedReply = msg
2024-06-08 16:01:16 -06:00
err = m.Shared.Ctx.Store.UpdateMessage(msg.Parent)
}
if err != nil {
return shared.WrapError(fmt.Errorf("Could not update selected message: %v", err))
}
}
return msgMessageCloned(msg)
}
}
func (m *Model) updateMessageContent(message *api.Message) tea.Cmd {
return func() tea.Msg {
2024-06-08 16:01:16 -06:00
err := m.Shared.Ctx.Store.UpdateMessage(message)
if err != nil {
return shared.WrapError(fmt.Errorf("Could not update message: %v", err))
}
return msgMessageUpdated(message)
}
}
func cycleSelectedMessage(selected *api.Message, choices []api.Message, dir MessageCycleDirection) (*api.Message, error) {
2024-06-02 16:40:46 -06:00
currentIndex := -1
for i, reply := range choices {
if reply.ID == selected.ID {
2024-06-02 16:40:46 -06:00
currentIndex = i
break
}
}
if currentIndex < 0 {
// this should probably be an assert
return nil, fmt.Errorf("Selected message %d not found in choices, this is a bug", selected.ID)
2024-06-02 16:40:46 -06:00
}
var next int
if dir == CyclePrev {
// Wrap around to the last reply if at the beginning
next = (currentIndex - 1 + len(choices)) % len(choices)
2024-06-02 16:40:46 -06:00
} else {
// Wrap around to the first reply if at the end
next = (currentIndex + 1) % len(choices)
2024-06-02 16:40:46 -06:00
}
return &choices[next], nil
2024-06-02 16:40:46 -06:00
}
func (m *Model) cycleSelectedRoot(conv *api.Conversation, dir MessageCycleDirection) tea.Cmd {
2024-06-02 16:40:46 -06:00
if len(m.rootMessages) < 2 {
return nil
2024-06-02 16:40:46 -06:00
}
return func() tea.Msg {
nextRoot, err := cycleSelectedMessage(conv.SelectedRoot, m.rootMessages, dir)
if err != nil {
return shared.WrapError(err)
}
2024-06-02 16:40:46 -06:00
conv.SelectedRoot = nextRoot
2024-06-08 16:01:16 -06:00
err = m.Shared.Ctx.Store.UpdateConversation(conv)
if err != nil {
return shared.WrapError(fmt.Errorf("Could not update conversation SelectedRoot: %v", err))
}
return msgSelectedRootCycled(nextRoot)
2024-06-02 16:40:46 -06:00
}
}
func (m *Model) cycleSelectedReply(message *api.Message, dir MessageCycleDirection) tea.Cmd {
2024-06-02 16:40:46 -06:00
if len(message.Replies) < 2 {
return nil
2024-06-02 16:40:46 -06:00
}
return func() tea.Msg {
nextReply, err := cycleSelectedMessage(message.SelectedReply, message.Replies, dir)
if err != nil {
return shared.WrapError(err)
}
2024-06-02 16:40:46 -06:00
message.SelectedReply = nextReply
2024-06-08 16:01:16 -06:00
err = m.Shared.Ctx.Store.UpdateMessage(message)
if err != nil {
return shared.WrapError(fmt.Errorf("Could not update message SelectedReply: %v", err))
}
return msgSelectedReplyCycled(nextReply)
2024-06-02 16:40:46 -06:00
}
}
func (m *Model) persistConversation() tea.Cmd {
conversation := m.conversation
messages := m.messages
var err error
if conversation.ID == 0 {
return func() tea.Msg {
// Start a new conversation with all messages so far
2024-06-08 16:01:16 -06:00
conversation, messages, err = m.Shared.Ctx.Store.StartConversation(messages...)
if err != nil {
return shared.MsgError(fmt.Errorf("Could not start new conversation: %v", err))
}
return msgConversationPersisted{true, conversation, messages}
2024-06-02 16:40:46 -06:00
}
}
return func() tea.Msg {
// else, we'll handle updating an existing conversation's messages
for i := range messages {
if messages[i].ID > 0 {
// message has an ID, update it
2024-06-08 16:01:16 -06:00
err := m.Shared.Ctx.Store.UpdateMessage(&messages[i])
if err != nil {
return shared.MsgError(err)
}
} else if i > 0 {
// messages is new, so add it as a reply to previous message
2024-06-08 16:01:16 -06:00
saved, err := m.Shared.Ctx.Store.Reply(&messages[i-1], messages[i])
if err != nil {
return shared.MsgError(err)
}
messages[i] = saved[0]
} else {
// message has no id and no previous messages to add it to
// this shouldn't happen?
return fmt.Errorf("Error: no messages to reply to")
2024-06-02 16:40:46 -06:00
}
}
return msgConversationPersisted{false, conversation, messages}
2024-06-02 16:40:46 -06:00
}
}
func (m *Model) executeToolCalls(toolCalls []api.ToolCall) tea.Cmd {
return func() tea.Msg {
results, err := agent.ExecuteToolCalls(toolCalls, m.Ctx.EnabledTools)
if err != nil {
return shared.MsgError(err)
}
return msgToolResults(results)
}
}
2024-06-02 16:40:46 -06:00
func (m *Model) promptLLM() tea.Cmd {
m.state = pendingResponse
2024-06-02 16:40:46 -06:00
m.replyCursor.Blink = false
m.startTime = time.Now()
m.elapsed = 0
m.tokenCount = 0
2024-06-02 16:40:46 -06:00
return func() tea.Msg {
2024-06-08 16:01:16 -06:00
model, provider, err := m.Shared.Ctx.GetModelProvider(*m.Shared.Ctx.Config.Defaults.Model)
2024-06-02 16:40:46 -06:00
if err != nil {
return shared.MsgError(err)
}
requestParams := api.RequestParameters{
2024-06-02 16:40:46 -06:00
Model: model,
2024-06-08 16:01:16 -06:00
MaxTokens: *m.Shared.Ctx.Config.Defaults.MaxTokens,
Temperature: *m.Shared.Ctx.Config.Defaults.Temperature,
ToolBag: m.Shared.Ctx.EnabledTools,
2024-06-02 16:40:46 -06:00
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
select {
case <-m.stopSignal:
cancel()
}
}()
resp, err := provider.CreateChatCompletionStream(
ctx, requestParams, m.messages, m.chatReplyChunks,
2024-06-02 16:40:46 -06:00
)
if errors.Is(err, context.Canceled) {
return msgChatResponseCanceled(struct{}{})
}
if err != nil {
return msgChatResponseError(err)
2024-06-02 16:40:46 -06:00
}
return msgChatResponse(resp)
2024-06-02 16:40:46 -06:00
}
}