From 8ddac2f820a8d58a3f126348e1ed8d9745862a29 Mon Sep 17 00:00:00 2001 From: Matt Low Date: Sun, 23 Jun 2024 18:57:08 +0000 Subject: [PATCH] Introduce "agents" An agent is currently a name given to a system prompt and a set of tools which the agent has access to. This resolves the previous issue of the set of configured tools being available in *all* contexts, which wasn't always desired. Tools are now only available when an agent is explicitly requested using the `-a/--agent` flag. Agents are expected to be expanded on: the concept of task-specilized agents (e.g. coding), the ability to define a set of files an agent should always have access to for RAG purposes, etc. Other changes: - Removes the "tools" top-level config structure (though this is expected to come back along with the abillity to define custom tools). - Renamed `pkg/agent` to `pkg/agents` --- TODO.md | 8 +-- pkg/{agent => agents}/toolbox/dir_tree.go | 2 +- .../toolbox/file_insert_lines.go | 2 +- .../toolbox/file_replace_lines.go | 2 +- pkg/{agent => agents}/toolbox/read_dir.go | 2 +- pkg/{agent => agents}/toolbox/read_file.go | 2 +- pkg/{agent => agents}/toolbox/util/util.go | 0 pkg/{agent => agents}/toolbox/write_file.go | 2 +- pkg/{agent => agents}/tools.go | 4 +- pkg/cmd/cmd.go | 17 +++++- pkg/cmd/util/util.go | 9 +++ pkg/lmcli/config.go | 10 +++- pkg/lmcli/lmcli.go | 60 ++++++++++++++----- pkg/tui/views/chat/chat.go | 6 ++ pkg/tui/views/chat/conversation.go | 19 ++++-- 15 files changed, 108 insertions(+), 37 deletions(-) rename pkg/{agent => agents}/toolbox/dir_tree.go (98%) rename pkg/{agent => agents}/toolbox/file_insert_lines.go (98%) rename pkg/{agent => agents}/toolbox/file_replace_lines.go (98%) rename pkg/{agent => agents}/toolbox/read_dir.go (97%) rename pkg/{agent => agents}/toolbox/read_file.go (96%) rename pkg/{agent => agents}/toolbox/util/util.go (100%) rename pkg/{agent => agents}/toolbox/write_file.go (96%) rename pkg/{agent => agents}/tools.go (95%) diff --git a/TODO.md b/TODO.md index f432ba9..6554212 100644 --- a/TODO.md +++ b/TODO.md @@ -4,15 +4,15 @@ when calling anthropic? - [x] `dir_tree` tool - [x] Implement native Anthropic API tool calling -- [ ] Agents - a name given to a system prompt + set of available tools + +- [x] Agents - a name given to a system prompt + set of available tools + potentially other relevent data (e.g. external service credentials, files for RAG, etc), which the user explicitly selects (e.g. `lmcli chat --agent - pair-programmer`, `lmcli chat -a financier`). - - Specialized agents which have integrations beyond basic tool calling, + code-helper`, `lmcli chat -a financier`). + - [ ] Specialized agents which have integrations beyond basic tool calling, e.g. a coding agent which bakes in efficient code context management (only the current state of relevant files get shown to the model in the system prompt, rather than having them in the conversation messages) - - Agents may have some form of long term memory management (key-value? + - [ ] Agents may have some form of long term memory management (key-value? natural lang?). - [ ] Support for arbitrary external script tools - [ ] Search - RAG driven search of existing conversation "hey, remind me of diff --git a/pkg/agent/toolbox/dir_tree.go b/pkg/agents/toolbox/dir_tree.go similarity index 98% rename from pkg/agent/toolbox/dir_tree.go rename to pkg/agents/toolbox/dir_tree.go index 243d379..db8c926 100644 --- a/pkg/agent/toolbox/dir_tree.go +++ b/pkg/agents/toolbox/dir_tree.go @@ -7,7 +7,7 @@ import ( "strconv" "strings" - toolutil "git.mlow.ca/mlow/lmcli/pkg/agent/toolbox/util" + toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util" "git.mlow.ca/mlow/lmcli/pkg/api" ) diff --git a/pkg/agent/toolbox/file_insert_lines.go b/pkg/agents/toolbox/file_insert_lines.go similarity index 98% rename from pkg/agent/toolbox/file_insert_lines.go rename to pkg/agents/toolbox/file_insert_lines.go index 17a197a..4aa216b 100644 --- a/pkg/agent/toolbox/file_insert_lines.go +++ b/pkg/agents/toolbox/file_insert_lines.go @@ -5,7 +5,7 @@ import ( "os" "strings" - toolutil "git.mlow.ca/mlow/lmcli/pkg/agent/toolbox/util" + toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util" "git.mlow.ca/mlow/lmcli/pkg/api" ) diff --git a/pkg/agent/toolbox/file_replace_lines.go b/pkg/agents/toolbox/file_replace_lines.go similarity index 98% rename from pkg/agent/toolbox/file_replace_lines.go rename to pkg/agents/toolbox/file_replace_lines.go index ad346bb..0319beb 100644 --- a/pkg/agent/toolbox/file_replace_lines.go +++ b/pkg/agents/toolbox/file_replace_lines.go @@ -5,7 +5,7 @@ import ( "os" "strings" - toolutil "git.mlow.ca/mlow/lmcli/pkg/agent/toolbox/util" + toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util" "git.mlow.ca/mlow/lmcli/pkg/api" ) diff --git a/pkg/agent/toolbox/read_dir.go b/pkg/agents/toolbox/read_dir.go similarity index 97% rename from pkg/agent/toolbox/read_dir.go rename to pkg/agents/toolbox/read_dir.go index 02a2cfb..aced5e5 100644 --- a/pkg/agent/toolbox/read_dir.go +++ b/pkg/agents/toolbox/read_dir.go @@ -6,7 +6,7 @@ import ( "path/filepath" "strings" - toolutil "git.mlow.ca/mlow/lmcli/pkg/agent/toolbox/util" + toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util" "git.mlow.ca/mlow/lmcli/pkg/api" ) diff --git a/pkg/agent/toolbox/read_file.go b/pkg/agents/toolbox/read_file.go similarity index 96% rename from pkg/agent/toolbox/read_file.go rename to pkg/agents/toolbox/read_file.go index a35eedb..7427d0c 100644 --- a/pkg/agent/toolbox/read_file.go +++ b/pkg/agents/toolbox/read_file.go @@ -5,7 +5,7 @@ import ( "os" "strings" - toolutil "git.mlow.ca/mlow/lmcli/pkg/agent/toolbox/util" + toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util" "git.mlow.ca/mlow/lmcli/pkg/api" ) diff --git a/pkg/agent/toolbox/util/util.go b/pkg/agents/toolbox/util/util.go similarity index 100% rename from pkg/agent/toolbox/util/util.go rename to pkg/agents/toolbox/util/util.go diff --git a/pkg/agent/toolbox/write_file.go b/pkg/agents/toolbox/write_file.go similarity index 96% rename from pkg/agent/toolbox/write_file.go rename to pkg/agents/toolbox/write_file.go index 5f701a7..9c962ec 100644 --- a/pkg/agent/toolbox/write_file.go +++ b/pkg/agents/toolbox/write_file.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - toolutil "git.mlow.ca/mlow/lmcli/pkg/agent/toolbox/util" + toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util" "git.mlow.ca/mlow/lmcli/pkg/api" ) diff --git a/pkg/agent/tools.go b/pkg/agents/tools.go similarity index 95% rename from pkg/agent/tools.go rename to pkg/agents/tools.go index d920b23..aba83ed 100644 --- a/pkg/agent/tools.go +++ b/pkg/agents/tools.go @@ -1,9 +1,9 @@ -package agent +package agents import ( "fmt" - "git.mlow.ca/mlow/lmcli/pkg/agent/toolbox" + "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox" "git.mlow.ca/mlow/lmcli/pkg/api" ) diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 90dc878..f7ef736 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -51,6 +51,12 @@ func applyGenerationFlags(ctx *lmcli.Context, cmd *cobra.Command) { return ctx.GetModels(), cobra.ShellCompDirectiveDefault }) + // -a, --agent + f.StringVarP(&ctx.Config.Defaults.Agent, "agent", "a", ctx.Config.Defaults.Agent, "Which agent to interact with") + cmd.RegisterFlagCompletionFunc("agent", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return ctx.GetAgents(), cobra.ShellCompDirectiveDefault + }) + // --max-length f.IntVar(ctx.Config.Defaults.MaxTokens, "max-length", *ctx.Config.Defaults.MaxTokens, "Maximum response tokens") // --temperature @@ -65,14 +71,21 @@ func applyGenerationFlags(ctx *lmcli.Context, cmd *cobra.Command) { func validateGenerationFlags(ctx *lmcli.Context, cmd *cobra.Command) error { f := cmd.Flags() - model, err := f.GetString("model") if err != nil { return fmt.Errorf("Error parsing --model: %w", err) } - if !slices.Contains(ctx.GetModels(), model) { + if model != "" && !slices.Contains(ctx.GetModels(), model) { return fmt.Errorf("Unknown model: %s", model) } + + agent, err := f.GetString("agent") + if err != nil { + return fmt.Errorf("Error parsing --agent: %w", err) + } + if agent != "" && !slices.Contains(ctx.GetAgents(), agent) { + return fmt.Errorf("Unknown agent: %s", agent) + } return nil } diff --git a/pkg/cmd/util/util.go b/pkg/cmd/util/util.go index 2a49f04..fe98123 100644 --- a/pkg/cmd/util/util.go +++ b/pkg/cmd/util/util.go @@ -29,6 +29,15 @@ func Prompt(ctx *lmcli.Context, messages []api.Message, callback func(api.Messag } system := ctx.DefaultSystemPrompt() + + agent := ctx.GetAgent(ctx.Config.Defaults.Agent) + if agent != nil { + if agent.SystemPrompt != "" { + system = agent.SystemPrompt + } + params.ToolBag = agent.Toolbox + } + if system != "" { messages = api.ApplySystemPrompt(messages, system, false) } diff --git a/pkg/lmcli/config.go b/pkg/lmcli/config.go index 5c87114..d6a1461 100644 --- a/pkg/lmcli/config.go +++ b/pkg/lmcli/config.go @@ -15,6 +15,8 @@ type Config struct { Temperature *float32 `yaml:"temperature" default:"0.2"` SystemPrompt string `yaml:"systemPrompt,omitempty"` SystemPromptFile string `yaml:"systemPromptFile,omitempty"` + // CLI only + Agent string `yaml:"-"` } `yaml:"defaults"` Conversations *struct { TitleGenerationModel *string `yaml:"titleGenerationModel" default:"gpt-3.5-turbo"` @@ -23,9 +25,11 @@ type Config struct { Style *string `yaml:"style" default:"onedark"` Formatter *string `yaml:"formatter" default:"terminal16m"` } `yaml:"chroma"` - Tools *struct { - EnabledTools []string `yaml:"enabledTools"` - } `yaml:"tools"` + Agents []*struct { + Name string `yaml:"name"` + SystemPrompt string `yaml:"systemPrompt"` + Tools []string `yaml:"tools"` + } `yaml:"agents"` Providers []*struct { Name string `yaml:"name,omitempty"` Kind string `yaml:"kind"` diff --git a/pkg/lmcli/lmcli.go b/pkg/lmcli/lmcli.go index b48859d..5e6c620 100644 --- a/pkg/lmcli/lmcli.go +++ b/pkg/lmcli/lmcli.go @@ -6,7 +6,7 @@ import ( "path/filepath" "strings" - "git.mlow.ca/mlow/lmcli/pkg/agent" + "git.mlow.ca/mlow/lmcli/pkg/agents" "git.mlow.ca/mlow/lmcli/pkg/api" "git.mlow.ca/mlow/lmcli/pkg/api/provider/anthropic" "git.mlow.ca/mlow/lmcli/pkg/api/provider/google" @@ -18,20 +18,24 @@ import ( "gorm.io/gorm" ) +type Agent struct { + Name string + SystemPrompt string + Toolbox []api.ToolSpec +} + type Context struct { // high level app configuration, may be mutated at runtime Config Config Store ConversationStore - - Chroma *tty.ChromaHighlighter - EnabledTools []api.ToolSpec + Chroma *tty.ChromaHighlighter } func NewContext() (*Context, error) { configFile := filepath.Join(configDir(), "config.yaml") config, err := NewConfig(configFile) if err != nil { - Fatal("%v\n", err) + return nil, err } databaseFile := filepath.Join(dataDir(), "conversations.db") @@ -43,20 +47,12 @@ func NewContext() (*Context, error) { } store, err := NewSQLStore(db) if err != nil { - Fatal("%v\n", err) + return nil, err } chroma := tty.NewChromaHighlighter("markdown", *config.Chroma.Formatter, *config.Chroma.Style) - var enabledTools []api.ToolSpec - for _, toolName := range config.Tools.EnabledTools { - tool, ok := agent.AvailableTools[toolName] - if ok { - enabledTools = append(enabledTools, tool) - } - } - - return &Context{*config, store, chroma, enabledTools}, nil + return &Context{*config, store, chroma}, nil } func (c *Context) GetModels() (models []string) { @@ -82,6 +78,40 @@ func (c *Context) GetModels() (models []string) { return } +func (c *Context) GetAgents() (agents []string) { + for _, p := range c.Config.Agents { + agents = append(agents, p.Name) + } + return +} + +func (c *Context) GetAgent(name string) *Agent { + if name == "" { + return nil + } + + for _, a := range c.Config.Agents { + if name != a.Name { + continue + } + + var enabledTools []api.ToolSpec + for _, toolName := range a.Tools { + tool, ok := agents.AvailableTools[toolName] + if ok { + enabledTools = append(enabledTools, tool) + } + } + + return &Agent{ + Name: a.Name, + SystemPrompt: a.SystemPrompt, + Toolbox: enabledTools, + } + } + return nil +} + func (c *Context) DefaultSystemPrompt() string { if c.Config.Defaults.SystemPromptFile != "" { content, err := util.ReadFileContents(c.Config.Defaults.SystemPromptFile) diff --git a/pkg/tui/views/chat/chat.go b/pkg/tui/views/chat/chat.go index c07f4f2..e8b166d 100644 --- a/pkg/tui/views/chat/chat.go +++ b/pkg/tui/views/chat/chat.go @@ -144,6 +144,12 @@ func Chat(shared shared.Shared) Model { m.replyCursor.Focus() system := shared.Ctx.DefaultSystemPrompt() + + agent := shared.Ctx.GetAgent(shared.Ctx.Config.Defaults.Agent) + if agent != nil && agent.SystemPrompt != "" { + system = agent.SystemPrompt + } + if system != "" { m.messages = api.ApplySystemPrompt(m.messages, system, false) } diff --git a/pkg/tui/views/chat/conversation.go b/pkg/tui/views/chat/conversation.go index 850d5aa..00de5ee 100644 --- a/pkg/tui/views/chat/conversation.go +++ b/pkg/tui/views/chat/conversation.go @@ -6,7 +6,7 @@ import ( "fmt" "time" - "git.mlow.ca/mlow/lmcli/pkg/agent" + "git.mlow.ca/mlow/lmcli/pkg/agents" "git.mlow.ca/mlow/lmcli/pkg/api" cmdutil "git.mlow.ca/mlow/lmcli/pkg/cmd/util" "git.mlow.ca/mlow/lmcli/pkg/tui/shared" @@ -244,7 +244,12 @@ func (m *Model) persistConversation() tea.Cmd { func (m *Model) executeToolCalls(toolCalls []api.ToolCall) tea.Cmd { return func() tea.Msg { - results, err := agent.ExecuteToolCalls(toolCalls, m.Ctx.EnabledTools) + agent := m.Shared.Ctx.GetAgent(m.Shared.Ctx.Config.Defaults.Agent) + if agent == nil { + return shared.MsgError(fmt.Errorf("Attempted to execute tool calls with no agent configured")) + } + + results, err := agents.ExecuteToolCalls(toolCalls, agent.Toolbox) if err != nil { return shared.MsgError(err) } @@ -266,11 +271,15 @@ func (m *Model) promptLLM() tea.Cmd { return shared.MsgError(err) } - requestParams := api.RequestParameters{ + params := api.RequestParameters{ Model: model, MaxTokens: *m.Shared.Ctx.Config.Defaults.MaxTokens, Temperature: *m.Shared.Ctx.Config.Defaults.Temperature, - ToolBag: m.Shared.Ctx.EnabledTools, + } + + agent := m.Shared.Ctx.GetAgent(m.Shared.Ctx.Config.Defaults.Agent) + if agent != nil { + params.ToolBag = agent.Toolbox } ctx, cancel := context.WithCancel(context.Background()) @@ -283,7 +292,7 @@ func (m *Model) promptLLM() tea.Cmd { }() resp, err := provider.CreateChatCompletionStream( - ctx, requestParams, m.messages, m.chatReplyChunks, + ctx, params, m.messages, m.chatReplyChunks, ) if errors.Is(err, context.Canceled) {