Compare commits
3 Commits
db788760a3
...
77c0d1bce8
Author | SHA1 | Date | |
---|---|---|---|
77c0d1bce8 | |||
51ce74ad3a | |||
b93ee94233 |
20
README.md
20
README.md
@ -4,29 +4,25 @@
|
|||||||
|
|
||||||
Current features:
|
Current features:
|
||||||
- Perform one-shot prompts with `lmcli prompt <message>`
|
- Perform one-shot prompts with `lmcli prompt <message>`
|
||||||
- Manage persistent conversations with the `new`, `reply`, `view`, and `rm`
|
- Manage persistent conversations with the `new`, `reply`, `view`, and `rm`,
|
||||||
sub-commands.
|
`edit`, `retry`, `continue` sub-commands.
|
||||||
- Syntax highlighted output
|
- Syntax highlighted output
|
||||||
- Tool calling, see the [Tools](#tools) section.
|
- Tool calling, see the [Tools](#tools) section.
|
||||||
|
|
||||||
Planned features:
|
|
||||||
- Ask questions about content received on stdin
|
|
||||||
- Conversation editing
|
|
||||||
|
|
||||||
Maybe features:
|
Maybe features:
|
||||||
|
- Chat-like interface (`lmcli chat`) for rapid back-and-forth conversations
|
||||||
- Support for additional models/APIs besides just OpenAI
|
- Support for additional models/APIs besides just OpenAI
|
||||||
- Natural language image generation, iterative editing
|
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
Tools must be explicitly enabled by adding the tool's name to the
|
Tools must be explicitly enabled by adding the tool's name to the
|
||||||
`openai.enabledTools` array in `config.yaml`.
|
`openai.enabledTools` array in `config.yaml`.
|
||||||
|
|
||||||
Note: all filesystem related tools operate relative to the current directory
|
Note: all filesystem related tools operate relative to the current directory
|
||||||
only. They do not accept absolute paths, and all efforts are made to ensure
|
only. They do not accept absolute paths, and efforts are made to ensure they
|
||||||
they cannot escape above the working directory (not quite using chroot, but in
|
cannot escape above the working directory). **Close attention must be paid to
|
||||||
effect). **Close attention must be paid to where you are running `lmcli`, as
|
where you are running `lmcli`, as the model could at any time decide to use one
|
||||||
the model could at any time decide to use one of these tools to discover and
|
of these tools to discover and read potentially sensitive information from your
|
||||||
read potentially sensitive information from your filesystem.**
|
filesystem.**
|
||||||
|
|
||||||
It's best to only have tools enabled in `config.yaml` when you intend to be
|
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
|
using them, since their descriptions (see `pkg/cli/functions.go`) count towards
|
||||||
|
@ -18,7 +18,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Limit to number of conversations shown with `ls`, without --all
|
// Limit number of conversations shown with `ls`, without --all
|
||||||
LS_LIMIT int = 25
|
LS_LIMIT int = 25
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,14 +32,15 @@ func init() {
|
|||||||
cmd.MarkFlagsMutuallyExclusive("system-prompt", "system-prompt-file")
|
cmd.MarkFlagsMutuallyExclusive("system-prompt", "system-prompt-file")
|
||||||
}
|
}
|
||||||
|
|
||||||
lsCmd.Flags().Bool("all", false, fmt.Sprintf("Show all conversations, by default only the last %d are shown", LS_LIMIT))
|
listCmd.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")
|
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(
|
rootCmd.AddCommand(
|
||||||
cloneCmd,
|
cloneCmd,
|
||||||
continueCmd,
|
continueCmd,
|
||||||
editCmd,
|
editCmd,
|
||||||
lsCmd,
|
listCmd,
|
||||||
newCmd,
|
newCmd,
|
||||||
promptCmd,
|
promptCmd,
|
||||||
renameCmd,
|
renameCmd,
|
||||||
@ -114,8 +115,7 @@ func lookupConversationE(shortName string) (*Conversation, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleConversationReply handles sending messages to an existing
|
// handleConversationReply handles sending messages to an existing
|
||||||
// conversation, optionally persisting them. It displays the entire
|
// conversation, optionally persisting both the sent replies and responses.
|
||||||
// conversation before
|
|
||||||
func handleConversationReply(c *Conversation, persist bool, toSend ...Message) {
|
func handleConversationReply(c *Conversation, persist bool, toSend ...Message) {
|
||||||
existing, err := store.Messages(c)
|
existing, err := store.Messages(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -182,14 +182,15 @@ var rootCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var lsCmd = &cobra.Command{
|
var listCmd = &cobra.Command{
|
||||||
Use: "ls",
|
Use: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
Short: "List conversations",
|
Short: "List conversations",
|
||||||
Long: `List conversations in order of recent activity`,
|
Long: `List conversations in order of recent activity`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
conversations, err := store.Conversations()
|
conversations, err := store.Conversations()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Could not fetch conversations.")
|
Fatal("Could not fetch conversations.\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -659,34 +660,39 @@ var editCmd = &cobra.Command{
|
|||||||
Fatal("Could not retrieve messages for conversation: %s\n", conversation.Title)
|
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
|
// walk backwards through the conversation deleting messages until and
|
||||||
// including the last user message
|
// including the last user message
|
||||||
toRemove := []Message{}
|
toRemove := []Message{}
|
||||||
var lastUserMessage *Message
|
var toEdit *Message
|
||||||
for i := len(messages) - 1; i >= 0; i-- {
|
for i := len(messages) - 1; i >= 0; i-- {
|
||||||
if messages[i].Role == MessageRoleUser {
|
if i == desiredIdx {
|
||||||
lastUserMessage = &messages[i]
|
toEdit = &messages[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
toRemove = append(toRemove, messages[i])
|
toRemove = append(toRemove, messages[i])
|
||||||
messages = messages[:i]
|
messages = messages[:i]
|
||||||
if lastUserMessage != nil {
|
if toEdit != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if lastUserMessage == nil {
|
existingContents := toEdit.OriginalContent
|
||||||
Fatal("No messages left in the conversation, nothing to edit.\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
existingContents := lastUserMessage.OriginalContent
|
|
||||||
|
|
||||||
newContents := inputFromArgsOrEditor(args[1:], "# Save when finished editing\n", existingContents)
|
newContents := inputFromArgsOrEditor(args[1:], "# Save when finished editing\n", existingContents)
|
||||||
if newContents == existingContents {
|
switch newContents {
|
||||||
|
case existingContents:
|
||||||
Fatal("No edits were made.\n")
|
Fatal("No edits were made.\n")
|
||||||
}
|
case "":
|
||||||
|
|
||||||
if newContents == "" {
|
|
||||||
Fatal("No message was provided.\n")
|
Fatal("No message was provided.\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user