Update state action
This commit is contained in:
70
action/state.go
Normal file
70
action/state.go
Normal file
@@ -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.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,13 +88,23 @@ func (a *Agent) decision(
|
|||||||
func (a *Agent) generateParameters(ctx context.Context, action Action, conversation []openai.ChatCompletionMessage) (*decisionResult, error) {
|
func (a *Agent) generateParameters(ctx context.Context, action Action, conversation []openai.ChatCompletionMessage) (*decisionResult, error) {
|
||||||
return a.decision(ctx,
|
return a.decision(ctx,
|
||||||
conversation,
|
conversation,
|
||||||
a.options.actions.ToTools(),
|
a.systemActions().ToTools(),
|
||||||
action.Definition().Name)
|
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 {
|
func (a *Agent) prepareHUD() PromptHUD {
|
||||||
return 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.Hobbies}}Hobbies: {{.Character.Hobbies}}
|
||||||
{{end}}{{if .Character.MusicTaste}}Music taste: {{.Character.MusicTaste}}
|
{{end}}{{if .Character.MusicTaste}}Music taste: {{.Character.MusicTaste}}
|
||||||
{{end}}
|
{{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
|
// 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
|
return nil, "", err
|
||||||
}
|
}
|
||||||
// Get all the actions definitions
|
// Get all the actions definitions
|
||||||
definitions := []action.ActionDefinition{action.NewReply().Definition()}
|
definitions := []action.ActionDefinition{}
|
||||||
for _, m := range a.options.actions {
|
for _, m := range a.systemActions() {
|
||||||
definitions = append(definitions, m.Definition())
|
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
|
// Get all the available actions IDs
|
||||||
actionsID := []string{}
|
actionsID := []string{}
|
||||||
for _, m := range a.options.actions {
|
for _, m := range a.systemActions() {
|
||||||
actionsID = append(actionsID, m.Definition().Name.String())
|
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
|
// Find the action
|
||||||
chosenAction := append(a.options.actions, action.NewReply()).Find(actionChoice.Tool)
|
chosenAction := a.systemActions().Find(actionChoice.Tool)
|
||||||
if chosenAction == nil {
|
if chosenAction == nil {
|
||||||
return nil, "", fmt.Errorf("no action found for intent:" + actionChoice.Tool)
|
return nil, "", fmt.Errorf("no action found for intent:" + actionChoice.Tool)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ type Agent struct {
|
|||||||
jobQueue chan *Job
|
jobQueue chan *Job
|
||||||
actionContext *action.ActionContext
|
actionContext *action.ActionContext
|
||||||
context *action.ActionContext
|
context *action.ActionContext
|
||||||
availableActions []Action
|
|
||||||
|
|
||||||
currentReasoning string
|
currentReasoning string
|
||||||
|
currentState *action.StateResult
|
||||||
nextAction Action
|
nextAction Action
|
||||||
currentConversation []openai.ChatCompletionMessage
|
currentConversation []openai.ChatCompletionMessage
|
||||||
}
|
}
|
||||||
@@ -82,8 +82,8 @@ func New(opts ...Option) (*Agent, error) {
|
|||||||
options: options,
|
options: options,
|
||||||
client: client,
|
client: client,
|
||||||
Character: options.character,
|
Character: options.character,
|
||||||
|
currentState: &action.StateResult{},
|
||||||
context: action.NewContext(ctx, cancel),
|
context: action.NewContext(ctx, cancel),
|
||||||
availableActions: options.actions,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.options.randomIdentity {
|
if a.options.randomIdentity {
|
||||||
@@ -135,6 +135,30 @@ func (a *Agent) Stop() {
|
|||||||
a.context.Cancel()
|
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) {
|
func (a *Agent) consumeJob(job *Job, role string) {
|
||||||
// Consume the job and generate a response
|
// Consume the job and generate a response
|
||||||
a.Lock()
|
a.Lock()
|
||||||
@@ -175,9 +199,9 @@ func (a *Agent) consumeJob(job *Job, role string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if chosenAction == nil || chosenAction.Definition().Name.Is(action.ReplyActionName) {
|
if chosenAction == nil {
|
||||||
job.Result.SetResult(ActionState{ActionCurrentState{nil, nil, "No action to do, just reply"}, ""})
|
//job.Result.SetResult(ActionState{ActionCurrentState{nil, nil, "No action to do, just reply"}, ""})
|
||||||
job.Result.Finish(nil)
|
job.Result.Finish(fmt.Errorf("no action to do"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,26 +211,23 @@ func (a *Agent) consumeJob(job *Job, role string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if params.actionParams == nil {
|
||||||
|
job.Result.Finish(fmt.Errorf("no parameters"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !job.Callback(ActionCurrentState{chosenAction, params.actionParams, reasoning}) {
|
if !job.Callback(ActionCurrentState{chosenAction, params.actionParams, reasoning}) {
|
||||||
job.Result.SetResult(ActionState{ActionCurrentState{chosenAction, params.actionParams, reasoning}, "stopped by callback"})
|
job.Result.SetResult(ActionState{ActionCurrentState{chosenAction, params.actionParams, reasoning}, "stopped by callback"})
|
||||||
job.Result.Finish(nil)
|
job.Result.Finish(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if params.actionParams == nil {
|
if !chosenAction.Definition().Name.Is(action.ReplyActionName) {
|
||||||
job.Result.Finish(fmt.Errorf("no parameters"))
|
result, err := a.runAction(chosenAction, params)
|
||||||
return
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
job.Result.Finish(fmt.Errorf("error running action: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stateResult := ActionState{ActionCurrentState{chosenAction, params.actionParams, reasoning}, result}
|
stateResult := ActionState{ActionCurrentState{chosenAction, params.actionParams, reasoning}, result}
|
||||||
job.Result.SetResult(stateResult)
|
job.Result.SetResult(stateResult)
|
||||||
@@ -252,6 +273,7 @@ func (a *Agent) consumeJob(job *Job, role string) {
|
|||||||
a.consumeJob(job, role)
|
a.consumeJob(job, role)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a human-readable response
|
// Generate a human-readable response
|
||||||
resp, err := a.client.CreateChatCompletion(ctx,
|
resp, err := a.client.CreateChatCompletion(ctx,
|
||||||
@@ -335,7 +357,6 @@ func (a *Agent) Run() error {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case job := <-a.jobQueue:
|
case job := <-a.jobQueue:
|
||||||
|
|
||||||
// Consume the job and generate a response
|
// Consume the job and generate a response
|
||||||
// TODO: Give a short-term memory to the agent
|
// TODO: Give a short-term memory to the agent
|
||||||
a.consumeJob(job, UserRole)
|
a.consumeJob(job, UserRole)
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ type options struct {
|
|||||||
character Character
|
character Character
|
||||||
randomIdentityGuidance string
|
randomIdentityGuidance string
|
||||||
randomIdentity bool
|
randomIdentity bool
|
||||||
actions Actions
|
userActions Actions
|
||||||
enableHUD, standaloneJob bool
|
enableHUD, standaloneJob bool
|
||||||
context context.Context
|
context context.Context
|
||||||
|
permanentGoal string
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultOptions() *options {
|
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 {
|
func WithContext(ctx context.Context) Option {
|
||||||
return func(o *options) error {
|
return func(o *options) error {
|
||||||
o.context = ctx
|
o.context = ctx
|
||||||
@@ -116,7 +124,7 @@ func WithRandomIdentity(guidance ...string) Option {
|
|||||||
|
|
||||||
func WithActions(actions ...Action) Option {
|
func WithActions(actions ...Action) Option {
|
||||||
return func(o *options) error {
|
return func(o *options) error {
|
||||||
o.actions = actions
|
o.userActions = actions
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/mudler/local-agent-framework/action"
|
||||||
"github.com/mudler/local-agent-framework/llm"
|
"github.com/mudler/local-agent-framework/llm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,24 +14,8 @@ import (
|
|||||||
// in the prompts
|
// in the prompts
|
||||||
type PromptHUD struct {
|
type PromptHUD struct {
|
||||||
Character Character `json:"character"`
|
Character Character `json:"character"`
|
||||||
CurrentState State `json:"current_state"`
|
CurrentState action.StateResult `json:"current_state"`
|
||||||
}
|
PermanentGoal string `json:"permanent_goal"`
|
||||||
|
|
||||||
// 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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Character struct {
|
type Character struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user