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"
)
// 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}
}

View File

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

View File

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

View File

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

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 {
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"),

View File

@@ -21,6 +21,7 @@ type JobResult struct {
sync.Mutex
// The result of a job
State []ActionState
Response string
Error error
ready chan bool
}
@@ -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

View File

@@ -18,12 +18,16 @@ type options struct {
randomIdentityGuidance string
randomIdentity bool
userActions Actions
enableHUD, standaloneJob bool
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

View File

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

View File

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