wip
This commit is contained in:
@@ -85,19 +85,28 @@ func (a *Agent) decision(
|
||||
return &decisionResult{actionParams: params}, nil
|
||||
}
|
||||
|
||||
func (a *Agent) generateParameters(ctx context.Context, action Action, conversation []openai.ChatCompletionMessage) (*decisionResult, error) {
|
||||
func (a *Agent) generateParameters(ctx context.Context, pickTemplate string, act Action, c []openai.ChatCompletionMessage, reasoning string) (*decisionResult, error) {
|
||||
conversation, _, _, err := a.prepareConversationParse(pickTemplate, c, false, reasoning)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a.decision(ctx,
|
||||
conversation,
|
||||
a.systemActions().ToTools(),
|
||||
action.Definition().Name)
|
||||
act.Definition().Name)
|
||||
}
|
||||
|
||||
func (a *Agent) systemInternalActions() Actions {
|
||||
if a.options.enableHUD {
|
||||
return append(a.options.userActions, action.NewState())
|
||||
}
|
||||
|
||||
return append(a.options.userActions)
|
||||
}
|
||||
|
||||
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())
|
||||
return append(a.systemInternalActions(), action.NewReply())
|
||||
}
|
||||
|
||||
func (a *Agent) prepareHUD() PromptHUD {
|
||||
@@ -108,83 +117,73 @@ func (a *Agent) prepareHUD() PromptHUD {
|
||||
}
|
||||
}
|
||||
|
||||
const hudTemplate = `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}}
|
||||
|
||||
This is your current state:
|
||||
NowDoing: {{if .CurrentState.NowDoing}}{{.CurrentState.NowDoing}}{{else}}Nothing{{end}}
|
||||
DoingNext: {{if .CurrentState.DoingNext}}{{.CurrentState.DoingNext}}{{else}}Nothing{{end}}
|
||||
Your permanent goal is: {{if .PermanentGoal}}{{.PermanentGoal}}{{else}}Nothing{{end}}
|
||||
Your current goal is: {{if .CurrentState.Goal}}{{.CurrentState.Goal}}{{else}}Nothing{{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
|
||||
func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.ChatCompletionMessage) (Action, string, error) {
|
||||
func (a *Agent) prepareConversationParse(templ string, messages []openai.ChatCompletionMessage, canReply bool, reasoning string) ([]openai.ChatCompletionMessage, Actions, []string, error) {
|
||||
// prepare the prompt
|
||||
prompt := bytes.NewBuffer([]byte{})
|
||||
hud := bytes.NewBuffer([]byte{})
|
||||
|
||||
promptTemplate, err := template.New("pickAction").Parse(templ)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, []Action{}, nil, err
|
||||
}
|
||||
hudTmpl, err := template.New("HUD").Parse(hudTemplate)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
|
||||
actions := a.systemActions()
|
||||
if !canReply {
|
||||
actions = a.systemInternalActions()
|
||||
}
|
||||
|
||||
// Get all the actions definitions
|
||||
definitions := []action.ActionDefinition{}
|
||||
for _, m := range a.systemActions() {
|
||||
for _, m := range actions {
|
||||
definitions = append(definitions, m.Definition())
|
||||
}
|
||||
|
||||
var promptHUD *PromptHUD
|
||||
if a.options.enableHUD {
|
||||
h := a.prepareHUD()
|
||||
promptHUD = &h
|
||||
}
|
||||
|
||||
err = promptTemplate.Execute(prompt, struct {
|
||||
HUD *PromptHUD
|
||||
Actions []action.ActionDefinition
|
||||
Reasoning string
|
||||
Messages []openai.ChatCompletionMessage
|
||||
}{
|
||||
Actions: definitions,
|
||||
Reasoning: reasoning,
|
||||
Messages: messages,
|
||||
HUD: promptHUD,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
err = hudTmpl.Execute(hud, a.prepareHUD())
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, []Action{}, nil, err
|
||||
}
|
||||
|
||||
if a.options.debugMode {
|
||||
fmt.Println("=== HUD START ===", hud.String(), "=== HUD END ===")
|
||||
fmt.Println("=== PROMPT START ===", prompt.String(), "=== PROMPT END ===")
|
||||
}
|
||||
|
||||
// Get all the available actions IDs
|
||||
actionsID := []string{}
|
||||
for _, m := range a.systemActions() {
|
||||
for _, m := range actions {
|
||||
actionsID = append(actionsID, m.Definition().Name.String())
|
||||
}
|
||||
|
||||
conversation := []openai.ChatCompletionMessage{}
|
||||
|
||||
if a.options.enableHUD {
|
||||
conversation = append(conversation, openai.ChatCompletionMessage{
|
||||
Role: "system",
|
||||
Content: hud.String(),
|
||||
})
|
||||
}
|
||||
|
||||
conversation = append(conversation, openai.ChatCompletionMessage{
|
||||
Role: "user",
|
||||
Content: prompt.String(),
|
||||
})
|
||||
|
||||
return conversation, actions, actionsID, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
conversation, actions, actionsID, err := a.prepareConversationParse(templ, messages, canReply, "")
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
// Get the LLM to think on what to do
|
||||
thought, err := a.decision(ctx,
|
||||
conversation,
|
||||
@@ -234,7 +233,7 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.
|
||||
}
|
||||
|
||||
// Find the action
|
||||
chosenAction := a.systemActions().Find(actionChoice.Tool)
|
||||
chosenAction := actions.Find(actionChoice.Tool)
|
||||
if chosenAction == nil {
|
||||
return nil, "", fmt.Errorf("no action found for intent:" + actionChoice.Tool)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -13,33 +12,6 @@ import (
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
const pickActionTemplate = `You can take any of the following tools:
|
||||
|
||||
{{range .Actions -}}
|
||||
- {{.Name}}: {{.Description }}
|
||||
{{ end }}
|
||||
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'.
|
||||
|
||||
{{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}}
|
||||
`
|
||||
|
||||
const reEvalTemplate = `You can take any of the following tools:
|
||||
|
||||
{{range .Actions -}}
|
||||
- {{.Name}}: {{.Description }}
|
||||
{{ end }}
|
||||
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'.
|
||||
|
||||
{{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}}
|
||||
|
||||
We already have called tools. Evaluate the current situation and decide if we need to execute other tools or answer back with a result.`
|
||||
|
||||
const (
|
||||
UserRole = "user"
|
||||
AssistantRole = "assistant"
|
||||
@@ -202,6 +174,7 @@ func (a *Agent) runAction(chosenAction Action, decisionResult *decisionResult) (
|
||||
}
|
||||
|
||||
func (a *Agent) consumeJob(job *Job, role string) {
|
||||
selfEvaluation := role == SystemRole
|
||||
// Consume the job and generate a response
|
||||
a.Lock()
|
||||
// Set the action context
|
||||
@@ -221,6 +194,17 @@ func (a *Agent) consumeJob(job *Job, role string) {
|
||||
})
|
||||
}
|
||||
|
||||
var pickTemplate string
|
||||
var reEvaluationTemplate string
|
||||
|
||||
if selfEvaluation {
|
||||
pickTemplate = pickSelfTemplate
|
||||
reEvaluationTemplate = reSelfEvalTemplate
|
||||
} else {
|
||||
pickTemplate = pickActionTemplate
|
||||
reEvaluationTemplate = reEvalTemplate
|
||||
}
|
||||
|
||||
// choose an action first
|
||||
var chosenAction Action
|
||||
var reasoning string
|
||||
@@ -234,7 +218,7 @@ func (a *Agent) consumeJob(job *Job, role string) {
|
||||
a.nextAction = nil
|
||||
} else {
|
||||
var err error
|
||||
chosenAction, reasoning, err = a.pickAction(ctx, pickActionTemplate, a.currentConversation)
|
||||
chosenAction, reasoning, err = a.pickAction(ctx, pickTemplate, a.currentConversation, true)
|
||||
if err != nil {
|
||||
job.Result.Finish(err)
|
||||
return
|
||||
@@ -247,7 +231,7 @@ func (a *Agent) consumeJob(job *Job, role string) {
|
||||
return
|
||||
}
|
||||
|
||||
params, err := a.generateParameters(ctx, chosenAction, a.currentConversation)
|
||||
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
|
||||
@@ -298,7 +282,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, reEvalTemplate, a.currentConversation)
|
||||
followingAction, reasoning, err := a.pickAction(ctx, reEvaluationTemplate, a.currentConversation, !selfEvaluation)
|
||||
if err != nil {
|
||||
job.Result.Finish(fmt.Errorf("error picking action: %w", err))
|
||||
return
|
||||
@@ -344,6 +328,7 @@ func (a *Agent) consumeJob(job *Job, role string) {
|
||||
}
|
||||
|
||||
func (a *Agent) periodicallyRun() {
|
||||
if len(a.CurrentConversation()) != 0 {
|
||||
// Here the LLM could decide to store some part of the conversation too in the memory
|
||||
evaluateMemory := NewJob(
|
||||
WithText(
|
||||
@@ -352,35 +337,39 @@ func (a *Agent) periodicallyRun() {
|
||||
a.consumeJob(evaluateMemory, SystemRole)
|
||||
|
||||
a.ResetConversation()
|
||||
}
|
||||
|
||||
// Here we go in a loop of
|
||||
// - asking the agent to do something
|
||||
// - evaluating the result
|
||||
// - asking the agent to do something else based on the result
|
||||
|
||||
whatNext := NewJob(WithText("What should I do next?"))
|
||||
// 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."))
|
||||
a.consumeJob(whatNext, SystemRole)
|
||||
|
||||
doWork := NewJob(WithText("Try to fullfill our goals automatically"))
|
||||
a.consumeJob(doWork, SystemRole)
|
||||
// a.ResetConversation()
|
||||
|
||||
results := []string{}
|
||||
for _, v := range doWork.Result.State {
|
||||
results = append(results, v.Result)
|
||||
}
|
||||
// doWork := NewJob(WithText("Select the tool to use based on your goal and the current state."))
|
||||
// a.consumeJob(doWork, SystemRole)
|
||||
|
||||
a.ResetConversation()
|
||||
// results := []string{}
|
||||
// for _, v := range doWork.Result.State {
|
||||
// results = append(results, v.Result)
|
||||
// }
|
||||
|
||||
// Here the LLM could decide to do something based on the result of our automatic action
|
||||
evaluateAction := NewJob(
|
||||
WithText(
|
||||
`Evaluate the current situation and decide if we need to execute other tools (for instance to store results into permanent, or short memory).
|
||||
We have done the following actions:
|
||||
` + strings.Join(results, "\n"),
|
||||
))
|
||||
a.consumeJob(evaluateAction, SystemRole)
|
||||
// a.ResetConversation()
|
||||
|
||||
a.ResetConversation()
|
||||
// // Here the LLM could decide to do something based on the result of our automatic action
|
||||
// evaluateAction := NewJob(
|
||||
// WithText(
|
||||
// `Evaluate the current situation and decide if we need to execute other tools (for instance to store results into permanent, or short memory).
|
||||
// We have done the following actions:
|
||||
// ` + strings.Join(results, "\n"),
|
||||
// ))
|
||||
// a.consumeJob(evaluateAction, SystemRole)
|
||||
|
||||
// a.ResetConversation()
|
||||
}
|
||||
|
||||
func (a *Agent) Run() error {
|
||||
|
||||
@@ -147,5 +147,32 @@ var _ = Describe("Agent test", func() {
|
||||
Expect(result.Error).ToNot(HaveOccurred())
|
||||
Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
|
||||
})
|
||||
|
||||
FIt("it automatically performs things in the background", func() {
|
||||
agent, err := New(
|
||||
WithLLMAPIURL(apiModel),
|
||||
WithModel(testModel),
|
||||
EnableHUD,
|
||||
DebugMode,
|
||||
EnableStandaloneJob,
|
||||
WithRandomIdentity(),
|
||||
WithPermanentGoal("get the weather of all the cities in italy"),
|
||||
)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
go agent.Run()
|
||||
defer agent.Stop()
|
||||
|
||||
Eventually(func() string {
|
||||
fmt.Println(agent.State())
|
||||
return agent.State().NowDoing
|
||||
}, "4m", "10s").Should(ContainSubstring("weather"), fmt.Sprint(agent.State()))
|
||||
|
||||
// result := agent.Ask(
|
||||
// WithText("Update your goals such as you want to learn to play the guitar"),
|
||||
// )
|
||||
// fmt.Printf("%+v\n", result)
|
||||
// Expect(result.Error).ToNot(HaveOccurred())
|
||||
// Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
65
agent/templates.go
Normal file
65
agent/templates.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package agent
|
||||
|
||||
const hud = `{{with .HUD }}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}}
|
||||
|
||||
This is your current state:
|
||||
NowDoing: {{if .CurrentState.NowDoing}}{{.CurrentState.NowDoing}}{{else}}Nothing{{end}}
|
||||
DoingNext: {{if .CurrentState.DoingNext}}{{.CurrentState.DoingNext}}{{else}}Nothing{{end}}
|
||||
Your permanent goal is: {{if .PermanentGoal}}{{.PermanentGoal}}{{else}}Nothing{{end}}
|
||||
Your current goal is: {{if .CurrentState.Goal}}{{.CurrentState.Goal}}{{else}}Nothing{{end}}
|
||||
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:
|
||||
|
||||
{{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}}
|
||||
|
||||
Act like a 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.
|
||||
For example, if the permanent goal is to "make a sandwich", you might want to "get the bread" first, and update the state afterwards by calling two tools in sequence.
|
||||
You can update the short-term goal, the current action, the next action, the history of actions, and the memories.
|
||||
You can't ask things to the user as you are thinking by yourself.
|
||||
|
||||
{{if .Reasoning}}Reasoning: {{.Reasoning}}{{end}}
|
||||
` + hud
|
||||
|
||||
const reSelfEvalTemplate = pickSelfTemplate + `
|
||||
|
||||
We already have called other tools. Evaluate the current situation and decide if we need to execute other tools.`
|
||||
|
||||
const pickActionTemplate = hud + `
|
||||
You can take any of the following tools:
|
||||
|
||||
{{range .Actions -}}
|
||||
- {{.Name}}: {{.Description }}
|
||||
{{ end }}
|
||||
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'.
|
||||
|
||||
{{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}}
|
||||
|
||||
{{if .Reasoning}}Reasoning: {{.Reasoning}}{{end}}
|
||||
`
|
||||
|
||||
const reEvalTemplate = pickActionTemplate + `
|
||||
|
||||
We already have called other tools. Evaluate the current situation and decide if we need to execute other tools or answer back with a result.`
|
||||
Reference in New Issue
Block a user