Update state action

This commit is contained in:
Ettore Di Giacinto
2024-04-04 00:19:56 +02:00
parent 936b2af4ca
commit 9173156e40
5 changed files with 197 additions and 95 deletions

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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 {