536 lines
14 KiB
Go
536 lines
14 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/mudler/LocalAGI/core/action"
|
|
"github.com/mudler/LocalAGI/core/types"
|
|
|
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
|
|
|
"github.com/sashabaranov/go-openai"
|
|
)
|
|
|
|
type decisionResult struct {
|
|
actionParams types.ActionParams
|
|
message string
|
|
actioName string
|
|
}
|
|
|
|
// decision forces the agent to take one of the available actions
|
|
func (a *Agent) decision(
|
|
ctx context.Context,
|
|
conversation []openai.ChatCompletionMessage,
|
|
tools []openai.Tool, toolchoice any, maxRetries int) (*decisionResult, error) {
|
|
|
|
var lastErr error
|
|
for attempts := 0; attempts < maxRetries; attempts++ {
|
|
decision := openai.ChatCompletionRequest{
|
|
Model: a.options.LLMAPI.Model,
|
|
Messages: conversation,
|
|
Tools: tools,
|
|
ToolChoice: toolchoice,
|
|
}
|
|
|
|
resp, err := a.client.CreateChatCompletion(ctx, decision)
|
|
if err != nil {
|
|
lastErr = err
|
|
xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", err)
|
|
continue
|
|
}
|
|
|
|
if len(resp.Choices) != 1 {
|
|
lastErr = fmt.Errorf("no choices: %d", len(resp.Choices))
|
|
xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", lastErr)
|
|
continue
|
|
}
|
|
|
|
msg := resp.Choices[0].Message
|
|
if len(msg.ToolCalls) != 1 {
|
|
if err := a.saveConversation(append(conversation, msg), "decision"); err != nil {
|
|
xlog.Error("Error saving conversation", "error", err)
|
|
}
|
|
return &decisionResult{message: msg.Content}, nil
|
|
}
|
|
|
|
params := types.ActionParams{}
|
|
if err := params.Read(msg.ToolCalls[0].Function.Arguments); err != nil {
|
|
lastErr = err
|
|
xlog.Warn("Attempt to parse action parameters failed", "attempt", attempts+1, "error", err)
|
|
continue
|
|
}
|
|
|
|
if err := a.saveConversation(append(conversation, msg), "decision"); err != nil {
|
|
xlog.Error("Error saving conversation", "error", err)
|
|
}
|
|
|
|
return &decisionResult{actionParams: params, actioName: msg.ToolCalls[0].Function.Name, message: msg.Content}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to make a decision after %d attempts: %w", maxRetries, lastErr)
|
|
}
|
|
|
|
type Messages []openai.ChatCompletionMessage
|
|
|
|
func (m Messages) ToOpenAI() []openai.ChatCompletionMessage {
|
|
return []openai.ChatCompletionMessage(m)
|
|
}
|
|
|
|
func (m Messages) RemoveIf(f func(msg openai.ChatCompletionMessage) bool) Messages {
|
|
for i := len(m) - 1; i >= 0; i-- {
|
|
if f(m[i]) {
|
|
m = append(m[:i], m[i+1:]...)
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (m Messages) String() string {
|
|
s := ""
|
|
for _, cc := range m {
|
|
s += cc.Role + ": " + cc.Content + "\n"
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (m Messages) Exist(content string) bool {
|
|
for _, cc := range m {
|
|
if cc.Content == content {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m Messages) RemoveLastUserMessage() Messages {
|
|
if len(m) == 0 {
|
|
return m
|
|
}
|
|
|
|
for i := len(m) - 1; i >= 0; i-- {
|
|
if m[i].Role == UserRole {
|
|
return append(m[:i], m[i+1:]...)
|
|
}
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
func (m Messages) Save(path string) error {
|
|
content, err := json.MarshalIndent(m, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
if _, err := f.Write(content); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m Messages) GetLatestUserMessage() *openai.ChatCompletionMessage {
|
|
for i := len(m) - 1; i >= 0; i-- {
|
|
msg := m[i]
|
|
if msg.Role == UserRole {
|
|
return &msg
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m Messages) IsLastMessageFromRole(role string) bool {
|
|
if len(m) == 0 {
|
|
return false
|
|
}
|
|
|
|
return m[len(m)-1].Role == role
|
|
}
|
|
|
|
func (a *Agent) generateParameters(ctx context.Context, pickTemplate string, act types.Action, c []openai.ChatCompletionMessage, reasoning string, maxAttempts int) (*decisionResult, error) {
|
|
stateHUD, err := renderTemplate(pickTemplate, a.prepareHUD(), a.availableActions(), reasoning)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conversation := c
|
|
if !Messages(c).Exist(stateHUD) && a.options.enableHUD {
|
|
conversation = append([]openai.ChatCompletionMessage{
|
|
{
|
|
Role: "system",
|
|
Content: stateHUD,
|
|
},
|
|
}, conversation...)
|
|
}
|
|
|
|
cc := conversation
|
|
if a.options.forceReasoning {
|
|
cc = append(conversation, openai.ChatCompletionMessage{
|
|
Role: "system",
|
|
Content: fmt.Sprintf("The agent decided to use the tool %s with the following reasoning: %s", act.Definition().Name, reasoning),
|
|
})
|
|
}
|
|
|
|
var result *decisionResult
|
|
var attemptErr error
|
|
|
|
for attempts := 0; attempts < maxAttempts; attempts++ {
|
|
result, attemptErr = a.decision(ctx,
|
|
cc,
|
|
a.availableActions().ToTools(),
|
|
openai.ToolChoice{
|
|
Type: openai.ToolTypeFunction,
|
|
Function: openai.ToolFunction{Name: act.Definition().Name.String()},
|
|
},
|
|
maxAttempts,
|
|
)
|
|
if attemptErr == nil && result.actionParams != nil {
|
|
return result, nil
|
|
}
|
|
xlog.Warn("Attempt to generate parameters failed", "attempt", attempts+1, "error", attemptErr)
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to generate parameters after %d attempts: %w", maxAttempts, attemptErr)
|
|
}
|
|
|
|
func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction types.Action, actionParams types.ActionParams, reasoning string, pickTemplate string, conv Messages) (Messages, error) {
|
|
// Planning: run all the actions in sequence
|
|
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
|
|
xlog.Debug("no plan action")
|
|
return conv, nil
|
|
}
|
|
|
|
xlog.Debug("[planning]...")
|
|
planResult := action.PlanResult{}
|
|
if err := actionParams.Unmarshal(&planResult); err != nil {
|
|
return conv, fmt.Errorf("error unmarshalling plan result: %w", err)
|
|
}
|
|
|
|
stateResult := types.ActionState{
|
|
ActionCurrentState: types.ActionCurrentState{
|
|
Job: job,
|
|
Action: chosenAction,
|
|
Params: actionParams,
|
|
Reasoning: reasoning,
|
|
},
|
|
ActionResult: types.ActionResult{
|
|
Result: fmt.Sprintf("planning %s, subtasks: %+v", planResult.Goal, planResult.Subtasks),
|
|
},
|
|
}
|
|
job.Result.SetResult(stateResult)
|
|
job.CallbackWithResult(stateResult)
|
|
|
|
xlog.Info("[Planning] starts", "agent", a.Character.Name, "goal", planResult.Goal)
|
|
for _, s := range planResult.Subtasks {
|
|
xlog.Info("[Planning] subtask", "agent", a.Character.Name, "action", s.Action, "reasoning", s.Reasoning)
|
|
}
|
|
|
|
if len(planResult.Subtasks) == 0 {
|
|
return conv, fmt.Errorf("no subtasks")
|
|
}
|
|
|
|
// Execute all subtasks in sequence
|
|
for _, subtask := range planResult.Subtasks {
|
|
xlog.Info("[subtask] Generating parameters",
|
|
"agent", a.Character.Name,
|
|
"action", subtask.Action,
|
|
"reasoning", reasoning,
|
|
)
|
|
|
|
subTaskAction := a.availableActions().Find(subtask.Action)
|
|
subTaskReasoning := fmt.Sprintf("%s Overall goal is: %s", subtask.Reasoning, planResult.Goal)
|
|
|
|
params, err := a.generateParameters(ctx, pickTemplate, subTaskAction, conv, subTaskReasoning, maxRetries)
|
|
if err != nil {
|
|
return conv, fmt.Errorf("error generating action's parameters: %w", err)
|
|
|
|
}
|
|
actionParams = params.actionParams
|
|
|
|
if !job.Callback(types.ActionCurrentState{
|
|
Job: job,
|
|
Action: subTaskAction,
|
|
Params: actionParams,
|
|
Reasoning: subTaskReasoning,
|
|
}) {
|
|
job.Result.SetResult(types.ActionState{
|
|
ActionCurrentState: types.ActionCurrentState{
|
|
Job: job,
|
|
Action: chosenAction,
|
|
Params: actionParams,
|
|
Reasoning: subTaskReasoning,
|
|
},
|
|
ActionResult: types.ActionResult{
|
|
Result: "stopped by callback",
|
|
},
|
|
})
|
|
job.Result.Conversation = conv
|
|
job.Result.Finish(nil)
|
|
break
|
|
}
|
|
|
|
result, err := a.runAction(ctx, subTaskAction, actionParams)
|
|
if err != nil {
|
|
return conv, fmt.Errorf("error running action: %w", err)
|
|
}
|
|
|
|
stateResult := types.ActionState{
|
|
ActionCurrentState: types.ActionCurrentState{
|
|
Job: job,
|
|
Action: subTaskAction,
|
|
Params: actionParams,
|
|
Reasoning: subTaskReasoning,
|
|
},
|
|
ActionResult: result,
|
|
}
|
|
job.Result.SetResult(stateResult)
|
|
job.CallbackWithResult(stateResult)
|
|
xlog.Debug("[subtask] Action executed", "agent", a.Character.Name, "action", subTaskAction.Definition().Name, "result", result)
|
|
conv = a.addFunctionResultToConversation(subTaskAction, actionParams, result, conv)
|
|
}
|
|
|
|
return conv, nil
|
|
}
|
|
|
|
func (a *Agent) availableActions() types.Actions {
|
|
// defaultActions := append(a.options.userActions, action.NewReply())
|
|
|
|
addPlanAction := func(actions types.Actions) types.Actions {
|
|
if !a.options.canPlan {
|
|
return actions
|
|
}
|
|
plannablesActions := []string{}
|
|
for _, a := range actions {
|
|
if a.Plannable() {
|
|
plannablesActions = append(plannablesActions, a.Definition().Name.String())
|
|
}
|
|
}
|
|
planAction := action.NewPlan(plannablesActions)
|
|
actions = append(actions, planAction)
|
|
return actions
|
|
}
|
|
|
|
defaultActions := append(a.mcpActions, a.options.userActions...)
|
|
|
|
if a.options.initiateConversations && a.selfEvaluationInProgress { // && self-evaluation..
|
|
acts := append(defaultActions, action.NewConversation())
|
|
if a.options.enableHUD {
|
|
acts = append(acts, action.NewState())
|
|
}
|
|
//if a.options.canStopItself {
|
|
// acts = append(acts, action.NewStop())
|
|
// }
|
|
|
|
return addPlanAction(acts)
|
|
}
|
|
|
|
if a.options.canStopItself {
|
|
acts := append(defaultActions, action.NewStop())
|
|
if a.options.enableHUD {
|
|
acts = append(acts, action.NewState())
|
|
}
|
|
return addPlanAction(acts)
|
|
}
|
|
|
|
if a.options.enableHUD {
|
|
return addPlanAction(append(defaultActions, action.NewState()))
|
|
}
|
|
|
|
return addPlanAction(defaultActions)
|
|
}
|
|
|
|
func (a *Agent) prepareHUD() (promptHUD *PromptHUD) {
|
|
if !a.options.enableHUD {
|
|
return nil
|
|
}
|
|
|
|
return &PromptHUD{
|
|
Character: a.Character,
|
|
CurrentState: *a.currentState,
|
|
PermanentGoal: a.options.permanentGoal,
|
|
ShowCharacter: a.options.showCharacter,
|
|
}
|
|
}
|
|
|
|
// pickAction picks an action based on the conversation
|
|
func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.ChatCompletionMessage, maxRetries int) (types.Action, types.ActionParams, string, error) {
|
|
c := messages
|
|
|
|
xlog.Debug("[pickAction] picking action", "messages", messages)
|
|
|
|
if !a.options.forceReasoning {
|
|
xlog.Debug("not forcing reasoning")
|
|
// We also could avoid to use functions here and get just a reply from the LLM
|
|
// and then use the reply to get the action
|
|
thought, err := a.decision(ctx,
|
|
messages,
|
|
a.availableActions().ToTools(),
|
|
nil,
|
|
maxRetries)
|
|
if err != nil {
|
|
return nil, nil, "", err
|
|
}
|
|
|
|
xlog.Debug(fmt.Sprintf("thought action Name: %v", thought.actioName))
|
|
xlog.Debug(fmt.Sprintf("thought message: %v", thought.message))
|
|
|
|
// Find the action
|
|
chosenAction := a.availableActions().Find(thought.actioName)
|
|
if chosenAction == nil || thought.actioName == "" {
|
|
xlog.Debug("no answer")
|
|
|
|
// LLM replied with an answer?
|
|
//fmt.Errorf("no action found for intent:" + thought.actioName)
|
|
return nil, nil, thought.message, nil
|
|
}
|
|
xlog.Debug(fmt.Sprintf("chosenAction: %v", chosenAction.Definition().Name))
|
|
return chosenAction, thought.actionParams, thought.message, nil
|
|
}
|
|
|
|
xlog.Debug("[pickAction] forcing reasoning")
|
|
|
|
prompt, err := renderTemplate(templ, a.prepareHUD(), a.availableActions(), "")
|
|
if err != nil {
|
|
return nil, nil, "", err
|
|
}
|
|
// Get the LLM to think on what to do
|
|
// and have a thought
|
|
if !Messages(c).Exist(prompt) {
|
|
c = append([]openai.ChatCompletionMessage{
|
|
{
|
|
Role: "system",
|
|
Content: prompt,
|
|
},
|
|
}, c...)
|
|
}
|
|
|
|
actionsID := []string{}
|
|
for _, m := range a.availableActions() {
|
|
actionsID = append(actionsID, m.Definition().Name.String())
|
|
}
|
|
|
|
// thoughtPromptStringBuilder := strings.Builder{}
|
|
// thoughtPromptStringBuilder.WriteString("You have to pick an action based on the conversation and the prompt. Describe the full reasoning process for your choice. Here is a list of actions: ")
|
|
// for _, m := range a.availableActions() {
|
|
// thoughtPromptStringBuilder.WriteString(
|
|
// m.Definition().Name.String() + ": " + m.Definition().Description + "\n",
|
|
// )
|
|
// }
|
|
|
|
// thoughtPromptStringBuilder.WriteString("To not use any action, respond with 'none'")
|
|
|
|
//thoughtPromptStringBuilder.WriteString("\n\nConversation: " + Messages(c).RemoveIf(func(msg openai.ChatCompletionMessage) bool {
|
|
// return msg.Role == "system"
|
|
//}).String())
|
|
|
|
//thoughtPrompt := thoughtPromptStringBuilder.String()
|
|
|
|
//thoughtConv := []openai.ChatCompletionMessage{}
|
|
|
|
thought, err := a.askLLM(ctx,
|
|
c,
|
|
maxRetries,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, "", err
|
|
}
|
|
originalReasoning := thought.Content
|
|
|
|
// From the thought, get the action call
|
|
// Get all the available actions IDs
|
|
|
|
// by grammar, let's decide if we have achieved the goal
|
|
// 1. analyze response and check if goal is achieved
|
|
|
|
params, err := a.decision(ctx,
|
|
[]openai.ChatCompletionMessage{
|
|
{
|
|
Role: "system",
|
|
Content: "Extract an action to perform from the following reasoning: ",
|
|
},
|
|
{
|
|
Role: "user",
|
|
Content: originalReasoning,
|
|
}},
|
|
types.Actions{action.NewGoal()}.ToTools(),
|
|
action.NewGoal().Definition().Name, maxRetries)
|
|
if err != nil {
|
|
return nil, nil, "", fmt.Errorf("failed to get the action tool parameters: %v", err)
|
|
}
|
|
|
|
goalResponse := action.GoalResponse{}
|
|
err = params.actionParams.Unmarshal(&goalResponse)
|
|
if err != nil {
|
|
return nil, nil, "", err
|
|
}
|
|
|
|
if goalResponse.Achieved {
|
|
xlog.Debug("[pickAction] goal achieved", "goal", goalResponse.Goal)
|
|
return nil, nil, "", nil
|
|
}
|
|
|
|
// if the goal is not achieved, pick an action
|
|
xlog.Debug("[pickAction] goal not achieved", "goal", goalResponse.Goal)
|
|
|
|
xlog.Debug("[pickAction] thought", "conv", c, "originalReasoning", originalReasoning)
|
|
|
|
// TODO: FORCE to select ana ction here
|
|
// NOTE: we do not give the full conversation here to pick the action
|
|
// to avoid hallucinations
|
|
params, err = a.decision(ctx,
|
|
[]openai.ChatCompletionMessage{
|
|
{
|
|
Role: "system",
|
|
Content: "Extract an action to perform from the following reasoning: ",
|
|
},
|
|
{
|
|
Role: "user",
|
|
Content: originalReasoning,
|
|
}},
|
|
a.availableActions().ToTools(),
|
|
nil, maxRetries)
|
|
if err != nil {
|
|
return nil, nil, "", fmt.Errorf("failed to get the action tool parameters: %v", err)
|
|
}
|
|
|
|
chosenAction := a.availableActions().Find(params.actioName)
|
|
|
|
// xlog.Debug("[pickAction] params", "params", params)
|
|
|
|
// if params.actionParams == nil {
|
|
// return nil, nil, params.message, nil
|
|
// }
|
|
|
|
// xlog.Debug("[pickAction] actionChoice", "actionChoice", params.actionParams, "message", params.message)
|
|
|
|
// actionChoice := action.IntentResponse{}
|
|
|
|
// err = params.actionParams.Unmarshal(&actionChoice)
|
|
// if err != nil {
|
|
// return nil, nil, "", err
|
|
// }
|
|
|
|
// if actionChoice.Tool == "" || actionChoice.Tool == "none" {
|
|
// return nil, nil, "", nil
|
|
// }
|
|
|
|
// // Find the action
|
|
// chosenAction := a.availableActions().Find(actionChoice.Tool)
|
|
// if chosenAction == nil {
|
|
// return nil, nil, "", fmt.Errorf("no action found for intent:" + actionChoice.Tool)
|
|
// }
|
|
|
|
return chosenAction, nil, originalReasoning, nil
|
|
}
|