use crate::agent::{AgentCallback, AgentConfig, AgentRole};
use crate::audit::AuditLogger;
use crate::error::Result;
use crate::guard::{Guard, GuardManager};
#[cfg(feature = "human-loop")]
use crate::human_loop::{HumanLoopProvider, PermissionService};
use crate::llm::{LlmClient, LlmConfig, OpenAiClient, ResponseFormat};
use crate::memory::checkpointer::Checkpointer;
use crate::memory::snapshot::{SnapshotManager, SnapshotPolicy};
use crate::memory::store::Store;
use crate::prelude::ReactAgent;
use crate::sandbox::SandboxManager;
use crate::tools::permission::PermissionPolicy;
use crate::tools::{Tool, ToolExecutionConfig};
use echo_core::circuit_breaker::CircuitBreakerConfig;
use std::sync::Arc;
pub struct ReactAgentBuilder {
name: String,
model: String,
system_prompt: String,
role: AgentRole,
llm_client: Option<Arc<dyn LlmClient>>,
llm_config: Option<LlmConfig>,
tools: Vec<Box<dyn Tool>>,
enable_builtin_tools: bool,
enable_memory: bool,
enable_task: bool,
enable_human_in_loop: bool,
enable_subagent: bool,
enable_cot: bool,
tool_error_feedback: bool,
tool_execution: ToolExecutionConfig,
max_iterations: usize,
token_limit: usize,
callbacks: Vec<Arc<dyn AgentCallback>>,
store: Option<Arc<dyn Store>>,
checkpointer: Option<Arc<dyn Checkpointer>>,
session_id: Option<String>,
conversation_id: Option<String>,
#[cfg(feature = "human-loop")]
approval_provider: Option<Arc<dyn HumanLoopProvider>>,
#[cfg(feature = "human-loop")]
permission_service: Option<Arc<PermissionService>>,
guards: Vec<Arc<dyn Guard>>,
permission_policy: Option<Arc<dyn PermissionPolicy>>,
audit_logger: Option<Arc<dyn AuditLogger>>,
snapshot_policy: Option<SnapshotPolicy>,
max_snapshots: usize,
response_format: Option<ResponseFormat>,
max_tool_output_tokens: Option<usize>,
circuit_breaker_config: Option<CircuitBreakerConfig>,
sandbox_manager: Option<Arc<SandboxManager>>,
}
impl Default for ReactAgentBuilder {
fn default() -> Self {
Self::new()
}
}
impl ReactAgentBuilder {
pub fn new() -> Self {
Self {
name: "assistant".to_string(),
model: String::new(),
system_prompt: "你是一个有帮助的助手".to_string(),
role: AgentRole::default(),
llm_client: None,
llm_config: None,
tools: Vec::new(),
enable_builtin_tools: false,
enable_memory: false,
enable_task: false,
enable_human_in_loop: false,
enable_subagent: false,
enable_cot: true,
tool_error_feedback: true,
tool_execution: ToolExecutionConfig::default(),
max_iterations: 10,
token_limit: usize::MAX,
callbacks: Vec::new(),
store: None,
checkpointer: None,
session_id: None,
conversation_id: None,
#[cfg(feature = "human-loop")]
approval_provider: None,
#[cfg(feature = "human-loop")]
permission_service: None,
guards: Vec::new(),
permission_policy: None,
audit_logger: None,
snapshot_policy: None,
max_snapshots: 10,
response_format: None,
max_tool_output_tokens: None,
circuit_breaker_config: None,
sandbox_manager: None,
}
}
pub fn simple(model: &str, system_prompt: &str) -> Result<ReactAgent> {
Self::new()
.model(model)
.system_prompt(system_prompt)
.build()
}
pub fn standard(model: &str, name: &str, system_prompt: &str) -> Result<ReactAgent> {
Self::new()
.model(model)
.name(name)
.system_prompt(system_prompt)
.enable_tools()
.build()
}
pub fn full_featured(model: &str, name: &str, system_prompt: &str) -> Result<ReactAgent> {
Self::new()
.model(model)
.name(name)
.system_prompt(system_prompt)
.enable_tools()
.enable_memory()
.enable_planning()
.build()
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = model.into();
self
}
pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = prompt.into();
self
}
pub fn role(mut self, role: AgentRole) -> Self {
self.role = role;
self
}
pub fn llm_client(mut self, client: Arc<dyn LlmClient>) -> Self {
self.model = client.model_name().to_string();
self.llm_client = Some(client);
self
}
pub fn llm_config(mut self, config: LlmConfig) -> Self {
self.model = config.model.clone();
self.llm_config = Some(config);
self
}
pub fn with_openai(mut self, model: &str) -> Result<Self> {
let client = Arc::new(OpenAiClient::from_env(model)?);
self.llm_client = Some(client);
self.model = model.to_string();
Ok(self)
}
pub fn enable_tools(mut self) -> Self {
self.enable_builtin_tools = true;
self
}
pub fn disable_tools(mut self) -> Self {
self.enable_builtin_tools = false;
self
}
pub fn tool(mut self, tool: Box<dyn Tool>) -> Self {
self.tools.push(tool);
self
}
pub fn tools(mut self, tools: Vec<Box<dyn Tool>>) -> Self {
self.tools.extend(tools);
self
}
pub fn enable_memory(mut self) -> Self {
self.enable_memory = true;
self
}
pub fn enable_planning(mut self) -> Self {
self.enable_task = true;
self
}
pub fn enable_human_in_loop(mut self) -> Self {
self.enable_human_in_loop = true;
self
}
pub fn enable_subagent(mut self) -> Self {
self.enable_subagent = true;
self
}
pub fn enable_cot(mut self) -> Self {
self.enable_cot = true;
self
}
pub fn disable_cot(mut self) -> Self {
self.enable_cot = false;
self
}
pub fn output_type<T: schemars::JsonSchema>(mut self) -> Self {
let schema_gen = schemars::r#gen::SchemaGenerator::default();
let root_schema = schema_gen.into_root_schema_for::<T>();
let schema_value = serde_json::to_value(root_schema).unwrap_or_default();
let type_name = std::any::type_name::<T>()
.rsplit("::")
.next()
.unwrap_or("output")
.to_lowercase();
self.response_format = Some(ResponseFormat::json_schema(type_name, schema_value));
self
}
pub fn response_format(mut self, fmt: ResponseFormat) -> Self {
self.response_format = Some(fmt);
self
}
pub fn max_iterations(mut self, max: usize) -> Self {
self.max_iterations = max;
self
}
pub fn tool_error_feedback(mut self, enabled: bool) -> Self {
self.tool_error_feedback = enabled;
self
}
pub fn tool_execution(mut self, config: ToolExecutionConfig) -> Self {
self.tool_execution = config;
self
}
pub fn token_limit(mut self, limit: usize) -> Self {
self.token_limit = limit;
self
}
pub fn max_tool_output_tokens(mut self, max: usize) -> Self {
self.max_tool_output_tokens = Some(max);
self
}
pub fn callback(mut self, callback: Arc<dyn AgentCallback>) -> Self {
self.callbacks.push(callback);
self
}
pub fn store(mut self, store: Arc<dyn Store>) -> Self {
self.store = Some(store);
self
}
pub fn with_memory_tools(mut self, store: Arc<dyn Store>) -> Self {
self.store = Some(store);
self.enable_memory = true;
self
}
pub fn checkpointer(
mut self,
checkpointer: Arc<dyn Checkpointer>,
session_id: impl Into<String>,
) -> Self {
self.checkpointer = Some(checkpointer);
self.session_id = Some(session_id.into());
self
}
pub fn checkpointer_only(mut self, checkpointer: Arc<dyn Checkpointer>) -> Self {
self.checkpointer = Some(checkpointer);
self
}
pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
pub fn conversation_id(mut self, conversation_id: impl Into<String>) -> Self {
self.conversation_id = Some(conversation_id.into());
self
}
#[cfg(feature = "human-loop")]
pub fn approval_provider(mut self, provider: Arc<dyn HumanLoopProvider>) -> Self {
self.approval_provider = Some(provider);
self
}
#[cfg(feature = "human-loop")]
pub fn permission_service(mut self, service: Arc<PermissionService>) -> Self {
self.permission_service = Some(service);
self
}
pub fn guard(mut self, guard: Arc<dyn Guard>) -> Self {
self.guards.push(guard);
self
}
pub fn guards(mut self, guards: Vec<Arc<dyn Guard>>) -> Self {
self.guards.extend(guards);
self
}
#[cfg(feature = "content-guard")]
pub fn with_content_guard(mut self, mode: echo_core::guard::content::ContentGuardMode) -> Self {
let guard = echo_core::guard::content::ContentGuard::new(mode);
self.guards.push(Arc::new(guard));
self
}
pub fn permission_policy(mut self, policy: Arc<dyn PermissionPolicy>) -> Self {
self.permission_policy = Some(policy);
self
}
pub fn audit_logger(mut self, logger: Arc<dyn AuditLogger>) -> Self {
self.audit_logger = Some(logger);
self
}
pub fn snapshot_policy(mut self, policy: SnapshotPolicy) -> Self {
self.snapshot_policy = Some(policy);
self
}
pub fn max_snapshots(mut self, max: usize) -> Self {
self.max_snapshots = max;
self
}
pub fn with_circuit_breaker(mut self, config: CircuitBreakerConfig) -> Self {
self.circuit_breaker_config = Some(config);
self
}
pub fn sandbox_manager(mut self, manager: Arc<SandboxManager>) -> Self {
self.sandbox_manager = Some(manager);
self
}
pub fn build(self) -> Result<ReactAgent> {
if self.model.trim().is_empty() {
return Err(crate::error::ConfigError::MissingConfig(
"model".to_string(),
"模型名称不能为空".to_string(),
)
.into());
}
if self.max_iterations == 0 {
return Err(crate::error::ConfigError::ConfigFileError(
"max_iterations 必须大于 0".to_string(),
)
.into());
}
if self.enable_subagent && !self.enable_builtin_tools {
return Err(crate::error::ConfigError::ConfigFileError(
"启用子 Agent 调度 (enable_subagent) 需要同时启用工具调用 (enable_builtin_tools)"
.to_string(),
)
.into());
}
let mut config = AgentConfig::new(&self.model, &self.name, &self.system_prompt)
.role(self.role)
.enable_tool(self.enable_builtin_tools)
.enable_memory(self.enable_memory)
.enable_task(self.enable_task)
.enable_human_in_loop(self.enable_human_in_loop)
.enable_subagent(self.enable_subagent)
.enable_cot(self.enable_cot)
.tool_error_feedback(self.tool_error_feedback)
.tool_execution(self.tool_execution)
.max_iterations(self.max_iterations)
.token_limit(self.token_limit);
if let Some(fmt) = self.response_format {
config = config.response_format(fmt);
}
if let Some(max) = self.max_tool_output_tokens {
config = config.max_tool_output_tokens(max);
}
for callback in self.callbacks {
config = config.with_callback(callback);
}
if let Some(session_id) = &self.session_id {
config = config.session_id(session_id);
}
if let Some(conversation_id) = &self.conversation_id {
config = config.conversation_id(conversation_id);
}
let has_external_store = self.store.is_some();
if has_external_store {
config = config.enable_memory(false);
}
let mut agent = crate::agent::react::ReactAgent::new(config);
if let Some(llm_client) = self.llm_client {
agent.set_llm_client(llm_client);
}
if let Some(llm_config) = self.llm_config {
agent.set_llm_config(llm_config);
}
for tool in self.tools {
agent.add_tool(tool);
}
if let Some(store) = self.store {
agent.set_memory_store(store);
}
if let (Some(checkpointer), Some(session_id)) = (self.checkpointer, self.session_id) {
agent.set_checkpointer(checkpointer, session_id);
}
#[cfg(feature = "human-loop")]
if let Some(provider) = self.approval_provider {
agent.set_approval_provider(provider);
}
#[cfg(feature = "human-loop")]
if let Some(service) = self.permission_service {
agent.set_permission_service(service);
}
if !self.guards.is_empty() {
agent.set_guard_manager(GuardManager::from_guards(self.guards));
}
if let Some(policy) = self.permission_policy {
agent.set_permission_policy(policy);
}
if let Some(logger) = self.audit_logger {
agent.set_audit_logger(logger);
}
if let Some(policy) = self.snapshot_policy {
agent.set_snapshot_manager(SnapshotManager::new(policy, self.max_snapshots));
}
if let Some(cb_config) = self.circuit_breaker_config {
agent.set_circuit_breaker(cb_config);
}
if let Some(manager) = self.sandbox_manager {
agent.set_sandbox_manager(manager);
}
Ok(agent)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::MockLlmClient;
use std::sync::Arc;
#[test]
fn test_builder_basic() {
let builder = ReactAgentBuilder::new()
.name("test-agent")
.model("qwen3-max")
.system_prompt("测试");
assert_eq!(builder.name, "test-agent");
assert_eq!(builder.model, "qwen3-max");
assert_eq!(builder.system_prompt, "测试");
}
#[test]
fn test_builder_chaining() {
let builder = ReactAgentBuilder::new()
.model("qwen3-max")
.enable_tools()
.enable_memory()
.max_iterations(20);
assert!(builder.enable_builtin_tools);
assert!(builder.enable_memory);
assert_eq!(builder.max_iterations, 20);
}
#[test]
fn test_react_agent_builder() {
let builder = ReactAgentBuilder::new()
.model("qwen3-max")
.system_prompt("测试")
.enable_tools();
assert!(builder.enable_builtin_tools);
}
#[test]
fn test_builder_llm_config_syncs_runtime_model_name() {
let agent = ReactAgentBuilder::new()
.llm_config(LlmConfig::openai("sk-demo", "gpt-4o"))
.system_prompt("测试")
.build()
.unwrap();
assert_eq!(agent.config().get_model_name(), "gpt-4o");
assert_eq!(
agent.llm_config().map(|cfg| cfg.model.as_str()),
Some("gpt-4o")
);
}
#[test]
fn test_builder_llm_client_syncs_runtime_model_name() {
let agent = ReactAgentBuilder::new()
.llm_client(Arc::new(
MockLlmClient::new().with_model_name("mock-topology"),
))
.system_prompt("测试")
.build()
.unwrap();
assert_eq!(agent.config().get_model_name(), "mock-topology");
}
#[test]
fn test_builder_tool_execution_config_is_applied() {
let agent = ReactAgentBuilder::new()
.model("qwen3-max")
.tool_execution(ToolExecutionConfig {
timeout_ms: 120_000,
..ToolExecutionConfig::default()
})
.build()
.unwrap();
assert_eq!(agent.config().get_tool_execution().timeout_ms, 120_000);
}
}