Private
Public Access
1
0

Large refactor - it compiles!

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.
This commit is contained in:
2024-10-20 02:38:42 +00:00
parent 2ea8a73eb5
commit 0384c7cb66
33 changed files with 701 additions and 626 deletions

View File

@@ -4,7 +4,8 @@ import (
"time"
"git.mlow.ca/mlow/lmcli/pkg/api"
"git.mlow.ca/mlow/lmcli/pkg/api/provider"
"git.mlow.ca/mlow/lmcli/pkg/provider"
"git.mlow.ca/mlow/lmcli/pkg/conversation"
"git.mlow.ca/mlow/lmcli/pkg/tui/model"
"github.com/charmbracelet/bubbles/cursor"
"github.com/charmbracelet/bubbles/spinner"
@@ -20,14 +21,12 @@ type (
msgConversationTitleGenerated string
// sent when the conversation has been persisted, triggers a reload of contents
msgConversationPersisted struct {
isNew bool
conversation *api.Conversation
messages []api.Message
conversation *conversation.Conversation
messages []conversation.Message
}
// sent when a conversation's messages are laoded
msgConversationMessagesLoaded struct {
messages []api.Message
rootMessages []api.Message
messages []conversation.Message
}
// a special case of common.MsgError that stops the response waiting animation
msgChatResponseError struct {
@@ -36,19 +35,19 @@ type (
// sent on each chunk received from LLM
msgChatResponseChunk provider.Chunk
// sent on each completed reply
msgChatResponse *api.Message
msgChatResponse *conversation.Message
// sent when the response is canceled
msgChatResponseCanceled struct{}
// sent when results from a tool call are returned
msgToolResults []api.ToolResult
// sent when the given message is made the new selected reply of its parent
msgSelectedReplyCycled *api.Message
msgSelectedReplyCycled *conversation.Message
// sent when the given message is made the new selected root of the current conversation
msgSelectedRootCycled *api.Message
msgSelectedRootCycled *conversation.Message
// sent when a message's contents are updated and saved
msgMessageUpdated *api.Message
msgMessageUpdated *conversation.Message
// sent when a message is cloned, with the cloned message
msgMessageCloned *api.Message
msgMessageCloned *conversation.Message
)
type focusState int
@@ -84,7 +83,7 @@ type Model struct {
selectedMessage int
editorTarget editorTarget
stopSignal chan struct{}
replyChan chan api.Message
replyChan chan conversation.Message
chatReplyChunks chan provider.Chunk
persistence bool // whether we will save new messages in the conversation
@@ -137,7 +136,7 @@ func Chat(app *model.AppModel) *Model {
persistence: true,
stopSignal: make(chan struct{}),
replyChan: make(chan api.Message),
replyChan: make(chan conversation.Message),
chatReplyChunks: make(chan provider.Chunk),
wrap: true,

View File

@@ -4,6 +4,7 @@ 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"
@@ -21,13 +22,7 @@ func (m *Model) loadConversationMessages() tea.Cmd {
if err != nil {
return shared.AsMsgError(err)
}
rootMessages, err := m.App.LoadConversationRootMessages()
if err != nil {
return shared.AsMsgError(err)
}
return msgConversationMessagesLoaded{
messages, rootMessages,
}
return msgConversationMessagesLoaded{messages}
}
}
@@ -41,7 +36,7 @@ func (m *Model) generateConversationTitle() tea.Cmd {
}
}
func (m *Model) updateConversationTitle(conversation *api.Conversation) tea.Cmd {
func (m *Model) updateConversationTitle(conversation *conversation.Conversation) tea.Cmd {
return func() tea.Msg {
err := m.App.UpdateConversationTitle(conversation)
if err != nil {
@@ -51,7 +46,7 @@ func (m *Model) updateConversationTitle(conversation *api.Conversation) tea.Cmd
}
}
func (m *Model) cloneMessage(message api.Message, selected bool) tea.Cmd {
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 {
@@ -61,7 +56,7 @@ func (m *Model) cloneMessage(message api.Message, selected bool) tea.Cmd {
}
}
func (m *Model) updateMessageContent(message *api.Message) tea.Cmd {
func (m *Model) updateMessageContent(message *conversation.Message) tea.Cmd {
return func() tea.Msg {
err := m.App.UpdateMessageContent(message)
if err != nil {
@@ -71,14 +66,13 @@ func (m *Model) updateMessageContent(message *api.Message) tea.Cmd {
}
}
func (m *Model) cycleSelectedRoot(conv *api.Conversation, dir model.MessageCycleDirection) tea.Cmd {
if len(m.App.RootMessages) < 2 {
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, m.App.RootMessages, dir)
nextRoot, err := m.App.CycleSelectedRoot(conv, dir)
if err != nil {
return shared.WrapError(err)
}
@@ -86,7 +80,7 @@ func (m *Model) cycleSelectedRoot(conv *api.Conversation, dir model.MessageCycle
}
}
func (m *Model) cycleSelectedReply(message *api.Message, dir model.MessageCycleDirection) tea.Cmd {
func (m *Model) cycleSelectedReply(message *conversation.Message, dir model.MessageCycleDirection) tea.Cmd {
if len(message.Replies) < 2 {
return nil
}
@@ -106,7 +100,7 @@ func (m *Model) persistConversation() tea.Cmd {
if err != nil {
return shared.AsMsgError(err)
}
return msgConversationPersisted{conversation.ID == 0, conversation, messages}
return msgConversationPersisted{conversation, messages}
}
}

View File

@@ -5,6 +5,7 @@ import (
"strings"
"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"
tuiutil "git.mlow.ca/mlow/lmcli/pkg/tui/util"
@@ -70,12 +71,12 @@ func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd {
}
func (m *Model) scrollSelection(dir int) {
if m.selectedMessage + dir < 0 || m.selectedMessage + dir >= len(m.App.Messages) {
if m.selectedMessage+dir < 0 || m.selectedMessage+dir >= len(m.App.Messages) {
return
}
newIdx := m.selectedMessage
for i := newIdx + dir; i >= 0 && i < len(m.App.Messages); i += dir{
for i := newIdx + dir; i >= 0 && i < len(m.App.Messages); i += dir {
if !m.showDetails && m.App.Messages[i].Role.IsSystem() {
continue
}
@@ -175,7 +176,7 @@ func (m *Model) handleInputKey(msg tea.KeyMsg) tea.Cmd {
return shared.WrapError(fmt.Errorf("Can't reply to a user message"))
}
m.addMessage(api.Message{
m.addMessage(conversation.Message{
Role: api.MessageRoleUser,
Content: input,
})

View File

@@ -5,13 +5,14 @@ import (
"time"
"git.mlow.ca/mlow/lmcli/pkg/api"
"git.mlow.ca/mlow/lmcli/pkg/conversation"
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
tuiutil "git.mlow.ca/mlow/lmcli/pkg/tui/util"
"github.com/charmbracelet/bubbles/cursor"
tea "github.com/charmbracelet/bubbletea"
)
func (m *Model) setMessage(i int, msg api.Message) {
func (m *Model) setMessage(i int, msg conversation.Message) {
if i >= len(m.App.Messages) {
panic("i out of range")
}
@@ -19,7 +20,7 @@ func (m *Model) setMessage(i int, msg api.Message) {
m.messageCache[i] = m.renderMessage(i)
}
func (m *Model) addMessage(msg api.Message) {
func (m *Model) addMessage(msg conversation.Message) {
m.App.Messages = append(m.App.Messages, msg)
m.messageCache = append(m.messageCache, m.renderMessage(len(m.App.Messages)-1))
}
@@ -95,7 +96,6 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
}
}
case msgConversationMessagesLoaded:
m.App.RootMessages = msg.rootMessages
m.App.Messages = msg.messages
if m.selectedMessage == -1 {
m.selectedMessage = len(msg.messages) - 1
@@ -117,7 +117,7 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
m.setMessageContents(last, m.App.Messages[last].Content+msg.Content)
} else {
// use chunk in a new message
m.addMessage(api.Message{
m.addMessage(conversation.Message{
Role: api.MessageRoleAssistant,
Content: msg.Content,
})
@@ -133,7 +133,7 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
case msgChatResponse:
m.state = idle
reply := (*api.Message)(msg)
reply := (*conversation.Message)(msg)
reply.Content = strings.TrimSpace(reply.Content)
last := len(m.App.Messages) - 1
@@ -181,9 +181,9 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
panic("Previous message not a tool call, unexpected")
}
m.addMessage(api.Message{
m.addMessage(conversation.Message{
Role: api.MessageRoleToolResult,
ToolResults: api.ToolResults(msg),
ToolResults: conversation.ToolResults(msg),
})
if m.persistence {
@@ -207,15 +207,11 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
case msgConversationPersisted:
m.App.Conversation = msg.conversation
m.App.Messages = msg.messages
if msg.isNew {
m.App.RootMessages = []api.Message{m.App.Messages[0]}
}
m.rebuildMessageCache()
m.updateContent()
case msgMessageCloned:
if msg.Parent == nil {
m.App.Conversation = msg.Conversation
m.App.RootMessages = append(m.App.RootMessages, *msg)
}
cmds = append(cmds, m.loadConversationMessages())
case msgSelectedRootCycled, msgSelectedReplyCycled, msgMessageUpdated:

View File

@@ -6,6 +6,7 @@ import (
"strings"
"git.mlow.ca/mlow/lmcli/pkg/api"
"git.mlow.ca/mlow/lmcli/pkg/conversation"
"git.mlow.ca/mlow/lmcli/pkg/tui/styles"
tuiutil "git.mlow.ca/mlow/lmcli/pkg/tui/util"
"github.com/charmbracelet/lipgloss"
@@ -44,7 +45,7 @@ var (
footerStyle = lipgloss.NewStyle().Padding(0, 1)
)
func (m *Model) renderMessageHeading(i int, message *api.Message) string {
func (m *Model) renderMessageHeading(i int, message *conversation.Message) string {
friendly := message.Role.FriendlyRole()
style := systemStyle
@@ -70,15 +71,15 @@ func (m *Model) renderMessageHeading(i int, message *api.Message) string {
prefix = " "
}
if i == 0 && len(m.App.RootMessages) > 1 && m.App.Conversation.SelectedRootID != nil {
if i == 0 && len(m.App.Conversation.RootMessages) > 1 && m.App.Conversation.SelectedRootID != nil {
selectedRootIndex := 0
for j, reply := range m.App.RootMessages {
for j, reply := range m.App.Conversation.RootMessages {
if reply.ID == *m.App.Conversation.SelectedRootID {
selectedRootIndex = j
break
}
}
suffix += faintStyle.Render(fmt.Sprintf(" <%d/%d>", selectedRootIndex+1, len(m.App.RootMessages)))
suffix += faintStyle.Render(fmt.Sprintf(" <%d/%d>", selectedRootIndex+1, len(m.App.Conversation.RootMessages)))
}
if i > 0 && len(m.App.Messages[i-1].Replies) > 1 {
// Find the selected reply index
@@ -230,9 +231,9 @@ func (m *Model) conversationMessagesView() string {
// Render a placeholder for the incoming assistant reply
if m.state == pendingResponse && m.App.Messages[len(m.App.Messages)-1].Role != api.MessageRoleAssistant {
heading := m.renderMessageHeading(-1, &api.Message{
heading := m.renderMessageHeading(-1, &conversation.Message{
Role: api.MessageRoleAssistant,
Metadata: api.MessageMeta{
Metadata: conversation.MessageMeta{
GenerationModel: &m.App.Model,
},
})