From 9173156e4075d80bc25e75ac6df47cc58249796b Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Thu, 4 Apr 2024 00:19:56 +0200 Subject: [PATCH] Update state action --- action/state.go | 70 +++++++++++++++++++++ agent/actions.go | 30 +++++++-- agent/agent.go | 157 +++++++++++++++++++++++++++-------------------- agent/options.go | 12 +++- agent/state.go | 23 ++----- 5 files changed, 197 insertions(+), 95 deletions(-) create mode 100644 action/state.go diff --git a/action/state.go b/action/state.go new file mode 100644 index 0000000..649928f --- /dev/null +++ b/action/state.go @@ -0,0 +1,70 @@ +package action + +import ( + "github.com/sashabaranov/go-openai/jsonschema" +) + +const StateActionName = "update_state" + +func NewState() *StateAction { + return &StateAction{} +} + +type StateAction struct{} + +// State is the structure +// that is used to keep track of the current state +// and the Agent's short memory that it can update +// Besides a long term memory that is accessible by the agent (With vector database), +// And a context memory (that is always powered by a vector database), +// this memory is the shorter one that the LLM keeps across conversation and across its +// reasoning process's and life time. +// TODO: A special action is then used to let the LLM itself update its memory +// periodically during self-processing, and the same action is ALSO exposed +// during the conversation to let the user put for example, a new goal to the agent. +type StateResult struct { + NowDoing string `json:"doing_now"` + DoingNext string `json:"doing_next"` + DoneHistory []string `json:"done_history"` + Memories []string `json:"memories"` + Goal string `json:"goal"` +} + +func (a *StateAction) Run(ActionParams) (string, error) { + return "no-op", nil +} + +func (a *StateAction) Definition() ActionDefinition { + return ActionDefinition{ + Name: StateActionName, + Description: "update the agent state (short memory) with the current state of the conversation.", + Properties: map[string]jsonschema.Definition{ + "goal": { + Type: jsonschema.String, + Description: "The current goal of the agent.", + }, + "doing_next": { + Type: jsonschema.String, + Description: "The next action the agent will do.", + }, + "done_history": { + Type: jsonschema.Array, + Items: &jsonschema.Definition{ + Type: jsonschema.String, + }, + Description: "A list of actions that the agent has done.", + }, + "now_doing": { + Type: jsonschema.String, + Description: "The current action the agent is doing.", + }, + "memories": { + Type: jsonschema.Array, + Items: &jsonschema.Definition{ + Type: jsonschema.String, + }, + Description: "A list of memories to keep between conversations.", + }, + }, + } +} diff --git a/agent/actions.go b/agent/actions.go index 917b492..a6bda21 100644 --- a/agent/actions.go +++ b/agent/actions.go @@ -88,13 +88,23 @@ func (a *Agent) decision( func (a *Agent) generateParameters(ctx context.Context, action Action, conversation []openai.ChatCompletionMessage) (*decisionResult, error) { return a.decision(ctx, conversation, - a.options.actions.ToTools(), + a.systemActions().ToTools(), action.Definition().Name) } +func (a *Agent) systemActions() Actions { + if a.options.enableHUD { + return append(a.options.userActions, action.NewReply(), action.NewState()) + } + + return append(a.options.userActions, action.NewReply()) +} + func (a *Agent) prepareHUD() PromptHUD { return PromptHUD{ - Character: a.Character, + Character: a.Character, + CurrentState: *a.currentState, + PermanentGoal: a.options.permanentGoal, } } @@ -105,6 +115,14 @@ const hudTemplate = `You have a character and your replies and actions might be {{end}}{{if .Character.Hobbies}}Hobbies: {{.Character.Hobbies}} {{end}}{{if .Character.MusicTaste}}Music taste: {{.Character.MusicTaste}} {{end}} + +This is your current state: +{{if .CurrentState.NowDoing}}NowDoing: {{.CurrentState.NowDoing}} {{end}} +{{if .CurrentState.DoingNext}}DoingNext: {{.CurrentState.DoingNext}} {{end}} +{{if .PermanentGoal}}Your permanent goal is: {{.PermanentGoal}} {{end}} +{{if .CurrentState.Goal}}Your current goal is: {{.CurrentState.Goal}} {{end}} +You have done: {{range .CurrentState.DoneHistory}}{{.}} {{end}} +You have a short memory with: {{range .CurrentState.Memories}}{{.}} {{end}} ` // pickAction picks an action based on the conversation @@ -122,8 +140,8 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai. return nil, "", err } // Get all the actions definitions - definitions := []action.ActionDefinition{action.NewReply().Definition()} - for _, m := range a.options.actions { + definitions := []action.ActionDefinition{} + for _, m := range a.systemActions() { definitions = append(definitions, m.Definition()) } @@ -148,7 +166,7 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai. // Get all the available actions IDs actionsID := []string{} - for _, m := range a.options.actions { + for _, m := range a.systemActions() { actionsID = append(actionsID, m.Definition().Name.String()) } @@ -215,7 +233,7 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai. } // Find the action - chosenAction := append(a.options.actions, action.NewReply()).Find(actionChoice.Tool) + chosenAction := a.systemActions().Find(actionChoice.Tool) if chosenAction == nil { return nil, "", fmt.Errorf("no action found for intent:" + actionChoice.Tool) } diff --git a/agent/agent.go b/agent/agent.go index 0293eb8..e6b8d70 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -47,15 +47,15 @@ const ( type Agent struct { sync.Mutex - options *options - Character Character - client *openai.Client - jobQueue chan *Job - actionContext *action.ActionContext - context *action.ActionContext - availableActions []Action + options *options + Character Character + client *openai.Client + jobQueue chan *Job + actionContext *action.ActionContext + context *action.ActionContext currentReasoning string + currentState *action.StateResult nextAction Action currentConversation []openai.ChatCompletionMessage } @@ -78,12 +78,12 @@ func New(opts ...Option) (*Agent, error) { ctx, cancel := context.WithCancel(c) a := &Agent{ - jobQueue: make(chan *Job), - options: options, - client: client, - Character: options.character, - context: action.NewContext(ctx, cancel), - availableActions: options.actions, + jobQueue: make(chan *Job), + options: options, + client: client, + Character: options.character, + currentState: &action.StateResult{}, + context: action.NewContext(ctx, cancel), } if a.options.randomIdentity { @@ -135,6 +135,30 @@ func (a *Agent) Stop() { a.context.Cancel() } +func (a *Agent) runAction(chosenAction Action, decisionResult *decisionResult) (result string, err error) { + for _, action := range a.systemActions() { + if action.Definition().Name == chosenAction.Definition().Name { + if result, err = action.Run(decisionResult.actionParams); err != nil { + return "", fmt.Errorf("error running action: %w", err) + } + } + } + + if chosenAction.Definition().Name.Is(action.StateActionName) { + // We need to store the result in the state + state := action.StateResult{} + + err = decisionResult.actionParams.Unmarshal(&result) + if err != nil { + return "", err + } + // update the current state with the one we just got from the action + a.currentState = &state + } + + return result, nil +} + func (a *Agent) consumeJob(job *Job, role string) { // Consume the job and generate a response a.Lock() @@ -175,9 +199,9 @@ func (a *Agent) consumeJob(job *Job, role string) { } } - if chosenAction == nil || chosenAction.Definition().Name.Is(action.ReplyActionName) { - job.Result.SetResult(ActionState{ActionCurrentState{nil, nil, "No action to do, just reply"}, ""}) - job.Result.Finish(nil) + if chosenAction == nil { + //job.Result.SetResult(ActionState{ActionCurrentState{nil, nil, "No action to do, just reply"}, ""}) + job.Result.Finish(fmt.Errorf("no action to do")) return } @@ -187,70 +211,68 @@ func (a *Agent) consumeJob(job *Job, role string) { return } + if params.actionParams == nil { + job.Result.Finish(fmt.Errorf("no parameters")) + return + } + if !job.Callback(ActionCurrentState{chosenAction, params.actionParams, reasoning}) { job.Result.SetResult(ActionState{ActionCurrentState{chosenAction, params.actionParams, reasoning}, "stopped by callback"}) job.Result.Finish(nil) return } - if params.actionParams == nil { - job.Result.Finish(fmt.Errorf("no parameters")) - return - } - - var result string - for _, action := range a.options.actions { - if action.Definition().Name == chosenAction.Definition().Name { - if result, err = action.Run(params.actionParams); err != nil { - job.Result.Finish(fmt.Errorf("error running action: %w", err)) - return - } + if !chosenAction.Definition().Name.Is(action.ReplyActionName) { + result, err := a.runAction(chosenAction, params) + if err != nil { + job.Result.Finish(fmt.Errorf("error running action: %w", err)) + return } - } - stateResult := ActionState{ActionCurrentState{chosenAction, params.actionParams, reasoning}, result} - job.Result.SetResult(stateResult) - job.CallbackWithResult(stateResult) + stateResult := ActionState{ActionCurrentState{chosenAction, params.actionParams, reasoning}, result} + job.Result.SetResult(stateResult) + job.CallbackWithResult(stateResult) - // calling the function - a.currentConversation = append(a.currentConversation, openai.ChatCompletionMessage{ - Role: "assistant", - FunctionCall: &openai.FunctionCall{ - Name: chosenAction.Definition().Name.String(), - Arguments: params.actionParams.String(), - }, - }) + // calling the function + a.currentConversation = append(a.currentConversation, openai.ChatCompletionMessage{ + Role: "assistant", + FunctionCall: &openai.FunctionCall{ + Name: chosenAction.Definition().Name.String(), + Arguments: params.actionParams.String(), + }, + }) - // result of calling the function - a.currentConversation = append(a.currentConversation, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleTool, - Content: result, - Name: chosenAction.Definition().Name.String(), - ToolCallID: chosenAction.Definition().Name.String(), - }) + // result of calling the function + a.currentConversation = append(a.currentConversation, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + Content: result, + Name: chosenAction.Definition().Name.String(), + ToolCallID: chosenAction.Definition().Name.String(), + }) - //a.currentConversation = append(a.currentConversation, messages...) - //a.currentConversation = messages + //a.currentConversation = append(a.currentConversation, messages...) + //a.currentConversation = messages - // given the result, we can now ask OpenAI to complete the conversation or - // to continue using another tool given the result - followingAction, reasoning, err := a.pickAction(ctx, reEvalTemplate, a.currentConversation) - if err != nil { - job.Result.Finish(fmt.Errorf("error picking action: %w", err)) - return - } + // given the result, we can now ask OpenAI to complete the conversation or + // to continue using another tool given the result + followingAction, reasoning, err := a.pickAction(ctx, reEvalTemplate, a.currentConversation) + if err != nil { + job.Result.Finish(fmt.Errorf("error picking action: %w", err)) + return + } - if followingAction != nil && - !followingAction.Definition().Name.Is(action.ReplyActionName) && - !chosenAction.Definition().Name.Is(action.ReplyActionName) { - // We need to do another action (?) - // The agent decided to do another action - // call ourselves again - a.currentReasoning = reasoning - a.nextAction = followingAction - job.Text = "" - a.consumeJob(job, role) - return + if followingAction != nil && + !followingAction.Definition().Name.Is(action.ReplyActionName) && + !chosenAction.Definition().Name.Is(action.ReplyActionName) { + // We need to do another action (?) + // The agent decided to do another action + // call ourselves again + a.currentReasoning = reasoning + a.nextAction = followingAction + job.Text = "" + a.consumeJob(job, role) + return + } } // Generate a human-readable response @@ -335,7 +357,6 @@ func (a *Agent) Run() error { for { select { case job := <-a.jobQueue: - // Consume the job and generate a response // TODO: Give a short-term memory to the agent a.consumeJob(job, UserRole) diff --git a/agent/options.go b/agent/options.go index 10272e5..baef145 100644 --- a/agent/options.go +++ b/agent/options.go @@ -17,9 +17,10 @@ type options struct { character Character randomIdentityGuidance string randomIdentity bool - actions Actions + userActions Actions enableHUD, standaloneJob bool context context.Context + permanentGoal string } func defaultOptions() *options { @@ -74,6 +75,13 @@ func WithLLMAPIKey(key string) Option { } } +func WithPermanentGoal(goal string) Option { + return func(o *options) error { + o.permanentGoal = goal + return nil + } +} + func WithContext(ctx context.Context) Option { return func(o *options) error { o.context = ctx @@ -116,7 +124,7 @@ func WithRandomIdentity(guidance ...string) Option { func WithActions(actions ...Action) Option { return func(o *options) error { - o.actions = actions + o.userActions = actions return nil } } diff --git a/agent/state.go b/agent/state.go index ad0b7e9..e3f740d 100644 --- a/agent/state.go +++ b/agent/state.go @@ -5,6 +5,7 @@ import ( "fmt" "os" + "github.com/mudler/local-agent-framework/action" "github.com/mudler/local-agent-framework/llm" ) @@ -12,25 +13,9 @@ import ( // all information that should be displayed to the LLM // in the prompts type PromptHUD struct { - Character Character `json:"character"` - CurrentState State `json:"current_state"` -} - -// State is the structure -// that is used to keep track of the current state -// and the Agent's short memory that it can update -// Besides a long term memory that is accessible by the agent (With vector database), -// And a context memory (that is always powered by a vector database), -// this memory is the shorter one that the LLM keeps across conversation and across its -// reasoning process's and life time. -// TODO: A special action is then used to let the LLM itself update its memory -// periodically during self-processing, and the same action is ALSO exposed -// during the conversation to let the user put for example, a new goal to the agent. -type State struct { - NowDoing string `json:"doing_now"` - DoingNext string `json:"doing_next"` - DoneHistory []string `json:"done_history"` - Memories []string `json:"memories"` + Character Character `json:"character"` + CurrentState action.StateResult `json:"current_state"` + PermanentGoal string `json:"permanent_goal"` } type Character struct {