control show of character,global callbacks, re-add replies during internal runs to self-stop

This commit is contained in:
Ettore Di Giacinto
2024-04-05 01:20:20 +02:00
parent 744af19025
commit 652cef288d
9 changed files with 165 additions and 60 deletions

View File

@@ -4,6 +4,9 @@ import (
"github.com/sashabaranov/go-openai/jsonschema" "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 { func NewIntention(s ...string) *IntentAction {
return &IntentAction{tools: s} return &IntentAction{tools: s}
} }

View File

@@ -4,6 +4,9 @@ import (
"github.com/sashabaranov/go-openai/jsonschema" "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 { func NewReasoning() *ReasoningAction {
return &ReasoningAction{} return &ReasoningAction{}
} }

View File

@@ -55,7 +55,7 @@ type decisionResult struct {
message string 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( func (a *Agent) decision(
ctx context.Context, ctx context.Context,
conversation []openai.ChatCompletionMessage, conversation []openai.ChatCompletionMessage,
@@ -145,20 +145,20 @@ func (a *Agent) generateParameters(ctx context.Context, pickTemplate string, act
return a.decision(ctx, return a.decision(ctx,
conversation, conversation,
a.systemActions().ToTools(), a.systemInternalActions().ToTools(),
act.Definition().Name) openai.ToolChoice{
Type: openai.ToolTypeFunction,
Function: openai.ToolFunction{Name: act.Definition().Name.String()},
},
)
} }
func (a *Agent) systemInternalActions() Actions { func (a *Agent) systemInternalActions() Actions {
if a.options.enableHUD { 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) return append(a.options.userActions, action.NewReply())
}
func (a *Agent) systemActions() Actions {
return append(a.systemInternalActions(), action.NewReply())
} }
func (a *Agent) prepareHUD() PromptHUD { func (a *Agent) prepareHUD() PromptHUD {
@@ -166,10 +166,11 @@ func (a *Agent) prepareHUD() PromptHUD {
Character: a.Character, Character: a.Character,
CurrentState: *a.currentState, CurrentState: *a.currentState,
PermanentGoal: a.options.permanentGoal, 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 // prepare the prompt
prompt := bytes.NewBuffer([]byte{}) prompt := bytes.NewBuffer([]byte{})
@@ -178,10 +179,7 @@ func (a *Agent) prepareConversationParse(templ string, messages []openai.ChatCom
return nil, []Action{}, err return nil, []Action{}, err
} }
actions := a.systemActions() actions := a.systemInternalActions()
if !canReply {
actions = a.systemInternalActions()
}
// Get all the actions definitions // Get all the actions definitions
definitions := []action.ActionDefinition{} definitions := []action.ActionDefinition{}
@@ -225,7 +223,7 @@ func (a *Agent) prepareConversationParse(templ string, messages []openai.ChatCom
} }
// pickAction picks an action based on the conversation // 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 c := messages
// prepare the prompt // prepare the prompt
@@ -236,10 +234,7 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.
return nil, "", err return nil, "", err
} }
actions := a.systemActions() actions := a.systemInternalActions()
if !canReply {
actions = a.systemInternalActions()
}
// Get all the actions definitions // Get all the actions definitions
definitions := []action.ActionDefinition{} definitions := []action.ActionDefinition{}

View File

@@ -111,7 +111,7 @@ func (a *Agent) StopAction() {
// It discards any other computation. // It discards any other computation.
func (a *Agent) Ask(opts ...JobOption) *JobResult { func (a *Agent) Ask(opts ...JobOption) *JobResult {
a.StopAction() a.StopAction()
j := NewJob(opts...) j := NewJob(append(opts, WithReasoningCallback(a.options.reasoningCallback), WithResultCallback(a.options.resultCallback))...)
// fmt.Println("Job created", text) // fmt.Println("Job created", text)
a.jobQueue <- j a.jobQueue <- j
return j.Result.WaitResult() return j.Result.WaitResult()
@@ -138,7 +138,7 @@ func (a *Agent) Stop() {
} }
func (a *Agent) runAction(chosenAction Action, decisionResult *decisionResult) (result string, err error) { 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 action.Definition().Name == chosenAction.Definition().Name {
if result, err = action.Run(decisionResult.actionParams); err != nil { if result, err = action.Run(decisionResult.actionParams); err != nil {
return "", fmt.Errorf("error running action: %w", err) return "", fmt.Errorf("error running action: %w", err)
@@ -218,7 +218,7 @@ func (a *Agent) consumeJob(job *Job, role string) {
a.nextAction = nil a.nextAction = nil
} else { } else {
var err error 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 { if err != nil {
job.Result.Finish(err) job.Result.Finish(err)
return return
@@ -231,12 +231,21 @@ func (a *Agent) consumeJob(job *Job, role string) {
return return
} }
if a.options.debugMode {
fmt.Println("===> Generating parameters for", chosenAction.Definition().Name)
}
params, err := a.generateParameters(ctx, pickTemplate, chosenAction, a.currentConversation, reasoning) params, err := a.generateParameters(ctx, pickTemplate, chosenAction, a.currentConversation, reasoning)
if err != nil { if err != nil {
job.Result.Finish(fmt.Errorf("error generating action's parameters: %w", err)) job.Result.Finish(fmt.Errorf("error generating action's parameters: %w", err))
return return
} }
if a.options.debugMode {
fmt.Println("===> Generated parameters for", chosenAction.Definition().Name)
fmt.Println(params.actionParams.String())
}
if params.actionParams == nil { if params.actionParams == nil {
job.Result.Finish(fmt.Errorf("no parameters")) job.Result.Finish(fmt.Errorf("no parameters"))
return 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 // given the result, we can now ask OpenAI to complete the conversation or
// to continue using another tool given the result // 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 { if err != nil {
job.Result.Finish(fmt.Errorf("error picking action: %w", err)) job.Result.Finish(fmt.Errorf("error picking action: %w", err))
return return
@@ -324,16 +333,25 @@ func (a *Agent) consumeJob(job *Job, role string) {
msg := resp.Choices[0].Message msg := resp.Choices[0].Message
a.currentConversation = append(a.currentConversation, msg) a.currentConversation = append(a.currentConversation, msg)
job.Result.SetResponse(msg.Content)
job.Result.Finish(nil) job.Result.Finish(nil)
} }
func (a *Agent) periodicallyRun() { func (a *Agent) periodicallyRun() {
if a.options.debugMode {
fmt.Println("START -- Periodically run is starting")
}
if len(a.CurrentConversation()) != 0 { if len(a.CurrentConversation()) != 0 {
// Here the LLM could decide to store some part of the conversation too in the memory // Here the LLM could decide to store some part of the conversation too in the memory
evaluateMemory := NewJob( evaluateMemory := NewJob(
WithText( WithText(
`Evaluate the current conversation and decide if we need to store some relevant informations from it`, `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.consumeJob(evaluateMemory, SystemRole)
a.ResetConversation() a.ResetConversation()
@@ -345,8 +363,18 @@ func (a *Agent) periodicallyRun() {
// - asking the agent to do something else based on the result // - 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 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.consumeJob(whatNext, SystemRole)
a.ResetConversation()
if a.options.debugMode {
fmt.Println("STOP -- Periodically run is done")
}
// Save results from state
// a.ResetConversation() // a.ResetConversation()

View File

@@ -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 { type FakeInternetAction struct {
TestAction TestAction
} }
@@ -167,16 +186,43 @@ var _ = Describe("Agent test", func() {
Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State())) 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( agent, err := New(
WithLLMAPIURL(apiModel), WithLLMAPIURL(apiModel),
WithModel(testModel), WithModel(testModel),
EnableHUD, EnableHUD,
DebugMode, DebugMode,
EnableStandaloneJob, 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(), 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()) Expect(err).ToNot(HaveOccurred())
go agent.Run() go agent.Run()
@@ -184,8 +230,17 @@ var _ = Describe("Agent test", func() {
Eventually(func() string { Eventually(func() string {
fmt.Println(agent.State()) fmt.Println(agent.State())
return agent.State().NowDoing return agent.State().Goal
}, "4m", "10s").Should(ContainSubstring("weather"), fmt.Sprint(agent.State())) }, "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( // result := agent.Ask(
// WithText("Update your goals such as you want to learn to play the guitar"), // WithText("Update your goals such as you want to learn to play the guitar"),

View File

@@ -21,6 +21,7 @@ type JobResult struct {
sync.Mutex sync.Mutex
// The result of a job // The result of a job
State []ActionState State []ActionState
Response string
Error error Error error
ready chan bool ready chan bool
} }
@@ -104,6 +105,14 @@ func (j *JobResult) Finish(e error) {
close(j.ready) 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 // WaitResult waits for the result of a job
func (j *JobResult) WaitResult() *JobResult { func (j *JobResult) WaitResult() *JobResult {
<-j.ready <-j.ready

View File

@@ -18,12 +18,16 @@ type options struct {
randomIdentityGuidance string randomIdentityGuidance string
randomIdentity bool randomIdentity bool
userActions Actions userActions Actions
enableHUD, standaloneJob bool enableHUD, standaloneJob, showCharacter bool
debugMode bool debugMode bool
characterfile string characterfile string
statefile string statefile string
context context.Context context context.Context
permanentGoal string permanentGoal string
// callbacks
reasoningCallback func(ActionCurrentState) bool
resultCallback func(ActionState)
} }
func defaultOptions() *options { func defaultOptions() *options {
@@ -69,6 +73,11 @@ var EnableStandaloneJob = func(o *options) error {
return nil return nil
} }
var EnableCharacter = func(o *options) error {
o.showCharacter = true
return nil
}
func WithLLMAPIURL(url string) Option { func WithLLMAPIURL(url string) Option {
return func(o *options) error { return func(o *options) error {
o.LLMAPI.APIURL = url 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 { func WithModel(model string) Option {
return func(o *options) error { return func(o *options) error {
o.LLMAPI.Model = model o.LLMAPI.Model = model

View File

@@ -17,6 +17,7 @@ type PromptHUD struct {
Character Character `json:"character"` Character Character `json:"character"`
CurrentState action.StateResult `json:"current_state"` CurrentState action.StateResult `json:"current_state"`
PermanentGoal string `json:"permanent_goal"` PermanentGoal string `json:"permanent_goal"`
ShowCharacter bool `json:"show_character"`
} }
type Character struct { type Character struct {

View File

@@ -1,12 +1,13 @@
package agent 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}} {{if .Character.Name}}Name: {{.Character.Name}}
{{end}}{{if .Character.Age}}Age: {{.Character.Age}} {{end}}{{if .Character.Age}}Age: {{.Character.Age}}
{{end}}{{if .Character.Occupation}}Occupation: {{.Character.Occupation}} {{end}}{{if .Character.Occupation}}Occupation: {{.Character.Occupation}}
{{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}}
{{end}}
This is your current state: This is your current state:
NowDoing: {{if .CurrentState.NowDoing}}{{.CurrentState.NowDoing}}{{else}}Nothing{{end}} 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 done: {{range .CurrentState.DoneHistory}}{{.}} {{end}}
You have a short memory with: {{range .CurrentState.Memories}}{{.}} {{end}}{{end}}` You have a short memory with: {{range .CurrentState.Memories}}{{.}} {{end}}{{end}}`
const pickSelfTemplate = ` const pickSelfTemplate = `You can take any of the following tools:
You can take any of the following tools:
{{range .Actions -}} {{range .Actions -}}
- {{.Name}}: {{.Description }} - {{.Name}}: {{.Description }}
{{ end }} {{ end }}
{{if .Messages}} To finish your session, use the "reply" tool with your answer.
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}}
Act like as a fully autonomous smart AI agent having a character, the character and your state is defined in the message above. 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. 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. 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'. 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}} {{if .Reasoning}}Reasoning: {{.Reasoning}}{{end}}
` `