From 652cef288d5abcfa3931092ceb3ec2cdd1bb513e Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Fri, 5 Apr 2024 01:20:20 +0200 Subject: [PATCH] control show of character,global callbacks, re-add replies during internal runs to self-stop --- action/intention.go | 3 +++ action/reasoning.go | 3 +++ agent/actions.go | 33 ++++++++++------------- agent/agent.go | 40 +++++++++++++++++++++++----- agent/agent_test.go | 65 +++++++++++++++++++++++++++++++++++++++++---- agent/jobs.go | 15 ++++++++--- agent/options.go | 45 +++++++++++++++++++++++-------- agent/state.go | 1 + agent/templates.go | 20 +++----------- 9 files changed, 165 insertions(+), 60 deletions(-) diff --git a/action/intention.go b/action/intention.go index 394a8cd..8f778dd 100644 --- a/action/intention.go +++ b/action/intention.go @@ -4,6 +4,9 @@ import ( "github.com/sashabaranov/go-openai/jsonschema" ) +// NewIntention creates a new intention action +// The inention action is special as it tries to identify +// a tool to use and a reasoning over to use it func NewIntention(s ...string) *IntentAction { return &IntentAction{tools: s} } diff --git a/action/reasoning.go b/action/reasoning.go index 1ee48dd..22f53ee 100644 --- a/action/reasoning.go +++ b/action/reasoning.go @@ -4,6 +4,9 @@ import ( "github.com/sashabaranov/go-openai/jsonschema" ) +// NewReasoning creates a new reasoning action +// The reasoning action is special as it tries to force the LLM +// to think about what to do next func NewReasoning() *ReasoningAction { return &ReasoningAction{} } diff --git a/agent/actions.go b/agent/actions.go index 5fbff1c..3ff27be 100644 --- a/agent/actions.go +++ b/agent/actions.go @@ -55,7 +55,7 @@ type decisionResult struct { message string } -// decision forces the agent to take on of the available actions +// decision forces the agent to take one of the available actions func (a *Agent) decision( ctx context.Context, conversation []openai.ChatCompletionMessage, @@ -145,20 +145,20 @@ func (a *Agent) generateParameters(ctx context.Context, pickTemplate string, act return a.decision(ctx, conversation, - a.systemActions().ToTools(), - act.Definition().Name) + a.systemInternalActions().ToTools(), + openai.ToolChoice{ + Type: openai.ToolTypeFunction, + Function: openai.ToolFunction{Name: act.Definition().Name.String()}, + }, + ) } func (a *Agent) systemInternalActions() Actions { if a.options.enableHUD { - return append(a.options.userActions, action.NewState()) + return append(a.options.userActions, action.NewState(), action.NewReply()) } - return append(a.options.userActions) -} - -func (a *Agent) systemActions() Actions { - return append(a.systemInternalActions(), action.NewReply()) + return append(a.options.userActions, action.NewReply()) } func (a *Agent) prepareHUD() PromptHUD { @@ -166,10 +166,11 @@ func (a *Agent) prepareHUD() PromptHUD { Character: a.Character, CurrentState: *a.currentState, PermanentGoal: a.options.permanentGoal, + ShowCharacter: a.options.showCharacter, } } -func (a *Agent) prepareConversationParse(templ string, messages []openai.ChatCompletionMessage, canReply bool, reasoning string) ([]openai.ChatCompletionMessage, Actions, error) { +func (a *Agent) prepareConversationParse(templ string, messages []openai.ChatCompletionMessage, reasoning string) ([]openai.ChatCompletionMessage, Actions, error) { // prepare the prompt prompt := bytes.NewBuffer([]byte{}) @@ -178,10 +179,7 @@ func (a *Agent) prepareConversationParse(templ string, messages []openai.ChatCom return nil, []Action{}, err } - actions := a.systemActions() - if !canReply { - actions = a.systemInternalActions() - } + actions := a.systemInternalActions() // Get all the actions definitions definitions := []action.ActionDefinition{} @@ -225,7 +223,7 @@ func (a *Agent) prepareConversationParse(templ string, messages []openai.ChatCom } // pickAction picks an action based on the conversation -func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.ChatCompletionMessage, canReply bool) (Action, string, error) { +func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.ChatCompletionMessage) (Action, string, error) { c := messages // prepare the prompt @@ -236,10 +234,7 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai. return nil, "", err } - actions := a.systemActions() - if !canReply { - actions = a.systemInternalActions() - } + actions := a.systemInternalActions() // Get all the actions definitions definitions := []action.ActionDefinition{} diff --git a/agent/agent.go b/agent/agent.go index 5a72715..94ba6a5 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -111,7 +111,7 @@ func (a *Agent) StopAction() { // It discards any other computation. func (a *Agent) Ask(opts ...JobOption) *JobResult { a.StopAction() - j := NewJob(opts...) + j := NewJob(append(opts, WithReasoningCallback(a.options.reasoningCallback), WithResultCallback(a.options.resultCallback))...) // fmt.Println("Job created", text) a.jobQueue <- j return j.Result.WaitResult() @@ -138,7 +138,7 @@ func (a *Agent) Stop() { } func (a *Agent) runAction(chosenAction Action, decisionResult *decisionResult) (result string, err error) { - for _, action := range a.systemActions() { + for _, action := range a.systemInternalActions() { if action.Definition().Name == chosenAction.Definition().Name { if result, err = action.Run(decisionResult.actionParams); err != nil { return "", fmt.Errorf("error running action: %w", err) @@ -218,7 +218,7 @@ func (a *Agent) consumeJob(job *Job, role string) { a.nextAction = nil } else { var err error - chosenAction, reasoning, err = a.pickAction(ctx, pickTemplate, a.currentConversation, true) + chosenAction, reasoning, err = a.pickAction(ctx, pickTemplate, a.currentConversation) if err != nil { job.Result.Finish(err) return @@ -231,12 +231,21 @@ func (a *Agent) consumeJob(job *Job, role string) { return } + if a.options.debugMode { + fmt.Println("===> Generating parameters for", chosenAction.Definition().Name) + } + params, err := a.generateParameters(ctx, pickTemplate, chosenAction, a.currentConversation, reasoning) if err != nil { job.Result.Finish(fmt.Errorf("error generating action's parameters: %w", err)) return } + if a.options.debugMode { + fmt.Println("===> Generated parameters for", chosenAction.Definition().Name) + fmt.Println(params.actionParams.String()) + } + if params.actionParams == nil { job.Result.Finish(fmt.Errorf("no parameters")) return @@ -282,7 +291,7 @@ func (a *Agent) consumeJob(job *Job, role string) { // 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, reEvaluationTemplate, a.currentConversation, !selfEvaluation) + followingAction, reasoning, err := a.pickAction(ctx, reEvaluationTemplate, a.currentConversation) if err != nil { job.Result.Finish(fmt.Errorf("error picking action: %w", err)) return @@ -324,16 +333,25 @@ func (a *Agent) consumeJob(job *Job, role string) { msg := resp.Choices[0].Message a.currentConversation = append(a.currentConversation, msg) + job.Result.SetResponse(msg.Content) job.Result.Finish(nil) } func (a *Agent) periodicallyRun() { + + if a.options.debugMode { + fmt.Println("START -- Periodically run is starting") + } + if len(a.CurrentConversation()) != 0 { // Here the LLM could decide to store some part of the conversation too in the memory evaluateMemory := NewJob( WithText( `Evaluate the current conversation and decide if we need to store some relevant informations from it`, - )) + ), + WithReasoningCallback(a.options.reasoningCallback), + WithResultCallback(a.options.resultCallback), + ) a.consumeJob(evaluateMemory, SystemRole) a.ResetConversation() @@ -345,8 +363,18 @@ func (a *Agent) periodicallyRun() { // - asking the agent to do something else based on the result // whatNext := NewJob(WithText("Decide what to do based on the state")) - whatNext := NewJob(WithText("Decide what to based on the goal and the persistent goal.")) + whatNext := NewJob( + WithText("Decide what to based on the goal and the persistent goal."), + WithReasoningCallback(a.options.reasoningCallback), + WithResultCallback(a.options.resultCallback), + ) a.consumeJob(whatNext, SystemRole) + a.ResetConversation() + + if a.options.debugMode { + fmt.Println("STOP -- Periodically run is done") + } + // Save results from state // a.ResetConversation() diff --git a/agent/agent_test.go b/agent/agent_test.go index c4398fb..84c56f0 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -63,6 +63,25 @@ func (a *TestAction) Definition() action.ActionDefinition { } } +type FakeStoreResultAction struct { + TestAction +} + +func (a *FakeStoreResultAction) Definition() action.ActionDefinition { + return action.ActionDefinition{ + Name: "store_results", + Description: "store results permanently. Use this tool after you have a result you want to keep.", + Properties: map[string]jsonschema.Definition{ + "term": { + Type: jsonschema.String, + Description: "What to store permanently", + }, + }, + + Required: []string{"term"}, + } +} + type FakeInternetAction struct { TestAction } @@ -167,16 +186,43 @@ var _ = Describe("Agent test", func() { Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State())) }) - It("it automatically performs things in the background", func() { + FIt("it automatically performs things in the background", func() { agent, err := New( WithLLMAPIURL(apiModel), WithModel(testModel), EnableHUD, DebugMode, EnableStandaloneJob, - WithActions(&FakeInternetAction{TestAction{response: []string{"Roma, Venice, Milan"}}}), + WithAgentReasoningCallback(func(state ActionCurrentState) bool { + fmt.Println("Reasoning", state) + return true + }), + WithAgentResultCallback(func(state ActionState) { + fmt.Println("Reasoning", state.Reasoning) + fmt.Println("Action", state.Action) + fmt.Println("Result", state.Result) + }), + WithActions( + &FakeInternetAction{ + TestAction{ + response: []string{ + "Major cities in italy: Roma, Venice, Milan", + "In rome it's 30C today, it's sunny, and humidity is at 98%", + "In venice it's very hot today, it is 45C and the humidity is at 200%", + "In milan it's very cold today, it is 2C and the humidity is at 10%", + }, + }, + }, + &FakeStoreResultAction{ + TestAction{ + response: []string{ + "Result permanently stored", + }, + }, + }, + ), WithRandomIdentity(), - WithPermanentGoal("get the weather of all the cities in italy"), + WithPermanentGoal("get the weather of all the cities in italy and store the results"), ) Expect(err).ToNot(HaveOccurred()) go agent.Run() @@ -184,8 +230,17 @@ var _ = Describe("Agent test", func() { Eventually(func() string { fmt.Println(agent.State()) - return agent.State().NowDoing - }, "4m", "10s").Should(ContainSubstring("weather"), fmt.Sprint(agent.State())) + return agent.State().Goal + }, "10m", "10s").Should(ContainSubstring("weather"), fmt.Sprint(agent.State())) + + Eventually(func() string { + fmt.Println(agent.State()) + return agent.State().String() + }, "10m", "10s").Should(ContainSubstring("store"), fmt.Sprint(agent.State())) + Eventually(func() string { + fmt.Println(agent.State()) + return agent.State().String() + }, "10m", "10s").Should(ContainSubstring("inform"), fmt.Sprint(agent.State())) // result := agent.Ask( // WithText("Update your goals such as you want to learn to play the guitar"), diff --git a/agent/jobs.go b/agent/jobs.go index 9a168c3..48d01ac 100644 --- a/agent/jobs.go +++ b/agent/jobs.go @@ -20,9 +20,10 @@ type Job struct { type JobResult struct { sync.Mutex // The result of a job - State []ActionState - Error error - ready chan bool + State []ActionState + Response string + Error error + ready chan bool } type JobOption func(*Job) @@ -104,6 +105,14 @@ func (j *JobResult) Finish(e error) { close(j.ready) } +// SetResult sets the result of a job +func (j *JobResult) SetResponse(response string) { + j.Lock() + defer j.Unlock() + + j.Response = response +} + // WaitResult waits for the result of a job func (j *JobResult) WaitResult() *JobResult { <-j.ready diff --git a/agent/options.go b/agent/options.go index c9c6e42..58d628c 100644 --- a/agent/options.go +++ b/agent/options.go @@ -13,17 +13,21 @@ type llmOptions struct { } type options struct { - LLMAPI llmOptions - character Character - randomIdentityGuidance string - randomIdentity bool - userActions Actions - enableHUD, standaloneJob bool - debugMode bool - characterfile string - statefile string - context context.Context - permanentGoal string + LLMAPI llmOptions + character Character + randomIdentityGuidance string + randomIdentity bool + userActions Actions + enableHUD, standaloneJob, showCharacter bool + debugMode bool + characterfile string + statefile string + context context.Context + permanentGoal string + + // callbacks + reasoningCallback func(ActionCurrentState) bool + resultCallback func(ActionState) } func defaultOptions() *options { @@ -69,6 +73,11 @@ var EnableStandaloneJob = func(o *options) error { return nil } +var EnableCharacter = func(o *options) error { + o.showCharacter = true + return nil +} + func WithLLMAPIURL(url string) Option { return func(o *options) error { o.LLMAPI.APIURL = url @@ -97,6 +106,20 @@ func WithContext(ctx context.Context) Option { } } +func WithAgentReasoningCallback(cb func(ActionCurrentState) bool) Option { + return func(o *options) error { + o.reasoningCallback = cb + return nil + } +} + +func WithAgentResultCallback(cb func(ActionState)) Option { + return func(o *options) error { + o.resultCallback = cb + return nil + } +} + func WithModel(model string) Option { return func(o *options) error { o.LLMAPI.Model = model diff --git a/agent/state.go b/agent/state.go index c165f92..7d72af2 100644 --- a/agent/state.go +++ b/agent/state.go @@ -17,6 +17,7 @@ type PromptHUD struct { Character Character `json:"character"` CurrentState action.StateResult `json:"current_state"` PermanentGoal string `json:"permanent_goal"` + ShowCharacter bool `json:"show_character"` } type Character struct { diff --git a/agent/templates.go b/agent/templates.go index 301a241..b13b720 100644 --- a/agent/templates.go +++ b/agent/templates.go @@ -1,12 +1,13 @@ package agent -const hudTemplate = `{{with .HUD }}You have a character and your replies and actions might be influenced by it. +const hudTemplate = `{{with .HUD }}{{if .ShowCharacter}}You have a character and your replies and actions might be influenced by it. {{if .Character.Name}}Name: {{.Character.Name}} {{end}}{{if .Character.Age}}Age: {{.Character.Age}} {{end}}{{if .Character.Occupation}}Occupation: {{.Character.Occupation}} {{end}}{{if .Character.Hobbies}}Hobbies: {{.Character.Hobbies}} {{end}}{{if .Character.MusicTaste}}Music taste: {{.Character.MusicTaste}} {{end}} +{{end}} This is your current state: NowDoing: {{if .CurrentState.NowDoing}}{{.CurrentState.NowDoing}}{{else}}Nothing{{end}} @@ -16,20 +17,13 @@ Your current goal is: {{if .CurrentState.Goal}}{{.CurrentState.Goal}}{{else}}Not You have done: {{range .CurrentState.DoneHistory}}{{.}} {{end}} You have a short memory with: {{range .CurrentState.Memories}}{{.}} {{end}}{{end}}` -const pickSelfTemplate = ` -You can take any of the following tools: +const pickSelfTemplate = `You can take any of the following tools: {{range .Actions -}} - {{.Name}}: {{.Description }} {{ end }} -{{if .Messages}} -Consider the text below, decide which action to take and explain the detailed reasoning behind it. - -{{range .Messages -}} -{{.Role}}{{if .FunctionCall}}(tool_call){{.FunctionCall}}{{end}}: {{if .FunctionCall}}{{.FunctionCall}}{{else if .ToolCalls -}}{{range .ToolCalls -}}{{.Name}} called with {{.Arguments}}{{end}}{{ else }}{{.Content -}}{{end}} -{{end}} -{{end}} +To finish your session, use the "reply" tool with your answer. Act like as a fully autonomous smart AI agent having a character, the character and your state is defined in the message above. You are now self-evaluating what to do next based on the state in the previous message. @@ -53,12 +47,6 @@ You can take any of the following tools: To answer back to the user, use the "reply" tool. Given the text below, decide which action to take and explain the detailed reasoning behind it. For answering without picking a choice, reply with 'none'. -{{if .Messages}} -{{range .Messages -}} -{{.Role}}{{if .FunctionCall}}(tool_call){{.FunctionCall}}{{end}}: {{if .FunctionCall}}{{.FunctionCall}}{{else if .ToolCalls -}}{{range .ToolCalls -}}{{.Name}} called with {{.Arguments}}{{end}}{{ else }}{{.Content -}}{{end}} -{{end}} -{{end}} - {{if .Reasoning}}Reasoning: {{.Reasoning}}{{end}} `