2023-11-04 12:20:13 -06:00
package cli
import (
"fmt"
2023-12-05 22:47:35 -07:00
"os"
2023-11-12 23:35:57 -07:00
"slices"
2023-11-04 12:20:13 -06:00
"strings"
2023-11-12 00:19:45 -07:00
"time"
2023-11-04 16:56:22 -06:00
2023-11-04 12:20:13 -06:00
"github.com/spf13/cobra"
)
2023-11-18 08:07:17 -07:00
var (
2023-11-21 21:45:06 -07:00
maxTokens int
model string
systemPrompt string
systemPromptFile string
2023-11-18 08:07:17 -07:00
)
2023-11-05 11:19:30 -07:00
2024-01-03 00:20:46 -07:00
const (
2024-01-03 10:45:02 -07:00
// Limit number of conversations shown with `ls`, without --all
2024-01-03 00:20:46 -07:00
LS_LIMIT int = 25
)
2023-11-13 23:00:51 -07:00
func init ( ) {
2024-01-01 21:31:21 -07:00
inputCmds := [ ] * cobra . Command { newCmd , promptCmd , replyCmd , retryCmd , continueCmd , editCmd }
2023-11-18 08:07:17 -07:00
for _ , cmd := range inputCmds {
2024-01-02 22:50:16 -07:00
cmd . Flags ( ) . IntVar ( & maxTokens , "length" , * config . OpenAI . DefaultMaxLength , "Maximum response tokens" )
cmd . Flags ( ) . StringVar ( & model , "model" , * config . OpenAI . DefaultModel , "Which model to use model" )
cmd . Flags ( ) . StringVar ( & systemPrompt , "system-prompt" , * config . ModelDefaults . SystemPrompt , "System prompt" )
cmd . Flags ( ) . StringVar ( & systemPromptFile , "system-prompt-file" , "" , "A path to a file containing the system prompt" )
2023-11-21 21:45:06 -07:00
cmd . MarkFlagsMutuallyExclusive ( "system-prompt" , "system-prompt-file" )
2023-11-18 08:07:17 -07:00
}
2024-01-03 10:45:02 -07:00
listCmd . Flags ( ) . Bool ( "all" , false , fmt . Sprintf ( "Show all conversations, by default only the last %d are shown" , LS_LIMIT ) )
2024-01-02 22:50:16 -07:00
renameCmd . Flags ( ) . Bool ( "generate" , false , "Generate a conversation title" )
2024-01-09 11:10:05 -07:00
editCmd . Flags ( ) . Int ( "offset" , 1 , "Offset from the last reply to edit (Default: edit your last message, assuming there's an assistant reply)" )
2023-11-29 08:30:11 -07:00
2023-11-13 23:00:51 -07:00
rootCmd . AddCommand (
2024-01-03 00:32:29 -07:00
cloneCmd ,
2023-11-21 23:53:22 -07:00
continueCmd ,
2024-01-02 22:52:41 -07:00
editCmd ,
2024-01-03 10:45:02 -07:00
listCmd ,
2023-11-13 23:00:51 -07:00
newCmd ,
promptCmd ,
2023-11-29 08:30:11 -07:00
renameCmd ,
2023-11-13 23:00:51 -07:00
replyCmd ,
2023-11-21 23:53:22 -07:00
retryCmd ,
2023-11-13 23:00:51 -07:00
rmCmd ,
viewCmd ,
)
}
func Execute ( ) error {
return rootCmd . Execute ( )
}
2024-01-02 22:52:41 -07:00
func getSystemPrompt ( ) string {
2023-11-21 21:45:06 -07:00
if systemPromptFile != "" {
content , err := FileContents ( systemPromptFile )
if err != nil {
2024-01-02 22:52:41 -07:00
Fatal ( "Could not read file contents at %s: %v\n" , systemPromptFile , err )
2023-11-21 21:45:06 -07:00
}
return content
}
return systemPrompt
}
2024-01-01 21:31:21 -07:00
// fetchAndShowCompletion prompts the LLM with the given messages and streams
// the response to stdout. Returns all model reply messages.
func fetchAndShowCompletion ( messages [ ] Message ) ( [ ] Message , error ) {
content := make ( chan string ) // receives the reponse from LLM
defer close ( content )
2023-11-24 08:17:24 -07:00
2024-01-01 21:31:21 -07:00
// render all content received over the channel
go ShowDelayedContent ( content )
2023-11-24 08:17:24 -07:00
2023-11-28 21:43:53 -07:00
var replies [ ] Message
2024-01-01 21:31:21 -07:00
response , err := CreateChatCompletionStream ( model , messages , maxTokens , content , & replies )
2023-11-24 08:17:24 -07:00
if response != "" {
2024-01-01 21:31:21 -07:00
// there was some content, so break to a new line after it
fmt . Println ( )
2023-11-24 18:26:37 -07:00
if err != nil {
Warn ( "Received partial response. Error: %v\n" , err )
err = nil
}
2023-11-24 08:17:24 -07:00
}
2023-11-28 21:43:53 -07:00
return replies , err
2023-11-23 20:45:43 -07:00
}
2024-01-02 22:52:41 -07:00
// lookupConversation either returns the conversation found by the
2024-01-01 21:31:21 -07:00
// short name or exits the program
2024-01-02 22:52:41 -07:00
func lookupConversation ( shortName string ) * Conversation {
2024-01-01 21:31:21 -07:00
c , err := store . ConversationByShortName ( shortName )
if err != nil {
Fatal ( "Could not lookup conversation: %v\n" , err )
}
if c . ID == 0 {
Fatal ( "Conversation not found with short name: %s\n" , shortName )
}
return c
}
2024-01-03 00:32:29 -07:00
func lookupConversationE ( shortName string ) ( * Conversation , error ) {
c , err := store . ConversationByShortName ( shortName )
if err != nil {
return nil , fmt . Errorf ( "Could not lookup conversation: %v" , err )
}
if c . ID == 0 {
return nil , fmt . Errorf ( "Conversation not found with short name: %s" , shortName )
}
return c , nil
}
2024-01-01 21:31:21 -07:00
// handleConversationReply handles sending messages to an existing
2024-01-03 10:45:02 -07:00
// conversation, optionally persisting both the sent replies and responses.
2024-01-01 21:31:21 -07:00
func handleConversationReply ( c * Conversation , persist bool , toSend ... Message ) {
existing , err := store . Messages ( c )
if err != nil {
Fatal ( "Could not retrieve messages for conversation: %s\n" , c . Title )
}
if persist {
for _ , message := range toSend {
err = store . SaveMessage ( & message )
if err != nil {
Warn ( "Could not save %s message: %v\n" , message . Role , err )
}
}
}
allMessages := append ( existing , toSend ... )
RenderConversation ( allMessages , true )
// render a message header with no contents
( & Message { Role : MessageRoleAssistant } ) . RenderTTY ( )
replies , err := fetchAndShowCompletion ( allMessages )
2023-11-28 22:10:44 -07:00
if err != nil {
Fatal ( "Error fetching LLM response: %v\n" , err )
}
2024-01-01 21:31:21 -07:00
if persist {
for _ , reply := range replies {
reply . ConversationID = c . ID
2023-11-28 22:10:44 -07:00
2024-01-01 21:31:21 -07:00
err = store . SaveMessage ( & reply )
if err != nil {
Warn ( "Could not save reply: %v\n" , err )
}
2023-11-28 22:10:44 -07:00
}
}
}
2024-01-02 22:52:41 -07:00
// inputFromArgsOrEditor returns either the provided input from the args slice
2023-11-20 09:50:56 -07:00
// (joined with spaces), or if len(args) is 0, opens an editor and returns
// whatever input was provided there. placeholder is a string which populates
// the editor and gets stripped from the final output.
2024-01-02 22:52:41 -07:00
func inputFromArgsOrEditor ( args [ ] string , placeholder string , existingMessage string ) ( message string ) {
2023-11-20 09:50:56 -07:00
var err error
if len ( args ) == 0 {
2024-01-01 21:31:21 -07:00
message , err = InputFromEditor ( placeholder , "message.*.md" , existingMessage )
2023-11-20 09:50:56 -07:00
if err != nil {
Fatal ( "Failed to get input: %v\n" , err )
}
} else {
message = strings . Trim ( strings . Join ( args , " " ) , " \t\n" )
}
return
}
2023-11-04 12:20:13 -06:00
var rootCmd = & cobra . Command {
2024-01-03 00:32:29 -07:00
Use : "lmcli <command> [flags]" ,
Long : ` lmcli - Large Language Model CLI ` ,
SilenceErrors : true ,
SilenceUsage : true ,
2023-11-04 12:20:13 -06:00
Run : func ( cmd * cobra . Command , args [ ] string ) {
2024-01-03 00:32:55 -07:00
cmd . Usage ( )
2023-11-04 12:20:13 -06:00
} ,
}
2024-01-03 10:45:02 -07:00
var listCmd = & cobra . Command {
Use : "list" ,
Aliases : [ ] string { "ls" } ,
2024-01-03 10:22:43 -07:00
Short : "List conversations" ,
Long : ` List conversations in order of recent activity ` ,
2023-11-04 12:20:13 -06:00
Run : func ( cmd * cobra . Command , args [ ] string ) {
2023-11-12 17:20:54 -07:00
conversations , err := store . Conversations ( )
2023-11-12 00:19:45 -07:00
if err != nil {
2024-01-03 10:45:02 -07:00
Fatal ( "Could not fetch conversations.\n" )
2023-11-12 00:19:45 -07:00
return
}
2023-11-04 12:20:13 -06:00
2024-01-03 00:19:08 -07:00
type Category struct {
name string
cutoff time . Duration
}
2023-11-12 00:19:45 -07:00
2023-11-12 23:35:57 -07:00
type ConversationLine struct {
timeSinceReply time . Duration
formatted string
}
2023-11-12 00:19:45 -07:00
now := time . Now ( )
2024-01-03 00:19:08 -07:00
midnight := time . Date ( now . Year ( ) , now . Month ( ) , now . Day ( ) , 0 , 0 , 0 , 0 , now . Location ( ) )
monthStart := time . Date ( now . Year ( ) , now . Month ( ) , 1 , 0 , 0 , 0 , 0 , now . Location ( ) )
dayOfWeek := int ( now . Weekday ( ) )
categories := [ ] Category {
{ "today" , now . Sub ( midnight ) } ,
{ "yesterday" , now . Sub ( midnight . AddDate ( 0 , 0 , - 1 ) ) } ,
{ "this week" , now . Sub ( midnight . AddDate ( 0 , 0 , - dayOfWeek ) ) } ,
{ "last week" , now . Sub ( midnight . AddDate ( 0 , 0 , - ( dayOfWeek + 7 ) ) ) } ,
{ "this month" , now . Sub ( monthStart ) } ,
{ "last month" , now . Sub ( monthStart . AddDate ( 0 , - 1 , 0 ) ) } ,
{ "2 months ago" , now . Sub ( monthStart . AddDate ( 0 , - 2 , 0 ) ) } ,
{ "3 months ago" , now . Sub ( monthStart . AddDate ( 0 , - 3 , 0 ) ) } ,
{ "4 months ago" , now . Sub ( monthStart . AddDate ( 0 , - 4 , 0 ) ) } ,
{ "5 months ago" , now . Sub ( monthStart . AddDate ( 0 , - 5 , 0 ) ) } ,
{ "older" , now . Sub ( time . Time { } ) } ,
2023-11-12 00:19:45 -07:00
}
2023-11-12 23:35:57 -07:00
categorized := map [ string ] [ ] ConversationLine { }
2023-11-12 00:19:45 -07:00
2024-01-03 00:20:46 -07:00
all , _ := cmd . Flags ( ) . GetBool ( "all" )
2023-11-12 00:19:45 -07:00
for _ , conversation := range conversations {
2023-11-12 17:20:54 -07:00
lastMessage , err := store . LastMessage ( & conversation )
2023-11-12 00:19:45 -07:00
if lastMessage == nil || err != nil {
continue
}
2024-01-03 00:19:08 -07:00
2023-11-12 00:19:45 -07:00
messageAge := now . Sub ( lastMessage . CreatedAt )
var category string
2024-01-03 00:19:08 -07:00
for _ , c := range categories {
if messageAge < c . cutoff {
category = c . name
break
}
2023-11-12 00:19:45 -07:00
}
formatted := fmt . Sprintf (
"%s - %s - %s" ,
conversation . ShortName . String ,
humanTimeElapsedSince ( messageAge ) ,
conversation . Title ,
)
2024-01-03 00:19:08 -07:00
2023-11-12 23:35:57 -07:00
categorized [ category ] = append (
categorized [ category ] ,
ConversationLine { messageAge , formatted } ,
)
2023-11-12 00:19:45 -07:00
}
2024-01-03 00:20:46 -07:00
var conversationsPrinted int
outer :
2023-11-12 00:19:45 -07:00
for _ , category := range categories {
2024-01-03 00:20:46 -07:00
conversations , ok := categorized [ category . name ]
2023-11-12 00:19:45 -07:00
if ! ok {
continue
}
2024-01-03 00:20:46 -07:00
2023-11-12 23:35:57 -07:00
slices . SortFunc ( conversations , func ( a , b ConversationLine ) int {
return int ( a . timeSinceReply - b . timeSinceReply )
} )
2024-01-03 00:20:46 -07:00
fmt . Printf ( "%s:\n" , category . name )
2023-11-12 00:19:45 -07:00
for _ , conv := range conversations {
2024-01-03 00:20:46 -07:00
if conversationsPrinted >= LS_LIMIT && ! all {
fmt . Printf ( "%d remaining message(s), use --all to view.\n" , len ( conversations ) - conversationsPrinted )
break outer
}
2023-11-12 23:35:57 -07:00
fmt . Printf ( " %s\n" , conv . formatted )
2024-01-03 00:20:46 -07:00
conversationsPrinted ++
2023-11-12 00:19:45 -07:00
}
}
2023-11-04 12:20:13 -06:00
} ,
}
2023-11-12 23:56:05 -07:00
var rmCmd = & cobra . Command {
2023-12-05 22:47:35 -07:00
Use : "rm <conversation>..." ,
Short : "Remove conversations" ,
Long : ` Remove conversations by their short names. ` ,
2023-11-12 23:56:05 -07:00
Args : func ( cmd * cobra . Command , args [ ] string ) error {
argCount := 1
if err := cobra . MinimumNArgs ( argCount ) ( cmd , args ) ; err != nil {
return err
}
return nil
} ,
Run : func ( cmd * cobra . Command , args [ ] string ) {
2023-12-05 22:47:35 -07:00
var toRemove [ ] * Conversation
for _ , shortName := range args {
2024-01-02 22:52:41 -07:00
conversation := lookupConversation ( shortName )
2023-12-05 22:47:35 -07:00
toRemove = append ( toRemove , conversation )
2023-11-12 23:56:05 -07:00
}
2023-12-05 22:47:35 -07:00
var errors [ ] error
for _ , c := range toRemove {
err := store . DeleteConversation ( c )
if err != nil {
errors = append ( errors , fmt . Errorf ( "Could not remove conversation %s: %v" , c . ShortName . String , err ) )
}
2023-11-12 23:56:05 -07:00
}
2023-12-05 22:47:35 -07:00
for _ , err := range errors {
fmt . Fprintln ( os . Stderr , err . Error ( ) )
}
if len ( errors ) > 0 {
os . Exit ( 1 )
2023-11-12 23:56:05 -07:00
}
} ,
ValidArgsFunction : func ( cmd * cobra . Command , args [ ] string , toComplete string ) ( [ ] string , cobra . ShellCompDirective ) {
compMode := cobra . ShellCompDirectiveNoFileComp
2023-12-05 22:47:35 -07:00
var completions [ ] string
2024-01-01 21:31:21 -07:00
outer :
for _ , completion := range store . ConversationShortNameCompletions ( toComplete ) {
2023-12-05 22:47:35 -07:00
parts := strings . Split ( completion , "\t" )
for _ , arg := range args {
if parts [ 0 ] == arg {
continue outer
}
}
completions = append ( completions , completion )
2023-11-12 23:56:05 -07:00
}
2023-12-05 22:47:35 -07:00
return completions , compMode
2023-11-12 23:56:05 -07:00
} ,
}
2024-01-03 00:32:29 -07:00
var cloneCmd = & cobra . Command {
Use : "clone <conversation>" ,
2024-01-03 10:22:43 -07:00
Short : "Clone conversations" ,
2024-01-03 00:32:29 -07:00
Long : ` Clones the provided conversation. ` ,
Args : func ( cmd * cobra . Command , args [ ] string ) error {
argCount := 1
if err := cobra . MinimumNArgs ( argCount ) ( cmd , args ) ; err != nil {
return err
}
return nil
} ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
shortName := args [ 0 ]
toClone , err := lookupConversationE ( shortName )
if err != nil {
return err
}
messagesToCopy , err := store . Messages ( toClone )
if err != nil {
return fmt . Errorf ( "Could not retrieve messages for conversation: %s" , toClone . ShortName . String )
}
clone := & Conversation {
Title : toClone . Title + " - Clone" ,
}
if err := store . SaveConversation ( clone ) ; err != nil {
return fmt . Errorf ( "Cloud not create clone: %s" , err )
}
var errors [ ] error
messageCnt := 0
for _ , message := range messagesToCopy {
newMessage := message
newMessage . ConversationID = clone . ID
newMessage . ID = 0
if err := store . SaveMessage ( & newMessage ) ; err != nil {
errors = append ( errors , err )
} else {
messageCnt ++
}
}
if len ( errors ) > 0 {
return fmt . Errorf ( "Messages failed to be cloned: %v" , errors )
}
fmt . Printf ( "Cloned %d messages to: %s\n" , messageCnt , clone . Title )
return nil
} ,
ValidArgsFunction : func ( cmd * cobra . Command , args [ ] string , toComplete string ) ( [ ] string , cobra . ShellCompDirective ) {
compMode := cobra . ShellCompDirectiveNoFileComp
if len ( args ) != 0 {
return nil , compMode
}
return store . ConversationShortNameCompletions ( toComplete ) , compMode
} ,
}
2023-11-04 12:20:13 -06:00
var viewCmd = & cobra . Command {
2023-11-20 09:50:56 -07:00
Use : "view <conversation>" ,
2023-11-04 12:20:13 -06:00
Short : "View messages in a conversation" ,
2023-11-12 16:32:12 -07:00
Long : ` Finds a conversation by its short name and displays its contents. ` ,
Args : func ( cmd * cobra . Command , args [ ] string ) error {
argCount := 1
if err := cobra . MinimumNArgs ( argCount ) ( cmd , args ) ; err != nil {
return err
}
return nil
} ,
2023-11-04 12:20:13 -06:00
Run : func ( cmd * cobra . Command , args [ ] string ) {
2023-11-12 16:32:12 -07:00
shortName := args [ 0 ]
2024-01-02 22:52:41 -07:00
conversation := lookupConversation ( shortName )
2023-11-12 16:32:12 -07:00
2023-11-12 17:20:54 -07:00
messages , err := store . Messages ( conversation )
2023-11-12 16:32:12 -07:00
if err != nil {
Fatal ( "Could not retrieve messages for conversation: %s\n" , conversation . Title )
}
2023-11-18 08:57:03 -07:00
RenderConversation ( messages , false )
2023-11-12 16:32:12 -07:00
} ,
ValidArgsFunction : func ( cmd * cobra . Command , args [ ] string , toComplete string ) ( [ ] string , cobra . ShellCompDirective ) {
2023-11-12 22:26:21 -07:00
compMode := cobra . ShellCompDirectiveNoFileComp
if len ( args ) != 0 {
return nil , compMode
}
return store . ConversationShortNameCompletions ( toComplete ) , compMode
2023-11-04 12:20:13 -06:00
} ,
}
2024-01-01 21:31:21 -07:00
var renameCmd = & cobra . Command {
Use : "rename <conversation> [title]" ,
Short : "Rename a conversation" ,
Long : ` Renames a conversation, either with the provided title or by generating a new name. ` ,
2023-11-13 19:09:09 -07:00
Args : func ( cmd * cobra . Command , args [ ] string ) error {
argCount := 1
if err := cobra . MinimumNArgs ( argCount ) ( cmd , args ) ; err != nil {
return err
}
return nil
} ,
2023-11-04 16:37:18 -06:00
Run : func ( cmd * cobra . Command , args [ ] string ) {
2023-11-13 19:09:09 -07:00
shortName := args [ 0 ]
2024-01-02 22:52:41 -07:00
conversation := lookupConversation ( shortName )
2024-01-01 21:31:21 -07:00
var err error
generate , _ := cmd . Flags ( ) . GetBool ( "generate" )
var title string
if generate {
title , err = conversation . GenerateTitle ( )
if err != nil {
Fatal ( "Could not generate conversation title: %v\n" , err )
}
} else {
if len ( args ) < 2 {
2024-01-02 22:52:41 -07:00
Fatal ( "Conversation title not provided.\n" )
2024-01-01 21:31:21 -07:00
}
title = strings . Join ( args [ 1 : ] , " " )
2023-11-13 19:09:09 -07:00
}
2024-01-01 21:31:21 -07:00
conversation . Title = title
err = store . SaveConversation ( conversation )
2023-11-13 19:09:09 -07:00
if err != nil {
2024-01-01 21:31:21 -07:00
Warn ( "Could not save conversation with new title: %v\n" , err )
2023-11-13 19:09:09 -07:00
}
2024-01-01 21:31:21 -07:00
} ,
ValidArgsFunction : func ( cmd * cobra . Command , args [ ] string , toComplete string ) ( [ ] string , cobra . ShellCompDirective ) {
compMode := cobra . ShellCompDirectiveNoFileComp
if len ( args ) != 0 {
return nil , compMode
2023-11-18 08:17:18 -07:00
}
2024-01-01 21:31:21 -07:00
return store . ConversationShortNameCompletions ( toComplete ) , compMode
} ,
}
2023-11-13 19:09:09 -07:00
2024-01-01 21:31:21 -07:00
var replyCmd = & cobra . Command {
Use : "reply <conversation> [message]" ,
2024-01-03 10:22:43 -07:00
Short : "Reply to a conversation" ,
2024-01-01 21:31:21 -07:00
Long : ` Sends a reply to conversation and writes the response to stdout. ` ,
Args : func ( cmd * cobra . Command , args [ ] string ) error {
argCount := 1
if err := cobra . MinimumNArgs ( argCount ) ( cmd , args ) ; err != nil {
return err
2023-11-13 19:09:09 -07:00
}
2024-01-01 21:31:21 -07:00
return nil
} ,
Run : func ( cmd * cobra . Command , args [ ] string ) {
shortName := args [ 0 ]
2024-01-02 22:52:41 -07:00
conversation := lookupConversation ( shortName )
2023-11-13 19:09:09 -07:00
2024-01-02 22:52:41 -07:00
reply := inputFromArgsOrEditor ( args [ 1 : ] , "# How would you like to reply?\n" , "" )
2024-01-01 21:31:21 -07:00
if reply == "" {
Fatal ( "No reply was provided.\n" )
2023-11-13 19:09:09 -07:00
}
2024-01-01 21:31:21 -07:00
handleConversationReply ( conversation , true , Message {
ConversationID : conversation . ID ,
Role : MessageRoleUser ,
OriginalContent : reply ,
} )
2023-11-13 19:09:09 -07:00
} ,
ValidArgsFunction : func ( cmd * cobra . Command , args [ ] string , toComplete string ) ( [ ] string , cobra . ShellCompDirective ) {
compMode := cobra . ShellCompDirectiveNoFileComp
if len ( args ) != 0 {
return nil , compMode
}
return store . ConversationShortNameCompletions ( toComplete ) , compMode
2023-11-04 16:37:18 -06:00
} ,
}
2023-11-04 12:20:13 -06:00
var newCmd = & cobra . Command {
2023-11-20 09:50:56 -07:00
Use : "new [message]" ,
2023-11-04 12:20:13 -06:00
Short : "Start a new conversation" ,
2023-11-04 16:56:22 -06:00
Long : ` Start a new conversation with the Large Language Model. ` ,
2023-11-04 12:20:13 -06:00
Run : func ( cmd * cobra . Command , args [ ] string ) {
2024-01-02 22:52:41 -07:00
messageContents := inputFromArgsOrEditor ( args , "# What would you like to say?\n" , "" )
2023-11-04 12:20:13 -06:00
if messageContents == "" {
2023-11-04 13:58:48 -06:00
Fatal ( "No message was provided.\n" )
2023-11-04 12:20:13 -06:00
}
2023-11-29 08:30:11 -07:00
conversation := & Conversation { }
err := store . SaveConversation ( conversation )
2023-11-05 01:41:43 -06:00
if err != nil {
Fatal ( "Could not save new conversation: %v\n" , err )
}
2023-11-04 12:20:13 -06:00
2023-11-05 01:54:12 -06:00
messages := [ ] Message {
{
ConversationID : conversation . ID ,
2023-11-28 22:57:38 -07:00
Role : MessageRoleSystem ,
2024-01-02 22:52:41 -07:00
OriginalContent : getSystemPrompt ( ) ,
2023-11-05 01:54:12 -06:00
} ,
{
ConversationID : conversation . ID ,
2023-11-28 22:57:38 -07:00
Role : MessageRoleUser ,
2023-11-05 01:54:12 -06:00
OriginalContent : messageContents ,
} ,
2023-11-05 01:41:43 -06:00
}
2024-01-01 21:31:21 -07:00
handleConversationReply ( conversation , true , messages ... )
2023-11-04 12:20:13 -06:00
2023-11-29 08:30:11 -07:00
title , err := conversation . GenerateTitle ( )
2023-11-12 23:39:06 -07:00
if err != nil {
Warn ( "Could not generate title for conversation: %v\n" , err )
}
2023-11-20 09:50:56 -07:00
2023-11-29 08:30:11 -07:00
conversation . Title = title
err = store . SaveConversation ( conversation )
2023-11-12 23:39:06 -07:00
if err != nil {
Warn ( "Could not save conversation after generating title: %v\n" , err )
}
2023-11-04 12:20:13 -06:00
} ,
}
var promptCmd = & cobra . Command {
2023-11-20 09:50:56 -07:00
Use : "prompt [message]" ,
2023-11-04 12:20:13 -06:00
Short : "Do a one-shot prompt" ,
2023-11-04 16:56:22 -06:00
Long : ` Prompt the Large Language Model and get a response. ` ,
2023-11-04 12:20:13 -06:00
Run : func ( cmd * cobra . Command , args [ ] string ) {
2024-01-02 22:52:41 -07:00
message := inputFromArgsOrEditor ( args , "# What would you like to say?\n" , "" )
2023-11-20 09:50:56 -07:00
if message == "" {
2023-11-04 16:53:09 -06:00
Fatal ( "No message was provided.\n" )
}
2023-11-04 12:20:13 -06:00
messages := [ ] Message {
2023-11-05 01:54:12 -06:00
{
2023-11-28 22:57:38 -07:00
Role : MessageRoleSystem ,
2024-01-02 22:52:41 -07:00
OriginalContent : getSystemPrompt ( ) ,
2023-11-05 01:54:12 -06:00
} ,
2023-11-04 12:20:13 -06:00
{
2023-11-28 22:57:38 -07:00
Role : MessageRoleUser ,
2023-11-04 16:53:09 -06:00
OriginalContent : message ,
2023-11-04 12:20:13 -06:00
} ,
}
2024-01-01 21:31:21 -07:00
_ , err := fetchAndShowCompletion ( messages )
2023-11-04 12:20:13 -06:00
if err != nil {
2023-11-24 08:17:24 -07:00
Fatal ( "Error fetching LLM response: %v\n" , err )
2023-11-04 12:20:13 -06:00
}
} ,
}
2023-11-21 23:53:22 -07:00
var retryCmd = & cobra . Command {
Use : "retry <conversation>" ,
2024-01-03 10:22:43 -07:00
Short : "Retry the last user reply in a conversation" ,
2023-11-21 23:53:22 -07:00
Long : ` Re-prompt the conversation up to the last user response. Can be used to regenerate the last assistant reply, or simply generate one if an error occurred. ` ,
Args : func ( cmd * cobra . Command , args [ ] string ) error {
argCount := 1
if err := cobra . MinimumNArgs ( argCount ) ( cmd , args ) ; err != nil {
return err
}
return nil
} ,
Run : func ( cmd * cobra . Command , args [ ] string ) {
shortName := args [ 0 ]
2024-01-02 22:52:41 -07:00
conversation := lookupConversation ( shortName )
2023-11-21 23:53:22 -07:00
messages , err := store . Messages ( conversation )
if err != nil {
Fatal ( "Could not retrieve messages for conversation: %s\n" , conversation . Title )
}
2024-01-01 21:31:21 -07:00
// walk backwards through the conversation and delete messages, break
// when we find the latest user response
2023-11-23 20:45:43 -07:00
for i := len ( messages ) - 1 ; i >= 0 ; i -- {
2023-11-28 21:43:53 -07:00
if messages [ i ] . Role == MessageRoleUser {
2023-11-21 23:53:22 -07:00
break
}
2023-11-28 21:50:45 -07:00
2024-01-01 21:31:21 -07:00
err = store . DeleteMessage ( & messages [ i ] )
if err != nil {
Warn ( "Could not delete previous reply: %v\n" , err )
2023-11-28 21:50:45 -07:00
}
2023-11-21 23:53:22 -07:00
}
2024-01-01 21:31:21 -07:00
handleConversationReply ( conversation , true )
2023-11-21 23:53:22 -07:00
} ,
ValidArgsFunction : func ( cmd * cobra . Command , args [ ] string , toComplete string ) ( [ ] string , cobra . ShellCompDirective ) {
compMode := cobra . ShellCompDirectiveNoFileComp
if len ( args ) != 0 {
return nil , compMode
}
return store . ConversationShortNameCompletions ( toComplete ) , compMode
} ,
}
var continueCmd = & cobra . Command {
Use : "continue <conversation>" ,
2024-01-03 10:22:43 -07:00
Short : "Continue a conversation from the last message" ,
2023-11-21 23:53:22 -07:00
Long : ` Re-prompt the conversation with all existing prompts. Useful if a reply was cut short. ` ,
Args : func ( cmd * cobra . Command , args [ ] string ) error {
argCount := 1
if err := cobra . MinimumNArgs ( argCount ) ( cmd , args ) ; err != nil {
return err
}
return nil
} ,
Run : func ( cmd * cobra . Command , args [ ] string ) {
shortName := args [ 0 ]
2024-01-02 22:52:41 -07:00
conversation := lookupConversation ( shortName )
2024-01-01 21:31:21 -07:00
handleConversationReply ( conversation , true )
} ,
ValidArgsFunction : func ( cmd * cobra . Command , args [ ] string , toComplete string ) ( [ ] string , cobra . ShellCompDirective ) {
compMode := cobra . ShellCompDirectiveNoFileComp
if len ( args ) != 0 {
return nil , compMode
}
return store . ConversationShortNameCompletions ( toComplete ) , compMode
} ,
}
var editCmd = & cobra . Command {
Use : "edit <conversation>" ,
2024-01-03 10:22:43 -07:00
Short : "Edit the last user reply in a conversation" ,
2024-01-01 21:31:21 -07:00
Args : func ( cmd * cobra . Command , args [ ] string ) error {
argCount := 1
if err := cobra . MinimumNArgs ( argCount ) ( cmd , args ) ; err != nil {
return err
2023-11-21 23:53:22 -07:00
}
2024-01-01 21:31:21 -07:00
return nil
} ,
Run : func ( cmd * cobra . Command , args [ ] string ) {
shortName := args [ 0 ]
2024-01-02 22:52:41 -07:00
conversation := lookupConversation ( shortName )
2023-11-21 23:53:22 -07:00
messages , err := store . Messages ( conversation )
if err != nil {
Fatal ( "Could not retrieve messages for conversation: %s\n" , conversation . Title )
}
2024-01-09 11:10:05 -07:00
offset , _ := cmd . Flags ( ) . GetInt ( "offset" )
if offset < 0 {
offset = - offset
}
if offset > len ( messages ) - 1 {
Fatal ( "Offset %d is before the start of the conversation\n" , offset )
}
desiredIdx := len ( messages ) - 1 - offset
2024-01-01 21:31:21 -07:00
// walk backwards through the conversation deleting messages until and
// including the last user message
toRemove := [ ] Message { }
2024-01-09 11:10:05 -07:00
var toEdit * Message
2024-01-01 21:31:21 -07:00
for i := len ( messages ) - 1 ; i >= 0 ; i -- {
2024-01-09 11:10:05 -07:00
if i == desiredIdx {
toEdit = & messages [ i ]
2024-01-01 21:31:21 -07:00
}
toRemove = append ( toRemove , messages [ i ] )
messages = messages [ : i ]
2024-01-09 11:10:05 -07:00
if toEdit != nil {
2024-01-01 21:31:21 -07:00
break
}
}
2024-01-09 11:10:05 -07:00
existingContents := toEdit . OriginalContent
2023-11-21 23:53:22 -07:00
2024-01-02 22:52:41 -07:00
newContents := inputFromArgsOrEditor ( args [ 1 : ] , "# Save when finished editing\n" , existingContents )
2024-01-09 11:10:05 -07:00
switch newContents {
case existingContents :
2024-01-01 21:31:21 -07:00
Fatal ( "No edits were made.\n" )
2024-01-09 11:10:05 -07:00
case "" :
2024-01-01 21:31:21 -07:00
Fatal ( "No message was provided.\n" )
}
2024-01-02 22:52:41 -07:00
for _ , message := range toRemove {
2024-01-01 21:31:21 -07:00
err = store . DeleteMessage ( & message )
if err != nil {
Warn ( "Could not delete message: %v\n" , err )
}
}
handleConversationReply ( conversation , true , Message {
ConversationID : conversation . ID ,
Role : MessageRoleUser ,
OriginalContent : newContents ,
} )
2023-11-21 23:53:22 -07:00
} ,
ValidArgsFunction : func ( cmd * cobra . Command , args [ ] string , toComplete string ) ( [ ] string , cobra . ShellCompDirective ) {
compMode := cobra . ShellCompDirectiveNoFileComp
if len ( args ) != 0 {
return nil , compMode
}
return store . ConversationShortNameCompletions ( toComplete ) , compMode
} ,
}