This commit is contained in:
mudler
2024-04-04 20:00:58 +02:00
parent b4fd482f66
commit 79e5dffe09
4 changed files with 187 additions and 107 deletions

View File

@@ -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())
}
err = promptTemplate.Execute(prompt, struct {
Actions []action.ActionDefinition
Messages []openai.ChatCompletionMessage
}{
Actions: definitions,
Messages: messages,
})
if err != nil {
return nil, "", err
var promptHUD *PromptHUD
if a.options.enableHUD {
h := a.prepareHUD()
promptHUD = &h
}
err = hudTmpl.Execute(hud, a.prepareHUD())
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
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)
}

View File

@@ -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,43 +328,48 @@ func (a *Agent) consumeJob(job *Job, role string) {
}
func (a *Agent) periodicallyRun() {
// 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`,
))
a.consumeJob(evaluateMemory, SystemRole)
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`,
))
a.consumeJob(evaluateMemory, SystemRole)
a.ResetConversation()
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 {

View File

@@ -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
View 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.`