Files
2026-07-13 12:49:10 +08:00

268 lines
9.6 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 deep provides a prebuilt agent with deep task orchestration.
package deep
import (
"context"
"fmt"
"github.com/bytedance/sonic"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/adk/filesystem"
"github.com/cloudwego/eino/adk/internal"
filesystem2 "github.com/cloudwego/eino/adk/middlewares/filesystem"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool/utils"
"github.com/cloudwego/eino/schema"
)
func init() {
schema.RegisterName[TODO]("_eino_adk_prebuilt_deep_todo")
schema.RegisterName[[]TODO]("_eino_adk_prebuilt_deep_todo_slice")
}
// TypedConfig defines the configuration for creating a DeepAgent parameterized by message type.
// An Agentic DeepAgent (M = *schema.AgenticMessage) only supports Agentic sub-agents,
// and a standard DeepAgent (M = *schema.Message) only supports standard sub-agents.
// This is enforced by the type system through the SubAgents field.
type TypedConfig[M adk.MessageType] struct {
// Name is the identifier for the Deep agent.
Name string
// Description provides a brief explanation of the agent's purpose.
Description string
// ChatModel is the model used by DeepAgent for reasoning and task execution.
// If the agent uses any tools, this model must support the model.WithTools call option,
// as that's how the agent configures the model with tool information.
ChatModel model.BaseModel[M]
// Instruction contains the system prompt that guides the agent's behavior.
// When empty, a built-in default system prompt will be used, which includes general assistant
// behavior guidelines, security policies, coding style guidelines, and tool usage policies.
Instruction string
// SubAgents are specialized agents that can be invoked by the agent.
// For M = *schema.AgenticMessage, only agentic sub-agents are accepted.
SubAgents []adk.TypedAgent[M]
// ToolsConfig provides the tools and tool-calling configurations available for the agent to invoke.
ToolsConfig adk.ToolsConfig
// MaxIteration limits the maximum number of reasoning iterations the agent can perform.
MaxIteration int
// Backend provides filesystem operations used by tools and offloading.
// If set, filesystem tools (read_file, write_file, edit_file, glob, grep) will be registered.
// Optional.
Backend filesystem.Backend
// Shell provides shell command execution capability.
// If set, an execute tool will be registered to support shell command execution.
// Optional. Mutually exclusive with StreamingShell.
Shell filesystem.Shell
// StreamingShell provides streaming shell command execution capability.
// If set, a streaming execute tool will be registered to support streaming shell command execution.
// Optional. Mutually exclusive with Shell.
StreamingShell filesystem.StreamingShell
// WithoutWriteTodos disables the built-in write_todos tool when set to true.
WithoutWriteTodos bool
// WithoutGeneralSubAgent disables the general-purpose subagent when set to true.
WithoutGeneralSubAgent bool
// TaskToolDescriptionGenerator allows customizing the description for the task tool.
// If provided, this function generates the tool description based on available subagents.
TaskToolDescriptionGenerator func(ctx context.Context, availableAgents []adk.TypedAgent[M]) (string, error)
Middlewares []adk.AgentMiddleware
// Handlers configures interface-based handlers for extending agent behavior.
// Unlike Middlewares (struct-based), Handlers allow users to:
// - Add custom methods to their handler implementations
// - Return modified context from handler methods
// - Centralize configuration in struct fields instead of closures
//
// Handlers are processed after Middlewares, in registration order.
// See adk.ChatModelAgentMiddleware documentation for when to use Handlers vs Middlewares.
Handlers []adk.TypedChatModelAgentMiddleware[M]
ModelRetryConfig *adk.TypedModelRetryConfig[M]
// ModelFailoverConfig configures failover behavior for the ChatModel.
// When set, the agent will automatically fail over to alternative models on errors.
// This config is also propagated to the general sub-agent.
ModelFailoverConfig *adk.ModelFailoverConfig[M]
// OutputKey stores the agent's response in the session.
// Optional. When set, stores output via AddSessionValue(ctx, outputKey, msg.Content).
OutputKey string
}
// Config defines the configuration for creating a standard DeepAgent.
type Config = TypedConfig[*schema.Message]
// NewTyped creates a new typed Deep agent instance with the provided configuration.
// This function initializes built-in tools, creates a task tool for subagent orchestration,
// and returns a fully configured TypedChatModelAgent ready for execution.
func NewTyped[M adk.MessageType](ctx context.Context, cfg *TypedConfig[M]) (adk.TypedResumableAgent[M], error) {
handlers, err := buildTypedBuiltinAgentMiddlewares(ctx, cfg)
if err != nil {
return nil, err
}
instruction := cfg.Instruction
if len(instruction) == 0 {
instruction = internal.SelectPrompt(internal.I18nPrompts{
English: baseAgentInstruction,
Chinese: baseAgentInstructionChinese,
})
}
if !cfg.WithoutGeneralSubAgent || len(cfg.SubAgents) > 0 {
tt, err := typedTaskToolMiddleware(
ctx,
cfg.TaskToolDescriptionGenerator,
cfg.SubAgents,
cfg.WithoutGeneralSubAgent,
cfg.ChatModel,
instruction,
cfg.ToolsConfig,
cfg.MaxIteration,
cfg.Middlewares,
append(handlers, cfg.Handlers...),
cfg.ModelFailoverConfig,
)
if err != nil {
return nil, fmt.Errorf("failed to new task tool: %w", err)
}
handlers = append(handlers, tt)
}
return adk.NewTypedChatModelAgent(ctx, &adk.TypedChatModelAgentConfig[M]{
Name: cfg.Name,
Description: cfg.Description,
Instruction: instruction,
Model: cfg.ChatModel,
ToolsConfig: cfg.ToolsConfig,
MaxIterations: cfg.MaxIteration,
Middlewares: cfg.Middlewares,
Handlers: append(handlers, cfg.Handlers...),
GenModelInput: typedGenModelInput[M],
ModelRetryConfig: cfg.ModelRetryConfig,
ModelFailoverConfig: cfg.ModelFailoverConfig,
OutputKey: cfg.OutputKey,
})
}
// New creates a new Deep agent instance with the provided configuration.
// This function initializes built-in tools, creates a task tool for subagent orchestration,
// and returns a fully configured ChatModelAgent ready for execution.
func New(ctx context.Context, cfg *Config) (adk.ResumableAgent, error) {
return NewTyped(ctx, cfg)
}
func typedGenModelInput[M adk.MessageType](_ context.Context, instruction string, input *adk.TypedAgentInput[M]) ([]M, error) {
var zero M
switch any(zero).(type) {
case *schema.Message:
msgs := make([]*schema.Message, 0, len(input.Messages)+1)
if instruction != "" {
msgs = append(msgs, schema.SystemMessage(instruction))
}
// Type assertion is safe here because M = *schema.Message.
for _, m := range input.Messages {
msgs = append(msgs, any(m).(*schema.Message))
}
result := make([]M, len(msgs))
for i, m := range msgs {
result[i] = any(m).(M)
}
return result, nil
case *schema.AgenticMessage:
msgs := make([]*schema.AgenticMessage, 0, len(input.Messages)+1)
if instruction != "" {
msgs = append(msgs, schema.SystemAgenticMessage(instruction))
}
for _, m := range input.Messages {
msgs = append(msgs, any(m).(*schema.AgenticMessage))
}
result := make([]M, len(msgs))
for i, m := range msgs {
result[i] = any(m).(M)
}
return result, nil
}
panic("unreachable")
}
func buildTypedBuiltinAgentMiddlewares[M adk.MessageType](ctx context.Context, cfg *TypedConfig[M]) ([]adk.TypedChatModelAgentMiddleware[M], error) {
var ms []adk.TypedChatModelAgentMiddleware[M]
if !cfg.WithoutWriteTodos {
t, err := typedNewWriteTodos[M]()
if err != nil {
return nil, err
}
ms = append(ms, t)
}
if cfg.Backend != nil || cfg.Shell != nil || cfg.StreamingShell != nil {
fm, err := filesystem2.NewTyped[M](ctx, &filesystem2.MiddlewareConfig{
Backend: cfg.Backend,
Shell: cfg.Shell,
StreamingShell: cfg.StreamingShell,
})
if err != nil {
return nil, err
}
ms = append(ms, fm)
}
return ms, nil
}
type TODO struct {
Content string `json:"content"`
ActiveForm string `json:"activeForm"`
Status string `json:"status" jsonschema:"enum=pending,enum=in_progress,enum=completed"`
}
type writeTodosArguments struct {
Todos []TODO `json:"todos"`
}
func typedNewWriteTodos[M adk.MessageType]() (adk.TypedChatModelAgentMiddleware[M], error) {
toolDesc := internal.SelectPrompt(internal.I18nPrompts{
English: writeTodosToolDescription,
Chinese: writeTodosToolDescriptionChinese,
})
resultMsg := internal.SelectPrompt(internal.I18nPrompts{
English: "Updated todo list to %s",
Chinese: "已更新待办列表为 %s",
})
t, err := utils.InferTool("write_todos", toolDesc, func(ctx context.Context, input writeTodosArguments) (output string, err error) {
adk.AddSessionValue(ctx, SessionKeyTodos, input.Todos)
todos, err := sonic.MarshalString(input.Todos)
if err != nil {
return "", err
}
return fmt.Sprintf(resultMsg, todos), nil
})
if err != nil {
return nil, err
}
return typedBuildAppendPromptTool[M]("", t), nil
}