Matt Low
0384c7cb66
This refactor splits out all conversation concerns into a new `conversation` package. There is now a split between `conversation` and `api`s representation of `Message`, the latter storing the minimum information required for interaction with LLM providers. There is necessary conversation between the two when making LLM calls.
137 lines
3.2 KiB
Go
137 lines
3.2 KiB
Go
package chat
|
|
|
|
import (
|
|
"time"
|
|
|
|
"git.mlow.ca/mlow/lmcli/pkg/api"
|
|
"git.mlow.ca/mlow/lmcli/pkg/conversation"
|
|
"git.mlow.ca/mlow/lmcli/pkg/tui/model"
|
|
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
func (m *Model) waitForResponseChunk() tea.Cmd {
|
|
return func() tea.Msg {
|
|
return msgChatResponseChunk(<-m.chatReplyChunks)
|
|
}
|
|
}
|
|
|
|
func (m *Model) loadConversationMessages() tea.Cmd {
|
|
return func() tea.Msg {
|
|
messages, err := m.App.LoadConversationMessages()
|
|
if err != nil {
|
|
return shared.AsMsgError(err)
|
|
}
|
|
return msgConversationMessagesLoaded{messages}
|
|
}
|
|
}
|
|
|
|
func (m *Model) generateConversationTitle() tea.Cmd {
|
|
return func() tea.Msg {
|
|
title, err := m.App.GenerateConversationTitle(m.App.Messages)
|
|
if err != nil {
|
|
return shared.AsMsgError(err)
|
|
}
|
|
return msgConversationTitleGenerated(title)
|
|
}
|
|
}
|
|
|
|
func (m *Model) updateConversationTitle(conversation *conversation.Conversation) tea.Cmd {
|
|
return func() tea.Msg {
|
|
err := m.App.UpdateConversationTitle(conversation)
|
|
if err != nil {
|
|
return shared.WrapError(err)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (m *Model) cloneMessage(message conversation.Message, selected bool) tea.Cmd {
|
|
return func() tea.Msg {
|
|
msg, err := m.App.CloneMessage(message, selected)
|
|
if err != nil {
|
|
return shared.WrapError(err)
|
|
}
|
|
return msgMessageCloned(msg)
|
|
}
|
|
}
|
|
|
|
func (m *Model) updateMessageContent(message *conversation.Message) tea.Cmd {
|
|
return func() tea.Msg {
|
|
err := m.App.UpdateMessageContent(message)
|
|
if err != nil {
|
|
return shared.WrapError(err)
|
|
}
|
|
return msgMessageUpdated(message)
|
|
}
|
|
}
|
|
|
|
func (m *Model) cycleSelectedRoot(conv *conversation.Conversation, dir model.MessageCycleDirection) tea.Cmd {
|
|
if len(conv.RootMessages) < 2 {
|
|
return nil
|
|
}
|
|
|
|
return func() tea.Msg {
|
|
nextRoot, err := m.App.CycleSelectedRoot(conv, dir)
|
|
if err != nil {
|
|
return shared.WrapError(err)
|
|
}
|
|
return msgSelectedRootCycled(nextRoot)
|
|
}
|
|
}
|
|
|
|
func (m *Model) cycleSelectedReply(message *conversation.Message, dir model.MessageCycleDirection) tea.Cmd {
|
|
if len(message.Replies) < 2 {
|
|
return nil
|
|
}
|
|
|
|
return func() tea.Msg {
|
|
nextReply, err := m.App.CycleSelectedReply(message, dir)
|
|
if err != nil {
|
|
return shared.WrapError(err)
|
|
}
|
|
return msgSelectedReplyCycled(nextReply)
|
|
}
|
|
}
|
|
|
|
func (m *Model) persistConversation() tea.Cmd {
|
|
return func() tea.Msg {
|
|
conversation, messages, err := m.App.PersistConversation(m.App.Conversation, m.App.Messages)
|
|
if err != nil {
|
|
return shared.AsMsgError(err)
|
|
}
|
|
return msgConversationPersisted{conversation, messages}
|
|
}
|
|
}
|
|
|
|
func (m *Model) executeToolCalls(toolCalls []api.ToolCall) tea.Cmd {
|
|
return func() tea.Msg {
|
|
results, err := m.App.ExecuteToolCalls(toolCalls)
|
|
if err != nil {
|
|
return shared.AsMsgError(err)
|
|
}
|
|
return msgToolResults(results)
|
|
}
|
|
}
|
|
|
|
func (m *Model) promptLLM() tea.Cmd {
|
|
m.state = pendingResponse
|
|
m.spinner = getSpinner()
|
|
m.replyCursor.Blink = false
|
|
|
|
m.startTime = time.Now()
|
|
m.elapsed = 0
|
|
m.tokenCount = 0
|
|
|
|
return tea.Batch(
|
|
m.spinner.Tick,
|
|
func() tea.Msg {
|
|
resp, err := m.App.Prompt(m.App.Messages, m.chatReplyChunks, m.stopSignal)
|
|
if err != nil {
|
|
return msgChatResponseError{Err: err}
|
|
}
|
|
return msgChatResponse(resp)
|
|
},
|
|
)
|
|
}
|