Update state action
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
157
agent/agent.go
157
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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user