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

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

View File

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

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