Compare commits

..

No commits in common. "77c0d1bce8a28f0e575b0e17fc4301b78497c240" and "db788760a3cb386560b3f8be2a5339a191e00091" have entirely different histories.

2 changed files with 34 additions and 36 deletions

View File

@ -4,25 +4,29 @@
Current features:
- Perform one-shot prompts with `lmcli prompt <message>`
- Manage persistent conversations with the `new`, `reply`, `view`, and `rm`,
`edit`, `retry`, `continue` sub-commands.
- Manage persistent conversations with the `new`, `reply`, `view`, and `rm`
sub-commands.
- Syntax highlighted output
- Tool calling, see the [Tools](#tools) section.
Planned features:
- Ask questions about content received on stdin
- Conversation editing
Maybe features:
- Chat-like interface (`lmcli chat`) for rapid back-and-forth conversations
- Support for additional models/APIs besides just OpenAI
- Natural language image generation, iterative editing
## Tools
Tools must be explicitly enabled by adding the tool's name to the
`openai.enabledTools` array in `config.yaml`.
Note: all filesystem related tools operate relative to the current directory
only. They do not accept absolute paths, and efforts are made to ensure they
cannot escape above the working directory). **Close attention must be paid to
where you are running `lmcli`, as the model could at any time decide to use one
of these tools to discover and read potentially sensitive information from your
filesystem.**
only. They do not accept absolute paths, and all efforts are made to ensure
they cannot escape above the working directory (not quite using chroot, but in
effect). **Close attention must be paid to where you are running `lmcli`, as
the model could at any time decide to use one of these tools to discover and
read potentially sensitive information from your filesystem.**
It's best to only have tools enabled in `config.yaml` when you intend to be
using them, since their descriptions (see `pkg/cli/functions.go`) count towards

View File

@ -18,7 +18,7 @@ var (
)
const (
// Limit number of conversations shown with `ls`, without --all
// Limit to number of conversations shown with `ls`, without --all
LS_LIMIT int = 25
)
@ -32,15 +32,14 @@ func init() {
cmd.MarkFlagsMutuallyExclusive("system-prompt", "system-prompt-file")
}
listCmd.Flags().Bool("all", false, fmt.Sprintf("Show all conversations, by default only the last %d are shown", LS_LIMIT))
lsCmd.Flags().Bool("all", false, fmt.Sprintf("Show all conversations, by default only the last %d are shown", LS_LIMIT))
renameCmd.Flags().Bool("generate", false, "Generate a conversation title")
editCmd.Flags().Int("offset", 1, "Offset from the last reply to edit (Default: edit your last message, assuming there's an assistant reply)")
rootCmd.AddCommand(
cloneCmd,
continueCmd,
editCmd,
listCmd,
lsCmd,
newCmd,
promptCmd,
renameCmd,
@ -115,7 +114,8 @@ func lookupConversationE(shortName string) (*Conversation, error) {
}
// handleConversationReply handles sending messages to an existing
// conversation, optionally persisting both the sent replies and responses.
// conversation, optionally persisting them. It displays the entire
// conversation before
func handleConversationReply(c *Conversation, persist bool, toSend ...Message) {
existing, err := store.Messages(c)
if err != nil {
@ -182,15 +182,14 @@ var rootCmd = &cobra.Command{
},
}
var listCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
var lsCmd = &cobra.Command{
Use: "ls",
Short: "List conversations",
Long: `List conversations in order of recent activity`,
Run: func(cmd *cobra.Command, args []string) {
conversations, err := store.Conversations()
if err != nil {
Fatal("Could not fetch conversations.\n")
fmt.Println("Could not fetch conversations.")
return
}
@ -660,39 +659,34 @@ var editCmd = &cobra.Command{
Fatal("Could not retrieve messages for conversation: %s\n", conversation.Title)
}
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
// walk backwards through the conversation deleting messages until and
// including the last user message
toRemove := []Message{}
var toEdit *Message
var lastUserMessage *Message
for i := len(messages) - 1; i >= 0; i-- {
if i == desiredIdx {
toEdit = &messages[i]
if messages[i].Role == MessageRoleUser {
lastUserMessage = &messages[i]
}
toRemove = append(toRemove, messages[i])
messages = messages[:i]
if toEdit != nil {
if lastUserMessage != nil {
break
}
}
existingContents := toEdit.OriginalContent
if lastUserMessage == nil {
Fatal("No messages left in the conversation, nothing to edit.\n")
}
existingContents := lastUserMessage.OriginalContent
newContents := inputFromArgsOrEditor(args[1:], "# Save when finished editing\n", existingContents)
switch newContents {
case existingContents:
if newContents == existingContents {
Fatal("No edits were made.\n")
case "":
}
if newContents == "" {
Fatal("No message was provided.\n")
}