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

@@ -54,7 +54,7 @@ func ChatCmd(ctx *lmcli.Context) *cobra.Command {
if len(args) != 0 {
return nil, compMode
}
return ctx.Store.ConversationShortNameCompletions(toComplete), compMode
return ctx.Conversations.ConversationShortNameCompletions(toComplete), compMode
},
}

View File

@@ -27,7 +27,7 @@ func CloneCmd(ctx *lmcli.Context) *cobra.Command {
return err
}
clone, messageCnt, err := ctx.Store.CloneConversation(*toClone)
clone, messageCnt, err := ctx.Conversations.CloneConversation(*toClone)
if err != nil {
return fmt.Errorf("Failed to clone conversation: %v", err)
}
@@ -40,7 +40,7 @@ func CloneCmd(ctx *lmcli.Context) *cobra.Command {
if len(args) != 0 {
return nil, compMode
}
return ctx.Store.ConversationShortNameCompletions(toComplete), compMode
return ctx.Conversations.ConversationShortNameCompletions(toComplete), compMode
},
}
return cmd

View File

@@ -29,9 +29,9 @@ func ContinueCmd(ctx *lmcli.Context) *cobra.Command {
}
shortName := args[0]
conversation := cmdutil.LookupConversation(ctx, shortName)
c := cmdutil.LookupConversation(ctx, shortName)
messages, err := ctx.Store.PathToLeaf(conversation.SelectedRoot)
messages, err := ctx.Conversations.PathToLeaf(c.SelectedRoot)
if err != nil {
return fmt.Errorf("could not retrieve conversation messages: %v", err)
}
@@ -58,7 +58,7 @@ func ContinueCmd(ctx *lmcli.Context) *cobra.Command {
lastMessage.Content += strings.TrimRight(continuedOutput.Content, "\n\t ")
// Update the original message
err = ctx.Store.UpdateMessage(lastMessage)
err = ctx.Conversations.UpdateMessage(lastMessage)
if err != nil {
return fmt.Errorf("could not update the last message: %v", err)
}
@@ -70,7 +70,7 @@ func ContinueCmd(ctx *lmcli.Context) *cobra.Command {
if len(args) != 0 {
return nil, compMode
}
return ctx.Store.ConversationShortNameCompletions(toComplete), compMode
return ctx.Conversations.ConversationShortNameCompletions(toComplete), compMode
},
}
applyGenerationFlags(ctx, cmd)

View File

@@ -22,11 +22,11 @@ func EditCmd(ctx *lmcli.Context) *cobra.Command {
},
RunE: func(cmd *cobra.Command, args []string) error {
shortName := args[0]
conversation := cmdutil.LookupConversation(ctx, shortName)
c := cmdutil.LookupConversation(ctx, shortName)
messages, err := ctx.Store.PathToLeaf(conversation.SelectedRoot)
messages, err := ctx.Conversations.PathToLeaf(c.SelectedRoot)
if err != nil {
return fmt.Errorf("Could not retrieve messages for conversation: %s", conversation.Title)
return fmt.Errorf("Could not retrieve messages for conversation: %s", c.Title)
}
offset, _ := cmd.Flags().GetInt("offset")
@@ -62,11 +62,11 @@ func EditCmd(ctx *lmcli.Context) *cobra.Command {
// Update the message in-place
inplace, _ := cmd.Flags().GetBool("in-place")
if inplace {
return ctx.Store.UpdateMessage(&toEdit)
return ctx.Conversations.UpdateMessage(&toEdit)
}
// Otherwise, create a branch for the edited message
message, _, err := ctx.Store.CloneBranch(toEdit)
message, _, err := ctx.Conversations.CloneBranch(toEdit)
if err != nil {
return err
}
@@ -74,11 +74,11 @@ func EditCmd(ctx *lmcli.Context) *cobra.Command {
if desiredIdx > 0 {
// update selected reply
messages[desiredIdx-1].SelectedReply = message
err = ctx.Store.UpdateMessage(&messages[desiredIdx-1])
err = ctx.Conversations.UpdateMessage(&messages[desiredIdx-1])
} else {
// update selected root
conversation.SelectedRoot = message
err = ctx.Store.UpdateConversation(conversation)
c.SelectedRoot = message
err = ctx.Conversations.UpdateConversation(c)
}
return err
},
@@ -87,7 +87,7 @@ func EditCmd(ctx *lmcli.Context) *cobra.Command {
if len(args) != 0 {
return nil, compMode
}
return ctx.Store.ConversationShortNameCompletions(toComplete), compMode
return ctx.Conversations.ConversationShortNameCompletions(toComplete), compMode
},
}

View File

@@ -20,7 +20,7 @@ func ListCmd(ctx *lmcli.Context) *cobra.Command {
Short: "List conversations",
Long: `List conversations in order of recent activity`,
RunE: func(cmd *cobra.Command, args []string) error {
messages, err := ctx.Store.LatestConversationMessages()
messages, err := ctx.Conversations.LatestConversationMessages()
if err != nil {
return fmt.Errorf("Could not fetch conversations: %v", err)
}

View File

@@ -5,6 +5,7 @@ import (
"git.mlow.ca/mlow/lmcli/pkg/api"
cmdutil "git.mlow.ca/mlow/lmcli/pkg/cmd/util"
"git.mlow.ca/mlow/lmcli/pkg/conversation"
"git.mlow.ca/mlow/lmcli/pkg/lmcli"
"github.com/spf13/cobra"
)
@@ -25,12 +26,12 @@ func NewCmd(ctx *lmcli.Context) *cobra.Command {
return fmt.Errorf("No message was provided.")
}
messages := []api.Message{{
messages := []conversation.Message{{
Role: api.MessageRoleUser,
Content: input,
}}
conversation, messages, err := ctx.Store.StartConversation(messages...)
conversation, messages, err := ctx.Conversations.StartConversation(messages...)
if err != nil {
return fmt.Errorf("Could not start a new conversation: %v", err)
}
@@ -43,7 +44,7 @@ func NewCmd(ctx *lmcli.Context) *cobra.Command {
}
conversation.Title = title
err = ctx.Store.UpdateConversation(conversation)
err = ctx.Conversations.UpdateConversation(conversation)
if err != nil {
lmcli.Warn("Could not save conversation title: %v\n", err)
}

View File

@@ -5,6 +5,7 @@ import (
"git.mlow.ca/mlow/lmcli/pkg/api"
cmdutil "git.mlow.ca/mlow/lmcli/pkg/cmd/util"
"git.mlow.ca/mlow/lmcli/pkg/conversation"
"git.mlow.ca/mlow/lmcli/pkg/lmcli"
"github.com/spf13/cobra"
)
@@ -25,7 +26,7 @@ func PromptCmd(ctx *lmcli.Context) *cobra.Command {
return fmt.Errorf("No message was provided.")
}
messages := []api.Message{{
messages := []conversation.Message{{
Role: api.MessageRoleUser,
Content: input,
}}

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"strings"
"git.mlow.ca/mlow/lmcli/pkg/api"
cmdutil "git.mlow.ca/mlow/lmcli/pkg/cmd/util"
"git.mlow.ca/mlow/lmcli/pkg/conversation"
"git.mlow.ca/mlow/lmcli/pkg/lmcli"
"github.com/spf13/cobra"
)
@@ -23,14 +23,14 @@ func RemoveCmd(ctx *lmcli.Context) *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
var toRemove []*api.Conversation
var toRemove []*conversation.Conversation
for _, shortName := range args {
conversation := cmdutil.LookupConversation(ctx, shortName)
toRemove = append(toRemove, conversation)
}
var errors []error
for _, c := range toRemove {
err := ctx.Store.DeleteConversation(c)
err := ctx.Conversations.DeleteConversation(c)
if err != nil {
errors = append(errors, fmt.Errorf("Could not remove conversation %s: %v", c.ShortName.String, err))
}
@@ -44,7 +44,7 @@ func RemoveCmd(ctx *lmcli.Context) *cobra.Command {
compMode := cobra.ShellCompDirectiveNoFileComp
var completions []string
outer:
for _, completion := range ctx.Store.ConversationShortNameCompletions(toComplete) {
for _, completion := range ctx.Conversations.ConversationShortNameCompletions(toComplete) {
parts := strings.Split(completion, "\t")
for _, arg := range args {
if parts[0] == arg {

View File

@@ -30,7 +30,7 @@ func RenameCmd(ctx *lmcli.Context) *cobra.Command {
generate, _ := cmd.Flags().GetBool("generate")
if generate {
messages, err := ctx.Store.PathToLeaf(conversation.SelectedRoot)
messages, err := ctx.Conversations.PathToLeaf(conversation.SelectedRoot)
if err != nil {
return fmt.Errorf("Could not retrieve conversation messages: %v", err)
}
@@ -46,7 +46,7 @@ func RenameCmd(ctx *lmcli.Context) *cobra.Command {
}
conversation.Title = title
err = ctx.Store.UpdateConversation(conversation)
err = ctx.Conversations.UpdateConversation(conversation)
if err != nil {
lmcli.Warn("Could not update conversation title: %v\n", err)
}
@@ -57,7 +57,7 @@ func RenameCmd(ctx *lmcli.Context) *cobra.Command {
if len(args) != 0 {
return nil, compMode
}
return ctx.Store.ConversationShortNameCompletions(toComplete), compMode
return ctx.Conversations.ConversationShortNameCompletions(toComplete), compMode
},
}

View File

@@ -5,6 +5,7 @@ import (
"git.mlow.ca/mlow/lmcli/pkg/api"
cmdutil "git.mlow.ca/mlow/lmcli/pkg/cmd/util"
"git.mlow.ca/mlow/lmcli/pkg/conversation"
"git.mlow.ca/mlow/lmcli/pkg/lmcli"
"github.com/spf13/cobra"
)
@@ -28,14 +29,14 @@ func ReplyCmd(ctx *lmcli.Context) *cobra.Command {
}
shortName := args[0]
conversation := cmdutil.LookupConversation(ctx, shortName)
c := cmdutil.LookupConversation(ctx, shortName)
reply := inputFromArgsOrEditor(args[1:], "# How would you like to reply?\n", "")
if reply == "" {
return fmt.Errorf("No reply was provided.")
}
cmdutil.HandleConversationReply(ctx, conversation, true, api.Message{
cmdutil.HandleConversationReply(ctx, c, true, conversation.Message{
Role: api.MessageRoleUser,
Content: reply,
})
@@ -46,7 +47,7 @@ func ReplyCmd(ctx *lmcli.Context) *cobra.Command {
if len(args) != 0 {
return nil, compMode
}
return ctx.Store.ConversationShortNameCompletions(toComplete), compMode
return ctx.Conversations.ConversationShortNameCompletions(toComplete), compMode
},
}

View File

@@ -28,12 +28,12 @@ func RetryCmd(ctx *lmcli.Context) *cobra.Command {
}
shortName := args[0]
conversation := cmdutil.LookupConversation(ctx, shortName)
c := cmdutil.LookupConversation(ctx, shortName)
// Load the complete thread from the root message
messages, err := ctx.Store.PathToLeaf(conversation.SelectedRoot)
messages, err := ctx.Conversations.PathToLeaf(c.SelectedRoot)
if err != nil {
return fmt.Errorf("Could not retrieve messages for conversation: %s", conversation.Title)
return fmt.Errorf("Could not retrieve messages for conversation: %s", c.Title)
}
offset, _ := cmd.Flags().GetInt("offset")
@@ -67,7 +67,7 @@ func RetryCmd(ctx *lmcli.Context) *cobra.Command {
if len(args) != 0 {
return nil, compMode
}
return ctx.Store.ConversationShortNameCompletions(toComplete), compMode
return ctx.Conversations.ConversationShortNameCompletions(toComplete), compMode
},
}

View File

@@ -9,7 +9,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/lmcli"
"git.mlow.ca/mlow/lmcli/pkg/util"
"github.com/charmbracelet/lipgloss"
@@ -17,7 +18,7 @@ import (
// Prompt prompts the configured the configured model and streams the response
// to stdout. Returns all model reply messages.
func Prompt(ctx *lmcli.Context, messages []api.Message, callback func(api.Message)) (*api.Message, error) {
func Prompt(ctx *lmcli.Context, messages []conversation.Message, callback func(conversation.Message)) (*api.Message, error) {
m, _, p, err := ctx.GetModelProvider(*ctx.Config.Defaults.Model, "")
if err != nil {
return nil, err
@@ -40,7 +41,7 @@ func Prompt(ctx *lmcli.Context, messages []api.Message, callback func(api.Messag
}
if system != "" {
messages = api.ApplySystemPrompt(messages, system, false)
messages = conversation.ApplySystemPrompt(messages, system, false)
}
content := make(chan provider.Chunk)
@@ -50,7 +51,7 @@ func Prompt(ctx *lmcli.Context, messages []api.Message, callback func(api.Messag
go ShowDelayedContent(content)
reply, err := p.CreateChatCompletionStream(
context.Background(), params, messages, content,
context.Background(), params, conversation.MessagesToAPI(messages), content,
)
if reply.Content != "" {
@@ -67,8 +68,8 @@ func Prompt(ctx *lmcli.Context, messages []api.Message, callback func(api.Messag
// lookupConversation either returns the conversation found by the
// short name or exits the program
func LookupConversation(ctx *lmcli.Context, shortName string) *api.Conversation {
c, err := ctx.Store.ConversationByShortName(shortName)
func LookupConversation(ctx *lmcli.Context, shortName string) *conversation.Conversation {
c, err := ctx.Conversations.FindConversationByShortName(shortName)
if err != nil {
lmcli.Fatal("Could not lookup conversation: %v\n", err)
}
@@ -78,8 +79,8 @@ func LookupConversation(ctx *lmcli.Context, shortName string) *api.Conversation
return c
}
func LookupConversationE(ctx *lmcli.Context, shortName string) (*api.Conversation, error) {
c, err := ctx.Store.ConversationByShortName(shortName)
func LookupConversationE(ctx *lmcli.Context, shortName string) (*conversation.Conversation, error) {
c, err := ctx.Conversations.FindConversationByShortName(shortName)
if err != nil {
return nil, fmt.Errorf("Could not lookup conversation: %v", err)
}
@@ -89,8 +90,8 @@ func LookupConversationE(ctx *lmcli.Context, shortName string) (*api.Conversatio
return c, nil
}
func HandleConversationReply(ctx *lmcli.Context, c *api.Conversation, persist bool, toSend ...api.Message) {
messages, err := ctx.Store.PathToLeaf(c.SelectedRoot)
func HandleConversationReply(ctx *lmcli.Context, c *conversation.Conversation, persist bool, toSend ...conversation.Message) {
messages, err := ctx.Conversations.PathToLeaf(c.SelectedRoot)
if err != nil {
lmcli.Fatal("Could not load messages: %v\n", err)
}
@@ -99,40 +100,40 @@ func HandleConversationReply(ctx *lmcli.Context, c *api.Conversation, persist bo
// handleConversationReply handles sending messages to an existing
// conversation, optionally persisting both the sent replies and responses.
func HandleReply(ctx *lmcli.Context, to *api.Message, persist bool, messages ...api.Message) {
func HandleReply(ctx *lmcli.Context, to *conversation.Message, persist bool, messages ...conversation.Message) {
if to == nil {
lmcli.Fatal("Can't prompt from an empty message.")
}
existing, err := ctx.Store.PathToRoot(to)
existing, err := ctx.Conversations.PathToRoot(to)
if err != nil {
lmcli.Fatal("Could not load messages: %v\n", err)
}
RenderConversation(ctx, append(existing, messages...), true)
var savedReplies []api.Message
var savedReplies []conversation.Message
if persist && len(messages) > 0 {
savedReplies, err = ctx.Store.Reply(to, messages...)
savedReplies, err = ctx.Conversations.Reply(to, messages...)
if err != nil {
lmcli.Warn("Could not save messages: %v\n", err)
}
}
// render a message header with no contents
RenderMessage(ctx, (&api.Message{Role: api.MessageRoleAssistant}))
RenderMessage(ctx, (&conversation.Message{Role: api.MessageRoleAssistant}))
var lastSavedMessage *api.Message
var lastSavedMessage *conversation.Message
lastSavedMessage = to
if len(savedReplies) > 0 {
lastSavedMessage = &savedReplies[len(savedReplies)-1]
}
replyCallback := func(reply api.Message) {
replyCallback := func(reply conversation.Message) {
if !persist {
return
}
savedReplies, err = ctx.Store.Reply(lastSavedMessage, reply)
savedReplies, err = ctx.Conversations.Reply(lastSavedMessage, reply)
if err != nil {
lmcli.Warn("Could not save reply: %v\n", err)
}
@@ -145,7 +146,7 @@ func HandleReply(ctx *lmcli.Context, to *api.Message, persist bool, messages ...
}
}
func FormatForExternalPrompt(messages []api.Message, system bool) string {
func FormatForExternalPrompt(messages []conversation.Message, system bool) string {
sb := strings.Builder{}
for _, message := range messages {
if message.Content == "" {
@@ -164,7 +165,7 @@ func FormatForExternalPrompt(messages []api.Message, system bool) string {
return sb.String()
}
func GenerateTitle(ctx *lmcli.Context, messages []api.Message) (string, error) {
func GenerateTitle(ctx *lmcli.Context, messages []conversation.Message) (string, error) {
const systemPrompt = `You will be shown a conversation between a user and an AI assistant. Your task is to generate a short title (8 words or less) for the provided conversation that reflects the conversation's topic. Your response is expected to be in JSON in the format shown below.
Example conversation:
@@ -189,19 +190,19 @@ Example response:
}
// Serialize the conversation to JSON
conversation, err := json.Marshal(msgs)
jsonBytes, err := json.Marshal(msgs)
if err != nil {
return "", err
}
generateRequest := []api.Message{
generateRequest := []conversation.Message{
{
Role: api.MessageRoleSystem,
Content: systemPrompt,
},
{
Role: api.MessageRoleUser,
Content: string(conversation),
Content: string(jsonBytes),
},
}
@@ -218,7 +219,7 @@ Example response:
}
response, err := p.CreateChatCompletion(
context.Background(), requestParams, generateRequest,
context.Background(), requestParams, conversation.MessagesToAPI(generateRequest),
)
if err != nil {
return "", err
@@ -293,7 +294,7 @@ func ShowDelayedContent(content <-chan provider.Chunk) {
// RenderConversation renders the given messages to TTY, with optional space
// for a subsequent message. spaceForResponse controls how many '\n' characters
// are printed immediately after the final message (1 if false, 2 if true)
func RenderConversation(ctx *lmcli.Context, messages []api.Message, spaceForResponse bool) {
func RenderConversation(ctx *lmcli.Context, messages []conversation.Message, spaceForResponse bool) {
l := len(messages)
for i, message := range messages {
RenderMessage(ctx, &message)
@@ -304,7 +305,7 @@ func RenderConversation(ctx *lmcli.Context, messages []api.Message, spaceForResp
}
}
func RenderMessage(ctx *lmcli.Context, m *api.Message) {
func RenderMessage(ctx *lmcli.Context, m *conversation.Message) {
var messageAge string
if m.CreatedAt.IsZero() {
messageAge = "now"

View File

@@ -24,7 +24,7 @@ func ViewCmd(ctx *lmcli.Context) *cobra.Command {
shortName := args[0]
conversation := cmdutil.LookupConversation(ctx, shortName)
messages, err := ctx.Store.PathToLeaf(conversation.SelectedRoot)
messages, err := ctx.Conversations.PathToLeaf(conversation.SelectedRoot)
if err != nil {
return fmt.Errorf("Could not retrieve messages for conversation %s: %v", conversation.ShortName.String, err)
}
@@ -37,7 +37,7 @@ func ViewCmd(ctx *lmcli.Context) *cobra.Command {
if len(args) != 0 {
return nil, compMode
}
return ctx.Store.ConversationShortNameCompletions(toComplete), compMode
return ctx.Conversations.ConversationShortNameCompletions(toComplete), compMode
},
}