diff --git a/README.md b/README.md index 4b90183..3fcf81d 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,7 @@ Note: For proper SSE handling, you should use a client that supports SSE nativel "standalone_job": false, "random_identity": false, "initiate_conversations": true, + "enable_planning": true, "identity_guidance": "You are a helpful assistant.", "periodic_runs": "0 * * * *", "permanent_goal": "Help users with their questions.", diff --git a/core/action/custom.go b/core/action/custom.go index 7ca84c4..bd886e2 100644 --- a/core/action/custom.go +++ b/core/action/custom.go @@ -75,6 +75,10 @@ func (a *CustomAction) initializeInterpreter() error { return nil } +func (a *CustomAction) Plannable() bool { + return true +} + func (a *CustomAction) Run(ctx context.Context, params ActionParams) (ActionResult, error) { v, err := a.i.Eval(fmt.Sprintf("%s.Run", a.config["name"])) if err != nil { diff --git a/core/action/intention.go b/core/action/intention.go index 95c540a..082efb1 100644 --- a/core/action/intention.go +++ b/core/action/intention.go @@ -25,6 +25,10 @@ func (a *IntentAction) Run(context.Context, ActionParams) (ActionResult, error) return ActionResult{}, nil } +func (a *IntentAction) Plannable() bool { + return false +} + func (a *IntentAction) Definition() ActionDefinition { return ActionDefinition{ Name: "pick_tool", diff --git a/core/action/newconversation.go b/core/action/newconversation.go index 067f54f..d8c7dc1 100644 --- a/core/action/newconversation.go +++ b/core/action/newconversation.go @@ -22,6 +22,10 @@ func (a *ConversationAction) Run(context.Context, ActionParams) (ActionResult, e return ActionResult{}, nil } +func (a *ConversationAction) Plannable() bool { + return false +} + func (a *ConversationAction) Definition() ActionDefinition { return ActionDefinition{ Name: ConversationActionName, diff --git a/core/action/noreply.go b/core/action/noreply.go index af00465..4215176 100644 --- a/core/action/noreply.go +++ b/core/action/noreply.go @@ -16,6 +16,10 @@ func (a *StopAction) Run(context.Context, ActionParams) (ActionResult, error) { return ActionResult{}, nil } +func (a *StopAction) Plannable() bool { + return false +} + func (a *StopAction) Definition() ActionDefinition { return ActionDefinition{ Name: StopActionName, diff --git a/core/action/plan.go b/core/action/plan.go index 6c6cfb5..b504d9d 100644 --- a/core/action/plan.go +++ b/core/action/plan.go @@ -10,22 +10,31 @@ import ( // used by the LLM to schedule more actions const PlanActionName = "plan" -func NewPlan() *PlanAction { - return &PlanAction{} +func NewPlan(plannableActions []string) *PlanAction { + return &PlanAction{ + plannables: plannableActions, + } } -type PlanAction struct{} +type PlanAction struct { + plannables []string +} type PlanResult struct { Subtasks []PlanSubtask `json:"subtasks"` + Goal string `json:"goal"` } type PlanSubtask struct { Action string `json:"action"` Reasoning string `json:"reasoning"` } -func (a *PlanAction) Run(context.Context, ActionParams) (string, error) { - return "no-op", nil +func (a *PlanAction) Run(context.Context, ActionParams) (ActionResult, error) { + return ActionResult{}, nil +} + +func (a *PlanAction) Plannable() bool { + return false } func (a *PlanAction) Definition() ActionDefinition { @@ -40,6 +49,7 @@ func (a *PlanAction) Definition() ActionDefinition { "action": { Type: jsonschema.String, Description: "The action to call", + Enum: a.plannables, }, "reasoning": { Type: jsonschema.String, @@ -47,7 +57,11 @@ func (a *PlanAction) Definition() ActionDefinition { }, }, }, + "goal": { + Type: jsonschema.String, + Description: "The goal of this plan", + }, }, - Required: []string{"subtasks"}, + Required: []string{"subtasks", "goal"}, } } diff --git a/core/action/reasoning.go b/core/action/reasoning.go index acd3262..f8dc78d 100644 --- a/core/action/reasoning.go +++ b/core/action/reasoning.go @@ -23,6 +23,10 @@ func (a *ReasoningAction) Run(context.Context, ActionParams) (ActionResult, erro return ActionResult{}, nil } +func (a *ReasoningAction) Plannable() bool { + return false +} + func (a *ReasoningAction) Definition() ActionDefinition { return ActionDefinition{ Name: "pick_action", diff --git a/core/action/reply.go b/core/action/reply.go index db47898..33b6627 100644 --- a/core/action/reply.go +++ b/core/action/reply.go @@ -25,6 +25,10 @@ func (a *ReplyAction) Run(context.Context, ActionParams) (string, error) { return "no-op", nil } +func (a *ReplyAction) Plannable() bool { + return false +} + func (a *ReplyAction) Definition() ActionDefinition { return ActionDefinition{ Name: ReplyActionName, diff --git a/core/action/state.go b/core/action/state.go index 5c9d990..d0fe4d0 100644 --- a/core/action/state.go +++ b/core/action/state.go @@ -37,6 +37,10 @@ func (a *StateAction) Run(context.Context, ActionParams) (ActionResult, error) { return ActionResult{Result: "internal state has been updated"}, nil } +func (a *StateAction) Plannable() bool { + return false +} + func (a *StateAction) Definition() ActionDefinition { return ActionDefinition{ Name: StateActionName, diff --git a/core/agent/actions.go b/core/agent/actions.go index a01bf5e..2ba0675 100644 --- a/core/agent/actions.go +++ b/core/agent/actions.go @@ -27,6 +27,7 @@ type ActionCurrentState struct { type Action interface { Run(ctx context.Context, action action.ActionParams) (action.ActionResult, error) Definition() action.ActionDefinition + Plannable() bool } type Actions []Action @@ -211,8 +212,76 @@ func (a *Agent) generateParameters(ctx context.Context, pickTemplate string, act ) } +func (a *Agent) handlePlanning(ctx context.Context, job *Job, chosenAction Action, actionParams action.ActionParams, reasoning string, pickTemplate string) error { + // Planning: run all the actions in sequence + if !chosenAction.Definition().Name.Is(action.PlanActionName) { + return nil + } + + planResult := action.PlanResult{} + if err := actionParams.Unmarshal(&planResult); err != nil { + return fmt.Errorf("error unmarshalling plan result: %w", err) + } + + xlog.Info("[Planning] starts", "agent", a.Character.Name, "goal", planResult.Goal) + for _, s := range planResult.Subtasks { + xlog.Info("[Planning] subtask", "agent", a.Character.Name, "action", s.Action, "reasoning", s.Reasoning) + } + + if len(planResult.Subtasks) == 0 { + return fmt.Errorf("no subtasks") + } + + // Execute all subtasks in sequence + for _, subtask := range planResult.Subtasks { + xlog.Info("[subtask] Generating parameters", + "agent", a.Character.Name, + "action", subtask.Action, + "reasoning", reasoning, + ) + + action := a.availableActions().Find(subtask.Action) + + params, err := a.generateParameters(ctx, pickTemplate, action, a.currentConversation, fmt.Sprintf("%s, overall goal is: %s", subtask.Reasoning, planResult.Goal)) + if err != nil { + return fmt.Errorf("error generating action's parameters: %w", err) + + } + actionParams = params.actionParams + + result, err := a.runAction(action, actionParams) + if err != nil { + return fmt.Errorf("error running action: %w", err) + } + + stateResult := ActionState{ActionCurrentState{action, actionParams, subtask.Reasoning}, result} + job.Result.SetResult(stateResult) + job.CallbackWithResult(stateResult) + xlog.Debug("[subtask] Action executed", "agent", a.Character.Name, "action", action.Definition().Name, "result", result) + a.addFunctionResultToConversation(action, actionParams, result) + } + + return nil +} + func (a *Agent) availableActions() Actions { // defaultActions := append(a.options.userActions, action.NewReply()) + + addPlanAction := func(actions Actions) Actions { + if !a.options.canPlan { + return actions + } + plannablesActions := []string{} + for _, a := range actions { + if a.Plannable() { + plannablesActions = append(plannablesActions, a.Definition().Name.String()) + } + } + planAction := action.NewPlan(plannablesActions) + actions = append(actions, planAction) + return actions + } + defaultActions := append(a.mcpActions, a.options.userActions...) if a.options.initiateConversations && a.selfEvaluationInProgress { // && self-evaluation.. @@ -224,7 +293,7 @@ func (a *Agent) availableActions() Actions { // acts = append(acts, action.NewStop()) // } - return acts + return addPlanAction(acts) } if a.options.canStopItself { @@ -232,14 +301,14 @@ func (a *Agent) availableActions() Actions { if a.options.enableHUD { acts = append(acts, action.NewState()) } - return acts + return addPlanAction(acts) } if a.options.enableHUD { - return append(defaultActions, action.NewState()) + return addPlanAction(append(defaultActions, action.NewState())) } - return defaultActions + return addPlanAction(defaultActions) } func (a *Agent) prepareHUD() (promptHUD *PromptHUD) { diff --git a/core/agent/agent.go b/core/agent/agent.go index 280941d..d2fe80a 100644 --- a/core/agent/agent.go +++ b/core/agent/agent.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "strings" "sync" "time" @@ -571,6 +570,11 @@ func (a *Agent) consumeJob(job *Job, role string) { return } + if err := a.handlePlanning(ctx, job, chosenAction, actionParams, reasoning, pickTemplate); err != nil { + job.Result.Finish(fmt.Errorf("error running action: %w", err)) + return + } + if !job.Callback(ActionCurrentState{chosenAction, actionParams, reasoning}) { job.Result.SetResult(ActionState{ActionCurrentState{chosenAction, actionParams, reasoning}, action.ActionResult{Result: "stopped by callback"}}) job.Result.Conversation = a.currentConversation @@ -620,27 +624,7 @@ func (a *Agent) consumeJob(job *Job, role string) { job.CallbackWithResult(stateResult) xlog.Debug("Action executed", "agent", a.Character.Name, "action", chosenAction.Definition().Name, "result", result) - // calling the function - a.currentConversation = append(a.currentConversation, openai.ChatCompletionMessage{ - Role: "assistant", - ToolCalls: []openai.ToolCall{ - { - Type: openai.ToolTypeFunction, - Function: openai.FunctionCall{ - Name: chosenAction.Definition().Name.String(), - Arguments: actionParams.String(), - }, - }, - }, - }) - - // result of calling the function - a.currentConversation = append(a.currentConversation, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleTool, - Content: result.Result, - Name: chosenAction.Definition().Name.String(), - ToolCallID: chosenAction.Definition().Name.String(), - }) + a.addFunctionResultToConversation(chosenAction, actionParams, result) //a.currentConversation = append(a.currentConversation, messages...) //a.currentConversation = messages @@ -776,8 +760,7 @@ func (a *Agent) consumeJob(job *Job, role string) { } // If we didn't got any message, we can use the response from the action - if chosenAction.Definition().Name.Is(action.ReplyActionName) && msg.Content == "" || - strings.Contains(msg.Content, "") { + if chosenAction.Definition().Name.Is(action.ReplyActionName) && msg.Content == "" { xlog.Info("No output returned from conversation, using the action response as a reply " + replyResponse.Message) msg = openai.ChatCompletionMessage{ @@ -794,6 +777,30 @@ func (a *Agent) consumeJob(job *Job, role string) { job.Result.Finish(nil) } +func (a *Agent) addFunctionResultToConversation(chosenAction Action, actionParams action.ActionParams, result action.ActionResult) { + // calling the function + a.currentConversation = append(a.currentConversation, openai.ChatCompletionMessage{ + Role: "assistant", + ToolCalls: []openai.ToolCall{ + { + Type: openai.ToolTypeFunction, + Function: openai.FunctionCall{ + Name: chosenAction.Definition().Name.String(), + Arguments: actionParams.String(), + }, + }, + }, + }) + + // result of calling the function + a.currentConversation = append(a.currentConversation, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + Content: result.Result, + Name: chosenAction.Definition().Name.String(), + ToolCallID: chosenAction.Definition().Name.String(), + }) +} + // This is running in the background. func (a *Agent) periodicallyRun(timer *time.Timer) { // Remember always to reset the timer - if we don't the agent will stop.. diff --git a/core/agent/agent_test.go b/core/agent/agent_test.go index adbb9fa..88314dc 100644 --- a/core/agent/agent_test.go +++ b/core/agent/agent_test.go @@ -36,6 +36,10 @@ type TestAction struct { response map[string]string } +func (a *TestAction) Plannable() bool { + return true +} + func (a *TestAction) Run(c context.Context, p action.ActionParams) (action.ActionResult, error) { for k, r := range a.response { if strings.Contains(strings.ToLower(p.String()), strings.ToLower(k)) { diff --git a/core/agent/mcp.go b/core/agent/mcp.go index d933283..82a4350 100644 --- a/core/agent/mcp.go +++ b/core/agent/mcp.go @@ -26,6 +26,10 @@ type mcpAction struct { toolDescription string } +func (a *mcpAction) Plannable() bool { + return true +} + func (m *mcpAction) Run(ctx context.Context, params action.ActionParams) (action.ActionResult, error) { resp, err := m.mcpClient.CallTool(ctx, m.toolName, params) if err != nil { diff --git a/core/agent/options.go b/core/agent/options.go index 2dbe551..da27302 100644 --- a/core/agent/options.go +++ b/core/agent/options.go @@ -26,6 +26,7 @@ type options struct { canStopItself bool initiateConversations bool forceReasoning bool + canPlan bool characterfile string statefile string context context.Context @@ -127,6 +128,11 @@ var EnableInitiateConversations = func(o *options) error { return nil } +var EnablePlanning = func(o *options) error { + o.canPlan = true + return nil +} + // EnableStandaloneJob is an option to enable the agent // to run jobs in the background automatically var EnableStandaloneJob = func(o *options) error { diff --git a/core/state/config.go b/core/state/config.go index e1692c3..e52588d 100644 --- a/core/state/config.go +++ b/core/state/config.go @@ -47,6 +47,7 @@ type AgentConfig struct { StandaloneJob bool `json:"standalone_job" form:"standalone_job"` RandomIdentity bool `json:"random_identity" form:"random_identity"` InitiateConversations bool `json:"initiate_conversations" form:"initiate_conversations"` + CanPlan bool `json:"enable_planning" form:"enable_planning"` IdentityGuidance string `json:"identity_guidance" form:"identity_guidance"` PeriodicRuns string `json:"periodic_runs" form:"periodic_runs"` PermanentGoal string `json:"permanent_goal" form:"permanent_goal"` diff --git a/core/state/pool.go b/core/state/pool.go index 2092dcf..120c64a 100644 --- a/core/state/pool.go +++ b/core/state/pool.go @@ -332,6 +332,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error opts = append(opts, CanStopItself) } + if config.CanPlan { + opts = append(opts, EnablePlanning) + } + if config.InitiateConversations { opts = append(opts, EnableInitiateConversations) } diff --git a/services/actions/browse.go b/services/actions/browse.go index c99d738..478ec6d 100644 --- a/services/actions/browse.go +++ b/services/actions/browse.go @@ -66,3 +66,7 @@ func (a *BrowseAction) Definition() action.ActionDefinition { Required: []string{"url"}, } } + +func (a *BrowseAction) Plannable() bool { + return true +} diff --git a/services/actions/callagents.go b/services/actions/callagents.go index e09d704..37a5fc8 100644 --- a/services/actions/callagents.go +++ b/services/actions/callagents.go @@ -85,3 +85,7 @@ func (a *CallAgentAction) Definition() action.ActionDefinition { Required: []string{"agent_name", "message"}, } } + +func (a *CallAgentAction) Plannable() bool { + return true +} diff --git a/services/actions/counter.go b/services/actions/counter.go index 1141faa..22b4397 100644 --- a/services/actions/counter.go +++ b/services/actions/counter.go @@ -44,7 +44,7 @@ func (a *CounterAction) Run(ctx context.Context, params action.ActionParams) (ac // Get current value or initialize if it doesn't exist currentValue, exists := a.counters[request.Name] - + // Update the counter newValue := currentValue + request.Adjustment a.counters[request.Name] = newValue @@ -93,3 +93,6 @@ func (a *CounterAction) Definition() action.ActionDefinition { } } +func (a *CounterAction) Plannable() bool { + return true +} diff --git a/services/actions/genimage.go b/services/actions/genimage.go index 69626d2..b395923 100644 --- a/services/actions/genimage.go +++ b/services/actions/genimage.go @@ -92,3 +92,7 @@ func (a *GenImageAction) Definition() action.ActionDefinition { Required: []string{"prompt"}, } } + +func (a *GenImageAction) Plannable() bool { + return true +} diff --git a/services/actions/githubissuecloser.go b/services/actions/githubissuecloser.go index 195d30c..0db15d8 100644 --- a/services/actions/githubissuecloser.go +++ b/services/actions/githubissuecloser.go @@ -116,3 +116,7 @@ func (g *GithubIssuesCloser) Definition() action.ActionDefinition { Required: []string{"issue_number", "repository", "owner"}, } } + +func (a *GithubIssuesCloser) Plannable() bool { + return true +} diff --git a/services/actions/githubissuecomment.go b/services/actions/githubissuecomment.go index 21e4181..0c41557 100644 --- a/services/actions/githubissuecomment.go +++ b/services/actions/githubissuecomment.go @@ -103,3 +103,7 @@ func (g *GithubIssuesCommenter) Definition() action.ActionDefinition { Required: []string{"issue_number", "repository", "owner", "comment"}, } } + +func (a *GithubIssuesCommenter) Plannable() bool { + return true +} diff --git a/services/actions/githubissuelabeler.go b/services/actions/githubissuelabeler.go index 386ee9e..e3d7fbc 100644 --- a/services/actions/githubissuelabeler.go +++ b/services/actions/githubissuelabeler.go @@ -118,3 +118,7 @@ func (g *GithubIssuesLabeler) Definition() action.ActionDefinition { Required: []string{"issue_number", "repository", "owner", "label"}, } } + +func (a *GithubIssuesLabeler) Plannable() bool { + return true +} diff --git a/services/actions/githubissueopener.go b/services/actions/githubissueopener.go index 58b5f07..2622734 100644 --- a/services/actions/githubissueopener.go +++ b/services/actions/githubissueopener.go @@ -109,3 +109,7 @@ func (g *GithubIssuesOpener) Definition() action.ActionDefinition { Required: []string{"title", "text", "owner", "repository"}, } } + +func (a *GithubIssuesOpener) Plannable() bool { + return true +} diff --git a/services/actions/githubissuereader.go b/services/actions/githubissuereader.go index 9d41076..d1515d9 100644 --- a/services/actions/githubissuereader.go +++ b/services/actions/githubissuereader.go @@ -97,3 +97,7 @@ func (g *GithubIssuesReader) Definition() action.ActionDefinition { Required: []string{"issue_number", "repository", "owner"}, } } + +func (a *GithubIssuesReader) Plannable() bool { + return true +} diff --git a/services/actions/githubissuesearch.go b/services/actions/githubissuesearch.go index d93619b..5b95d81 100644 --- a/services/actions/githubissuesearch.go +++ b/services/actions/githubissuesearch.go @@ -106,3 +106,7 @@ func (g *GithubIssueSearch) Definition() action.ActionDefinition { Required: []string{"query", "repository", "owner"}, } } + +func (a *GithubIssueSearch) Plannable() bool { + return true +} diff --git a/services/actions/githubrepositorycreateupdatecontent.go b/services/actions/githubrepositorycreateupdatecontent.go index 1f205cf..672b700 100644 --- a/services/actions/githubrepositorycreateupdatecontent.go +++ b/services/actions/githubrepositorycreateupdatecontent.go @@ -142,3 +142,7 @@ func (g *GithubRepositoryCreateOrUpdateContent) Definition() action.ActionDefini Required: []string{"path", "repository", "owner", "content"}, } } + +func (a *GithubRepositoryCreateOrUpdateContent) Plannable() bool { + return true +} diff --git a/services/actions/githubrepositorygetcontent.go b/services/actions/githubrepositorygetcontent.go index 74e2d55..4726983 100644 --- a/services/actions/githubrepositorygetcontent.go +++ b/services/actions/githubrepositorygetcontent.go @@ -107,3 +107,7 @@ func (g *GithubRepositoryGetContent) Definition() action.ActionDefinition { Required: []string{"path", "repository", "owner"}, } } + +func (a *GithubRepositoryGetContent) Plannable() bool { + return true +} diff --git a/services/actions/githubrepositoryreadme.go b/services/actions/githubrepositoryreadme.go index b62956e..f6b3d34 100644 --- a/services/actions/githubrepositoryreadme.go +++ b/services/actions/githubrepositoryreadme.go @@ -73,3 +73,7 @@ func (g *GithubRepositoryREADME) Definition() action.ActionDefinition { Required: []string{"repository", "owner"}, } } + +func (a *GithubRepositoryREADME) Plannable() bool { + return true +} diff --git a/services/actions/scrape.go b/services/actions/scrape.go index 8f69020..678f31a 100644 --- a/services/actions/scrape.go +++ b/services/actions/scrape.go @@ -54,3 +54,7 @@ func (a *ScraperAction) Definition() action.ActionDefinition { Required: []string{"url"}, } } + +func (a *ScraperAction) Plannable() bool { + return true +} diff --git a/services/actions/search.go b/services/actions/search.go index b5e7e20..086512b 100644 --- a/services/actions/search.go +++ b/services/actions/search.go @@ -85,3 +85,7 @@ func (a *SearchAction) Definition() action.ActionDefinition { Required: []string{"query"}, } } + +func (a *SearchAction) Plannable() bool { + return true +} diff --git a/services/actions/sendmail.go b/services/actions/sendmail.go index 1b23742..3e4b342 100644 --- a/services/actions/sendmail.go +++ b/services/actions/sendmail.go @@ -76,3 +76,7 @@ func (a *SendMailAction) Definition() action.ActionDefinition { Required: []string{"to", "subject", "message"}, } } + +func (a *SendMailAction) Plannable() bool { + return true +} diff --git a/services/actions/shell.go b/services/actions/shell.go index 36302bb..fe7956b 100644 --- a/services/actions/shell.go +++ b/services/actions/shell.go @@ -134,3 +134,7 @@ func sshCommand(privateKey, command, user, host string) (string, error) { return string(output), nil } + +func (a *ShellAction) Plannable() bool { + return true +} diff --git a/services/actions/twitter_post.go b/services/actions/twitter_post.go index 26c09c9..d438c0f 100644 --- a/services/actions/twitter_post.go +++ b/services/actions/twitter_post.go @@ -58,3 +58,7 @@ func (a *PostTweetAction) Definition() action.ActionDefinition { Required: []string{"text"}, } } + +func (a *PostTweetAction) Plannable() bool { + return true +} diff --git a/services/actions/wikipedia.go b/services/actions/wikipedia.go index 936b5cd..c3af3d5 100644 --- a/services/actions/wikipedia.go +++ b/services/actions/wikipedia.go @@ -48,3 +48,7 @@ func (a *WikipediaAction) Definition() action.ActionDefinition { Required: []string{"query"}, } } + +func (a *WikipediaAction) Plannable() bool { + return true +} diff --git a/webui/views/partials/agent-form.html b/webui/views/partials/agent-form.html index c36ee32..42353ad 100644 --- a/webui/views/partials/agent-form.html +++ b/webui/views/partials/agent-form.html @@ -253,6 +253,16 @@ Initiate Conversations + +
+ +