881 lines
27 KiB
Go
881 lines
27 KiB
Go
/*
|
||
* Copyright 2025 CloudWeGo Authors
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
|
||
// Package planexecute implements a plan–execute–replan style agent.
|
||
package planexecute
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"runtime/debug"
|
||
"strings"
|
||
|
||
"github.com/bytedance/sonic"
|
||
|
||
"github.com/cloudwego/eino/adk"
|
||
"github.com/cloudwego/eino/components/model"
|
||
"github.com/cloudwego/eino/components/prompt"
|
||
"github.com/cloudwego/eino/compose"
|
||
"github.com/cloudwego/eino/internal/safe"
|
||
"github.com/cloudwego/eino/schema"
|
||
)
|
||
|
||
func init() {
|
||
schema.RegisterName[*defaultPlan]("_eino_adk_plan_execute_default_plan")
|
||
schema.RegisterName[ExecutedStep]("_eino_adk_plan_execute_executed_step")
|
||
schema.RegisterName[[]ExecutedStep]("_eino_adk_plan_execute_executed_steps")
|
||
}
|
||
|
||
// Plan represents an execution plan with a sequence of actionable steps.
|
||
// It supports JSON serialization and deserialization while providing access to the first step.
|
||
type Plan interface {
|
||
// FirstStep returns the first step to be executed in the plan.
|
||
FirstStep() string
|
||
|
||
// Marshaler serializes the Plan into JSON.
|
||
// The resulting JSON can be used in prompt templates.
|
||
json.Marshaler
|
||
// Unmarshaler deserializes JSON content into the Plan.
|
||
// This processes output from structured chat models or tool calls into the Plan structure.
|
||
json.Unmarshaler
|
||
}
|
||
|
||
// NewPlan is a function type that creates a new Plan instance.
|
||
type NewPlan func(ctx context.Context) Plan
|
||
|
||
// defaultPlan is the default implementation of the Plan interface.
|
||
//
|
||
// JSON Schema:
|
||
//
|
||
// {
|
||
// "type": "object",
|
||
// "properties": {
|
||
// "steps": {
|
||
// "type": "array",
|
||
// "items": {
|
||
// "type": "string"
|
||
// },
|
||
// "description": "Ordered list of actions to be taken. Each step should be clear, actionable, and arranged in a logical sequence."
|
||
// }
|
||
// },
|
||
// "required": ["steps"]
|
||
// }
|
||
type defaultPlan struct {
|
||
// Steps contains the ordered list of actions to be taken.
|
||
// Each step should be clear, actionable, and arranged in a logical sequence.
|
||
Steps []string `json:"steps"`
|
||
}
|
||
|
||
// FirstStep returns the first step in the plan or an empty string if no steps exist.
|
||
func (p *defaultPlan) FirstStep() string {
|
||
if len(p.Steps) == 0 {
|
||
return ""
|
||
}
|
||
return p.Steps[0]
|
||
}
|
||
|
||
func (p *defaultPlan) MarshalJSON() ([]byte, error) {
|
||
type planTyp defaultPlan
|
||
return sonic.Marshal((*planTyp)(p))
|
||
}
|
||
|
||
func (p *defaultPlan) UnmarshalJSON(bytes []byte) error {
|
||
type planTyp defaultPlan
|
||
return sonic.Unmarshal(bytes, (*planTyp)(p))
|
||
}
|
||
|
||
// Response represents the final response to the user.
|
||
// This struct is used for JSON serialization/deserialization of the final response
|
||
// generated by the model.
|
||
type Response struct {
|
||
// Response is the complete response to provide to the user.
|
||
// This field is required.
|
||
Response string `json:"response"`
|
||
}
|
||
|
||
var (
|
||
// PlanToolInfo defines the schema for the Plan tool that can be used with ToolCallingChatModel.
|
||
// This schema instructs the model to generate a structured plan with ordered steps.
|
||
PlanToolInfo = schema.ToolInfo{
|
||
Name: "plan",
|
||
Desc: "Plan with a list of steps to execute in order. Each step should be clear, actionable, and arranged in a logical sequence. The output will be used to guide the execution process.",
|
||
ParamsOneOf: schema.NewParamsOneOfByParams(
|
||
map[string]*schema.ParameterInfo{
|
||
"steps": {
|
||
Type: schema.Array,
|
||
ElemInfo: &schema.ParameterInfo{Type: schema.String},
|
||
Desc: "different steps to follow, should be in sorted order",
|
||
Required: true,
|
||
},
|
||
},
|
||
),
|
||
}
|
||
|
||
// RespondToolInfo defines the schema for the response tool that can be used with ToolCallingChatModel.
|
||
// This schema instructs the model to generate a direct response to the user.
|
||
RespondToolInfo = schema.ToolInfo{
|
||
Name: "respond",
|
||
Desc: "Generate a direct response to the user. Use this tool when you have all the information needed to provide a final answer.",
|
||
ParamsOneOf: schema.NewParamsOneOfByParams(
|
||
map[string]*schema.ParameterInfo{
|
||
"response": {
|
||
Type: schema.String,
|
||
Desc: "The complete response to provide to the user",
|
||
Required: true,
|
||
},
|
||
},
|
||
),
|
||
}
|
||
|
||
// PlannerPrompt is the prompt template for the planner.
|
||
// It provides context and guidance to the planner on how to generate the Plan.
|
||
PlannerPrompt = prompt.FromMessages(schema.FString,
|
||
schema.SystemMessage(`You are an expert planning agent. Given an objective, create a comprehensive step-by-step plan to achieve the objective.
|
||
|
||
## YOUR TASK
|
||
Analyze the objective and generate a strategic plan that breaks down the goal into manageable, executable steps.
|
||
|
||
## PLANNING REQUIREMENTS
|
||
Each step in your plan must be:
|
||
- **Specific and actionable**: Clear instructions that can be executed without ambiguity
|
||
- **Self-contained**: Include all necessary context, parameters, and requirements
|
||
- **Independently executable**: Can be performed by an agent without dependencies on other steps
|
||
- **Logically sequenced**: Arranged in optimal order for efficient execution
|
||
- **Objective-focused**: Directly contribute to achieving the main goal
|
||
|
||
## PLANNING GUIDELINES
|
||
- Eliminate redundant or unnecessary steps
|
||
- Include relevant constraints, parameters, and success criteria for each step
|
||
- Ensure the final step produces a complete answer or deliverable
|
||
- Anticipate potential challenges and include mitigation strategies
|
||
- Structure steps to build upon each other logically
|
||
- Provide sufficient detail for successful execution
|
||
|
||
## QUALITY CRITERIA
|
||
- Plan completeness: Does it address all aspects of the objective?
|
||
- Step clarity: Can each step be understood and executed independently?
|
||
- Logical flow: Do steps follow a sensible progression?
|
||
- Efficiency: Is this the most direct path to the objective?
|
||
- Adaptability: Can the plan handle unexpected results or changes?`),
|
||
schema.MessagesPlaceholder("input", false),
|
||
)
|
||
|
||
// ExecutorPrompt is the prompt template for the executor.
|
||
// It provides context and guidance to the executor on how to execute the Task.
|
||
ExecutorPrompt = prompt.FromMessages(schema.FString,
|
||
schema.SystemMessage(`You are a diligent and meticulous executor agent. Follow the given plan and execute your tasks carefully and thoroughly.`),
|
||
schema.UserMessage(`## OBJECTIVE
|
||
{input}
|
||
## Given the following plan:
|
||
{plan}
|
||
## COMPLETED STEPS & RESULTS
|
||
{executed_steps}
|
||
## Your task is to execute the first step, which is:
|
||
{step}`))
|
||
|
||
// ReplannerPrompt is the prompt template for the replanner.
|
||
// It provides context and guidance to the replanner on how to regenerate the Plan.
|
||
ReplannerPrompt = prompt.FromMessages(schema.FString,
|
||
schema.SystemMessage(
|
||
`You are going to review the progress toward an objective. Analyze the current state and determine the optimal next action.
|
||
|
||
## YOUR TASK
|
||
Based on the progress above, you MUST choose exactly ONE action:
|
||
|
||
### Option 1: COMPLETE (if objective is fully achieved)
|
||
Call '{respond_tool}' with:
|
||
- A comprehensive final answer
|
||
- Clear conclusion summarizing how the objective was met
|
||
- Key insights from the execution process
|
||
|
||
### Option 2: CONTINUE (if more work is needed)
|
||
Call '{plan_tool}' with a revised plan that:
|
||
- Contains ONLY remaining steps (exclude completed ones)
|
||
- Incorporates lessons learned from executed steps
|
||
- Addresses any gaps or issues discovered
|
||
- Maintains logical step sequence
|
||
|
||
## PLANNING REQUIREMENTS
|
||
Each step in your plan must be:
|
||
- **Specific and actionable**: Clear instructions that can be executed without ambiguity
|
||
- **Self-contained**: Include all necessary context, parameters, and requirements
|
||
- **Independently executable**: Can be performed by an agent without dependencies on other steps
|
||
- **Logically sequenced**: Arranged in optimal order for efficient execution
|
||
- **Objective-focused**: Directly contribute to achieving the main goal
|
||
|
||
## PLANNING GUIDELINES
|
||
- Eliminate redundant or unnecessary steps
|
||
- Adapt strategy based on new information
|
||
- Include relevant constraints, parameters, and success criteria for each step
|
||
|
||
## DECISION CRITERIA
|
||
- Has the original objective been completely satisfied?
|
||
- Are there any remaining requirements or sub-goals?
|
||
- Do the results suggest a need for strategy adjustment?
|
||
- What specific actions are still required?`),
|
||
schema.UserMessage(`## OBJECTIVE
|
||
{input}
|
||
|
||
## ORIGINAL PLAN
|
||
{plan}
|
||
|
||
## COMPLETED STEPS & RESULTS
|
||
{executed_steps}`),
|
||
)
|
||
)
|
||
|
||
const (
|
||
// UserInputSessionKey is the session key for the user input.
|
||
UserInputSessionKey = "UserInput"
|
||
|
||
// PlanSessionKey is the session key for the plan.
|
||
PlanSessionKey = "Plan"
|
||
|
||
// ExecutedStepSessionKey is the session key for the execute result.
|
||
ExecutedStepSessionKey = "ExecutedStep"
|
||
|
||
// ExecutedStepsSessionKey is the session key for the execute results.
|
||
ExecutedStepsSessionKey = "ExecutedSteps"
|
||
)
|
||
|
||
// PlannerConfig provides configuration options for creating a planner agent.
|
||
// There are two ways to configure the planner to generate structured Plan output:
|
||
// 1. Use ChatModelWithFormattedOutput: A model pre-configured to output in the Plan format
|
||
// 2. Use ToolCallingChatModel + ToolInfo: A model that uses tool calling to generate
|
||
// the Plan structure
|
||
type PlannerConfig struct {
|
||
// ChatModelWithFormattedOutput is a model pre-configured to output in the Plan format.
|
||
// Create this by configuring a model to output structured data directly.
|
||
// See example: https://github.com/cloudwego/eino-ext/blob/main/components/model/openai/examples/structured/structured.go
|
||
ChatModelWithFormattedOutput model.BaseChatModel
|
||
|
||
// ToolCallingChatModel is a model that supports tool calling capabilities.
|
||
// When provided with ToolInfo, it will use tool calling to generate the Plan structure.
|
||
ToolCallingChatModel model.ToolCallingChatModel
|
||
|
||
// ToolInfo defines the schema for the Plan structure when using tool calling.
|
||
// Optional. If not provided, PlanToolInfo will be used as the default.
|
||
ToolInfo *schema.ToolInfo
|
||
|
||
// GenInputFn is a function that generates the input messages for the planner.
|
||
// Optional. If not provided, defaultGenPlannerInputFn will be used.
|
||
GenInputFn GenPlannerModelInputFn
|
||
|
||
// NewPlan creates a new Plan instance for JSON.
|
||
// The returned Plan will be used to unmarshal the model-generated JSON output.
|
||
// Optional. If not provided, defaultNewPlan will be used.
|
||
NewPlan NewPlan
|
||
}
|
||
|
||
// GenPlannerModelInputFn is a function type that generates input messages for the planner.
|
||
type GenPlannerModelInputFn func(ctx context.Context, userInput []adk.Message) ([]adk.Message, error)
|
||
|
||
func defaultNewPlan(ctx context.Context) Plan {
|
||
return &defaultPlan{}
|
||
}
|
||
|
||
func defaultGenPlannerInputFn(ctx context.Context, userInput []adk.Message) ([]adk.Message, error) {
|
||
msgs, err := PlannerPrompt.Format(ctx, map[string]any{
|
||
"input": userInput,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return msgs, nil
|
||
}
|
||
|
||
type planner struct {
|
||
toolCall bool
|
||
chatModel model.BaseChatModel
|
||
genInputFn GenPlannerModelInputFn
|
||
newPlan NewPlan
|
||
}
|
||
|
||
func (p *planner) Name(_ context.Context) string {
|
||
return "planner"
|
||
}
|
||
|
||
func (p *planner) Description(_ context.Context) string {
|
||
return "a planner agent"
|
||
}
|
||
|
||
func argToContent(msg adk.Message) (adk.Message, error) {
|
||
if len(msg.ToolCalls) == 0 {
|
||
return nil, schema.ErrNoValue
|
||
}
|
||
|
||
return schema.AssistantMessage(msg.ToolCalls[0].Function.Arguments, nil), nil
|
||
}
|
||
|
||
func (p *planner) Run(ctx context.Context, input *adk.AgentInput,
|
||
_ ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] {
|
||
|
||
iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
|
||
|
||
adk.AddSessionValue(ctx, UserInputSessionKey, input.Messages)
|
||
|
||
go func() {
|
||
defer func() {
|
||
panicErr := recover()
|
||
if panicErr != nil {
|
||
e := safe.NewPanicErr(panicErr, debug.Stack())
|
||
generator.Send(&adk.AgentEvent{Err: e})
|
||
}
|
||
|
||
generator.Close()
|
||
}()
|
||
|
||
c := compose.NewChain[*adk.AgentInput, Plan]().
|
||
AppendLambda(
|
||
compose.InvokableLambda(func(ctx context.Context, input *adk.AgentInput) (output []adk.Message, err error) {
|
||
return p.genInputFn(ctx, input.Messages)
|
||
}),
|
||
).
|
||
AppendChatModel(p.chatModel).
|
||
AppendLambda(
|
||
compose.CollectableLambda(func(ctx context.Context, sr *schema.StreamReader[adk.Message]) (adk.Message, error) {
|
||
if input.EnableStreaming {
|
||
ss := sr.Copy(2)
|
||
var sOutput *schema.StreamReader[*schema.Message]
|
||
if p.toolCall {
|
||
sOutput = schema.StreamReaderWithConvert(ss[0], argToContent)
|
||
} else {
|
||
sOutput = ss[0]
|
||
}
|
||
|
||
generator.Send(adk.EventFromMessage(nil, sOutput, schema.Assistant, ""))
|
||
|
||
return schema.ConcatMessageStream(ss[1])
|
||
}
|
||
|
||
msg, err := schema.ConcatMessageStream(sr)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var output adk.Message
|
||
if p.toolCall {
|
||
if len(msg.ToolCalls) == 0 {
|
||
return nil, fmt.Errorf("no tool call")
|
||
}
|
||
output = schema.AssistantMessage(msg.ToolCalls[0].Function.Arguments, nil)
|
||
} else {
|
||
output = msg
|
||
}
|
||
|
||
generator.Send(adk.EventFromMessage(output, nil, schema.Assistant, ""))
|
||
|
||
return msg, nil
|
||
}),
|
||
).
|
||
AppendLambda(
|
||
compose.InvokableLambda(func(ctx context.Context, msg adk.Message) (plan Plan, err error) {
|
||
var planJSON string
|
||
if p.toolCall {
|
||
if len(msg.ToolCalls) == 0 {
|
||
return nil, fmt.Errorf("no tool call")
|
||
}
|
||
planJSON = msg.ToolCalls[0].Function.Arguments
|
||
} else {
|
||
planJSON = msg.Content
|
||
}
|
||
|
||
plan = p.newPlan(ctx)
|
||
err = plan.UnmarshalJSON([]byte(planJSON))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("unmarshal plan error: %w", err)
|
||
}
|
||
|
||
adk.AddSessionValue(ctx, PlanSessionKey, plan)
|
||
|
||
return plan, nil
|
||
}),
|
||
)
|
||
|
||
var opts []compose.Option
|
||
if p.toolCall {
|
||
opts = append(opts, compose.WithChatModelOption(model.WithToolChoice(schema.ToolChoiceForced)))
|
||
}
|
||
|
||
r, err := c.Compile(ctx, compose.WithGraphName(p.Name(ctx)))
|
||
if err != nil { // unexpected
|
||
generator.Send(&adk.AgentEvent{Err: err})
|
||
return
|
||
}
|
||
|
||
_, err = r.Stream(ctx, input, opts...)
|
||
if err != nil {
|
||
generator.Send(&adk.AgentEvent{Err: err})
|
||
return
|
||
}
|
||
}()
|
||
|
||
return iterator
|
||
}
|
||
|
||
// NewPlanner creates a new planner agent based on the provided configuration.
|
||
// The planner agent uses either ChatModelWithFormattedOutput or ToolCallingChatModel+ToolInfo
|
||
// to generate structured Plan output.
|
||
//
|
||
// If ChatModelWithFormattedOutput is provided, it will be used directly.
|
||
// If ToolCallingChatModel is provided, it will be configured with ToolInfo (or PlanToolInfo by default)
|
||
// to generate structured Plan output.
|
||
func NewPlanner(_ context.Context, cfg *PlannerConfig) (adk.Agent, error) {
|
||
var chatModel model.BaseChatModel
|
||
var toolCall bool
|
||
if cfg.ChatModelWithFormattedOutput != nil {
|
||
chatModel = cfg.ChatModelWithFormattedOutput
|
||
} else {
|
||
toolCall = true
|
||
toolInfo := cfg.ToolInfo
|
||
if toolInfo == nil {
|
||
toolInfo = &PlanToolInfo
|
||
}
|
||
|
||
var err error
|
||
chatModel, err = cfg.ToolCallingChatModel.WithTools([]*schema.ToolInfo{toolInfo})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
inputFn := cfg.GenInputFn
|
||
if inputFn == nil {
|
||
inputFn = defaultGenPlannerInputFn
|
||
}
|
||
|
||
planParser := cfg.NewPlan
|
||
if planParser == nil {
|
||
planParser = defaultNewPlan
|
||
}
|
||
|
||
return &planner{
|
||
toolCall: toolCall,
|
||
chatModel: chatModel,
|
||
genInputFn: inputFn,
|
||
newPlan: planParser,
|
||
}, nil
|
||
}
|
||
|
||
// ExecutionContext is the input information for the executor and the planner.
|
||
type ExecutionContext struct {
|
||
UserInput []adk.Message
|
||
Plan Plan
|
||
ExecutedSteps []ExecutedStep
|
||
}
|
||
|
||
// GenModelInputFn is a function that generates the input messages for the executor and the planner.
|
||
type GenModelInputFn func(ctx context.Context, in *ExecutionContext) ([]adk.Message, error)
|
||
|
||
// ExecutorConfig provides configuration options for creating an executor agent.
|
||
type ExecutorConfig struct {
|
||
// Model is the chat model used by the executor.
|
||
// If the executor uses any tools, this model must support the model.WithTools call option,
|
||
// as that's how the executor configures the model with tool information.
|
||
Model model.BaseChatModel
|
||
|
||
// ToolsConfig specifies the tools available to the executor.
|
||
ToolsConfig adk.ToolsConfig
|
||
|
||
// MaxIterations defines the upper limit of ChatModel generation cycles.
|
||
// The agent will terminate with an error if this limit is exceeded.
|
||
// Optional. Defaults to 20.
|
||
MaxIterations int
|
||
|
||
// GenInputFn generates the input messages for the Executor.
|
||
// Optional. If not provided, defaultGenExecutorInputFn will be used.
|
||
GenInputFn GenModelInputFn
|
||
}
|
||
|
||
type ExecutedStep struct {
|
||
Step string
|
||
Result string
|
||
}
|
||
|
||
// NewExecutor creates a new executor agent.
|
||
func NewExecutor(ctx context.Context, cfg *ExecutorConfig) (adk.Agent, error) {
|
||
|
||
genInputFn := cfg.GenInputFn
|
||
if genInputFn == nil {
|
||
genInputFn = defaultGenExecutorInputFn
|
||
}
|
||
genInput := func(ctx context.Context, instruction string, _ *adk.AgentInput) ([]adk.Message, error) {
|
||
|
||
plan, ok := adk.GetSessionValue(ctx, PlanSessionKey)
|
||
if !ok {
|
||
panic("impossible: plan not found")
|
||
}
|
||
plan_ := plan.(Plan)
|
||
|
||
userInput, ok := adk.GetSessionValue(ctx, UserInputSessionKey)
|
||
if !ok {
|
||
panic("impossible: user input not found")
|
||
}
|
||
userInput_ := userInput.([]adk.Message)
|
||
|
||
var executedSteps_ []ExecutedStep
|
||
executedStep, ok := adk.GetSessionValue(ctx, ExecutedStepsSessionKey)
|
||
if ok {
|
||
executedSteps_ = executedStep.([]ExecutedStep)
|
||
}
|
||
|
||
in := &ExecutionContext{
|
||
UserInput: userInput_,
|
||
Plan: plan_,
|
||
ExecutedSteps: executedSteps_,
|
||
}
|
||
|
||
msgs, err := genInputFn(ctx, in)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return msgs, nil
|
||
}
|
||
|
||
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
|
||
Name: "executor",
|
||
Description: "an executor agent",
|
||
Model: cfg.Model,
|
||
ToolsConfig: cfg.ToolsConfig,
|
||
GenModelInput: genInput,
|
||
MaxIterations: cfg.MaxIterations,
|
||
OutputKey: ExecutedStepSessionKey,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return agent, nil
|
||
}
|
||
|
||
func defaultGenExecutorInputFn(ctx context.Context, in *ExecutionContext) ([]adk.Message, error) {
|
||
|
||
planContent, err := in.Plan.MarshalJSON()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
userMsgs, err := ExecutorPrompt.Format(ctx, map[string]any{
|
||
"input": formatInput(in.UserInput),
|
||
"plan": string(planContent),
|
||
"executed_steps": formatExecutedSteps(in.ExecutedSteps),
|
||
"step": in.Plan.FirstStep(),
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return userMsgs, nil
|
||
}
|
||
|
||
type replanner struct {
|
||
chatModel model.ToolCallingChatModel
|
||
planTool *schema.ToolInfo
|
||
respondTool *schema.ToolInfo
|
||
|
||
genInputFn GenModelInputFn
|
||
newPlan NewPlan
|
||
}
|
||
|
||
type ReplannerConfig struct {
|
||
// ChatModel is the model that supports tool calling capabilities.
|
||
// It will be configured with PlanTool and RespondTool to generate updated plans or responses.
|
||
ChatModel model.ToolCallingChatModel
|
||
|
||
// PlanTool defines the schema for the Plan tool that can be used with ToolCallingChatModel.
|
||
// Optional. If not provided, the default PlanToolInfo will be used.
|
||
PlanTool *schema.ToolInfo
|
||
|
||
// RespondTool defines the schema for the response tool that can be used with ToolCallingChatModel.
|
||
// Optional. If not provided, the default RespondToolInfo will be used.
|
||
RespondTool *schema.ToolInfo
|
||
|
||
// GenInputFn generates the input messages for the Replanner.
|
||
// Optional. If not provided, buildGenReplannerInputFn will be used.
|
||
GenInputFn GenModelInputFn
|
||
|
||
// NewPlan creates a new Plan instance.
|
||
// The returned Plan will be used to unmarshal the model-generated JSON output from PlanTool.
|
||
// Optional. If not provided, defaultNewPlan will be used.
|
||
NewPlan NewPlan
|
||
}
|
||
|
||
// formatInput formats the input messages into a string.
|
||
func formatInput(input []adk.Message) string {
|
||
var sb strings.Builder
|
||
for _, msg := range input {
|
||
sb.WriteString(msg.Content)
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
return sb.String()
|
||
}
|
||
|
||
func formatExecutedSteps(results []ExecutedStep) string {
|
||
var sb strings.Builder
|
||
for _, result := range results {
|
||
sb.WriteString(fmt.Sprintf("Step: %s\nResult: %s\n\n", result.Step, result.Result))
|
||
}
|
||
|
||
return sb.String()
|
||
}
|
||
|
||
func (r *replanner) Name(_ context.Context) string {
|
||
return "replanner"
|
||
}
|
||
|
||
func (r *replanner) Description(_ context.Context) string {
|
||
return "a replanner agent"
|
||
}
|
||
|
||
func (r *replanner) genInput(ctx context.Context) ([]adk.Message, error) {
|
||
|
||
executedStep, ok := adk.GetSessionValue(ctx, ExecutedStepSessionKey)
|
||
if !ok {
|
||
panic("impossible: execute result not found")
|
||
}
|
||
executedStep_ := executedStep.(string)
|
||
|
||
plan, ok := adk.GetSessionValue(ctx, PlanSessionKey)
|
||
if !ok {
|
||
panic("impossible: plan not found")
|
||
}
|
||
plan_ := plan.(Plan)
|
||
step := plan_.FirstStep()
|
||
|
||
var executedSteps_ []ExecutedStep
|
||
executedSteps, ok := adk.GetSessionValue(ctx, ExecutedStepsSessionKey)
|
||
if ok {
|
||
executedSteps_ = executedSteps.([]ExecutedStep)
|
||
}
|
||
|
||
executedSteps_ = append(executedSteps_, ExecutedStep{
|
||
Step: step,
|
||
Result: executedStep_,
|
||
})
|
||
adk.AddSessionValue(ctx, ExecutedStepsSessionKey, executedSteps_)
|
||
|
||
userInput, ok := adk.GetSessionValue(ctx, UserInputSessionKey)
|
||
if !ok {
|
||
panic("impossible: user input not found")
|
||
}
|
||
userInput_ := userInput.([]adk.Message)
|
||
|
||
in := &ExecutionContext{
|
||
UserInput: userInput_,
|
||
Plan: plan_,
|
||
ExecutedSteps: executedSteps_,
|
||
}
|
||
genInputFn := r.genInputFn
|
||
if genInputFn == nil {
|
||
genInputFn = buildGenReplannerInputFn(r.planTool.Name, r.respondTool.Name)
|
||
}
|
||
msgs, err := genInputFn(ctx, in)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return msgs, nil
|
||
}
|
||
|
||
func (r *replanner) Run(ctx context.Context, input *adk.AgentInput, _ ...adk.AgentRunOption) *adk.AsyncIterator[*adk.AgentEvent] {
|
||
iterator, generator := adk.NewAsyncIteratorPair[*adk.AgentEvent]()
|
||
|
||
go func() {
|
||
defer func() {
|
||
panicErr := recover()
|
||
if panicErr != nil {
|
||
e := safe.NewPanicErr(panicErr, debug.Stack())
|
||
generator.Send(&adk.AgentEvent{Err: e})
|
||
}
|
||
|
||
generator.Close()
|
||
}()
|
||
|
||
callOpt := model.WithToolChoice(schema.ToolChoiceForced)
|
||
|
||
c := compose.NewChain[struct{}, any]().
|
||
AppendLambda(
|
||
compose.InvokableLambda(func(ctx context.Context, input struct{}) (output []adk.Message, err error) {
|
||
return r.genInput(ctx)
|
||
}),
|
||
).
|
||
AppendChatModel(r.chatModel).
|
||
AppendLambda(
|
||
compose.CollectableLambda(func(ctx context.Context, sr *schema.StreamReader[adk.Message]) (adk.Message, error) {
|
||
if input.EnableStreaming {
|
||
ss := sr.Copy(2)
|
||
sOutput := schema.StreamReaderWithConvert(ss[0], argToContent)
|
||
generator.Send(adk.EventFromMessage(nil, sOutput, schema.Assistant, ""))
|
||
return schema.ConcatMessageStream(ss[1])
|
||
}
|
||
|
||
msg, err := schema.ConcatMessageStream(sr)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(msg.ToolCalls) > 0 {
|
||
output := schema.AssistantMessage(msg.ToolCalls[0].Function.Arguments, nil)
|
||
generator.Send(adk.EventFromMessage(output, nil, schema.Assistant, ""))
|
||
}
|
||
return msg, nil
|
||
}),
|
||
).
|
||
AppendLambda(
|
||
compose.InvokableLambda(func(ctx context.Context, msg adk.Message) (msgOrPlan any, err error) {
|
||
if len(msg.ToolCalls) == 0 {
|
||
return nil, fmt.Errorf("no tool call")
|
||
}
|
||
|
||
// exit
|
||
if msg.ToolCalls[0].Function.Name == r.respondTool.Name {
|
||
action := adk.NewBreakLoopAction(r.Name(ctx))
|
||
generator.Send(&adk.AgentEvent{Action: action})
|
||
return msg, nil
|
||
}
|
||
|
||
// replan
|
||
if msg.ToolCalls[0].Function.Name != r.planTool.Name {
|
||
return nil, fmt.Errorf("unexpected tool call: %s", msg.ToolCalls[0].Function.Name)
|
||
}
|
||
|
||
plan := r.newPlan(ctx)
|
||
if err = plan.UnmarshalJSON([]byte(msg.ToolCalls[0].Function.Arguments)); err != nil {
|
||
return nil, fmt.Errorf("unmarshal plan error: %w", err)
|
||
}
|
||
|
||
adk.AddSessionValue(ctx, PlanSessionKey, plan)
|
||
|
||
return plan, nil
|
||
}),
|
||
)
|
||
|
||
runnable, err := c.Compile(ctx, compose.WithGraphName(r.Name(ctx)))
|
||
if err != nil {
|
||
generator.Send(&adk.AgentEvent{Err: err})
|
||
return
|
||
}
|
||
|
||
_, err = runnable.Stream(ctx, struct{}{}, compose.WithChatModelOption(callOpt))
|
||
if err != nil {
|
||
generator.Send(&adk.AgentEvent{Err: err})
|
||
return
|
||
}
|
||
}()
|
||
|
||
return iterator
|
||
}
|
||
|
||
func buildGenReplannerInputFn(planToolName, respondToolName string) GenModelInputFn {
|
||
return func(ctx context.Context, in *ExecutionContext) ([]adk.Message, error) {
|
||
planContent, err := in.Plan.MarshalJSON()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
msgs, err := ReplannerPrompt.Format(ctx, map[string]any{
|
||
"plan": string(planContent),
|
||
"input": formatInput(in.UserInput),
|
||
"executed_steps": formatExecutedSteps(in.ExecutedSteps),
|
||
"plan_tool": planToolName,
|
||
"respond_tool": respondToolName,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return msgs, nil
|
||
}
|
||
}
|
||
|
||
// NewReplanner creates a plan-execute-replan agent wired with plan and respond tools.
|
||
// It configures the provided ToolCallingChatModel with the tools and returns an Agent.
|
||
func NewReplanner(_ context.Context, cfg *ReplannerConfig) (adk.Agent, error) {
|
||
planTool := cfg.PlanTool
|
||
if planTool == nil {
|
||
planTool = &PlanToolInfo
|
||
}
|
||
|
||
respondTool := cfg.RespondTool
|
||
if respondTool == nil {
|
||
respondTool = &RespondToolInfo
|
||
}
|
||
|
||
chatModel, err := cfg.ChatModel.WithTools([]*schema.ToolInfo{planTool, respondTool})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
planParser := cfg.NewPlan
|
||
if planParser == nil {
|
||
planParser = defaultNewPlan
|
||
}
|
||
|
||
return &replanner{
|
||
chatModel: chatModel,
|
||
planTool: planTool,
|
||
respondTool: respondTool,
|
||
genInputFn: cfg.GenInputFn,
|
||
newPlan: planParser,
|
||
}, nil
|
||
}
|
||
|
||
// Config provides configuration options for creating a plan-execute-replan agent.
|
||
type Config struct {
|
||
// Planner specifies the agent that generates the plan.
|
||
// You can use provided NewPlanner to create a planner agent.
|
||
Planner adk.Agent
|
||
|
||
// Executor specifies the agent that executes the plan generated by planner or replanner.
|
||
// You can use provided NewExecutor to create an executor agent.
|
||
Executor adk.Agent
|
||
|
||
// Replanner specifies the agent that replans the plan.
|
||
// You can use provided NewReplanner to create a replanner agent.
|
||
Replanner adk.Agent
|
||
|
||
// MaxIterations defines the maximum number of loops for 'execute-replan'.
|
||
// Optional. If not provided, 10 will be used as the default.
|
||
MaxIterations int
|
||
}
|
||
|
||
// New creates a new plan-execute-replan agent with the given configuration.
|
||
// The plan-execute-replan pattern works in three phases:
|
||
// 1. Planning: Generate a structured plan with clear, actionable steps
|
||
// 2. Execution: Execute the first step of the plan
|
||
// 3. Replanning: Evaluate progress and either complete the task or revise the plan
|
||
// This approach enables complex problem-solving through iterative refinement.
|
||
func New(ctx context.Context, cfg *Config) (adk.ResumableAgent, error) {
|
||
maxIterations := cfg.MaxIterations
|
||
if maxIterations <= 0 {
|
||
maxIterations = 10
|
||
}
|
||
loop, err := adk.NewLoopAgent(ctx, &adk.LoopAgentConfig{
|
||
Name: "execute_replan",
|
||
SubAgents: []adk.Agent{cfg.Executor, cfg.Replanner},
|
||
MaxIterations: maxIterations,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return adk.NewSequentialAgent(ctx, &adk.SequentialAgentConfig{
|
||
Name: "plan_execute_replan",
|
||
SubAgents: []adk.Agent{cfg.Planner, loop},
|
||
})
|
||
}
|