cloudwego--eino
881 行
27 KiB
Go
881 行
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},
|
|
})
|
|
}
|