echo_agent 0.1.4

Production-grade AI Agent framework for Rust — ReAct engine, multi-agent, memory, streaming, MCP, IM channels, workflows
Documentation
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

use echo_agent::human_loop::{HumanLoopEvent, HumanLoopManager};
use echo_agent::prelude::*;
use echo_agent::testing::MockLlmClient;
use echo_agent::tool;

#[tool(name = "add", description = "两数相加")]
async fn add(a: f64, b: f64) -> Result<ToolResult> {
    Ok(ToolResult::success(format!("{} + {} = {}", a, b, a + b)))
}

#[tool(name = "subtract", description = "两数相减")]
async fn subtract(a: f64, b: f64) -> Result<ToolResult> {
    Ok(ToolResult::success(format!("{} - {} = {}", a, b, a - b)))
}

#[tool(name = "multiply", description = "两数相乘")]
async fn multiply(a: f64, b: f64) -> Result<ToolResult> {
    Ok(ToolResult::success(format!("{} * {} = {}", a, b, a * b)))
}

#[tool(name = "divide", description = "两数相除")]
async fn divide(a: f64, b: f64) -> Result<ToolResult> {
    if b == 0.0 {
        return Ok(ToolResult::error("除数不能为 0".to_string()));
    }
    Ok(ToolResult::success(format!("{} / {} = {}", a, b, a / b)))
}

fn math_tools() -> Vec<Box<dyn Tool>> {
    vec![
        Box::new(AddTool),
        Box::new(SubtractTool),
        Box::new(MultiplyTool),
        Box::new(DivideTool),
    ]
}

fn spawn_human_loop_worker(
    manager: Arc<HumanLoopManager>,
    input_count: Arc<AtomicUsize>,
) -> tokio::task::JoinHandle<()> {
    tokio::spawn(async move {
        while let Some(event) = manager.recv_event().await {
            match event {
                HumanLoopEvent::ApprovalRequest {
                    tool_name,
                    prompt,
                    responder,
                    ..
                } => {
                    println!("🔔 审批请求: {tool_name} -> {prompt}");
                    responder.approve();
                }
                HumanLoopEvent::InputRequest { prompt, responder } => {
                    input_count.fetch_add(1, Ordering::Relaxed);
                    println!("💬 Human-in-the-loop: {prompt}");
                    responder.respond("用户补充:今天下雨,请按雨天折扣计算。".to_string());
                }
            }
        }
    })
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        .init();

    println!("🧪 demo04 - SubAgent 编排 + Human-in-the-Loop 演示\n");

    let human_loop = Arc::new(HumanLoopManager::new());
    let input_count = Arc::new(AtomicUsize::new(0));
    let worker = spawn_human_loop_worker(human_loop.clone(), input_count.clone());

    let weather_llm = Arc::new(
        MockLlmClient::new()
            .with_model_name("weather-agent-mock")
            .then_tool_call(
                "weather-hitl-1",
                "human_in_loop",
                r#"{
                    "reasoning":"用户没有说明购物当天是否下雨,无法决定是否触发雨天 85 折。",
                    "approval_type":"LLM",
                    "tool":"weather-context"
                }"#,
            )
            .then_tool_call(
                "weather-final-1",
                "final_answer",
                r#"{"answer":"已通过 human_in_loop 向用户确认:今天下雨,因此后续计算需要叠加雨天 85 折。"}"#,
            ),
    );

    let math_llm = Arc::new(
        MockLlmClient::new()
            .with_model_name("math-agent-mock")
            .then_tool_call("math-1", "multiply", r#"{"a":10,"b":15}"#)
            .then_tool_call("math-2", "multiply", r#"{"a":16,"b":8}"#)
            .then_tool_call("math-3", "multiply", r#"{"a":2,"b":98}"#)
            .then_tool_call("math-4", "multiply", r#"{"a":500,"b":0.8}"#)
            .then_tool_call("math-5", "add", r#"{"a":150,"b":128}"#)
            .then_tool_call("math-6", "add", r#"{"a":278,"b":196}"#)
            .then_tool_call("math-7", "add", r#"{"a":474,"b":400}"#)
            .then_tool_call("math-8", "multiply", r#"{"a":874,"b":0.9}"#)
            .then_tool_call("math-9", "multiply", r#"{"a":786.6,"b":0.85}"#)
            .then_tool_call("math-10", "subtract", r#"{"a":1000,"b":668.61}"#)
            .then_tool_call(
                "math-final-1",
                "final_answer",
                r#"{"answer":"计算结果:本子 150 元,笔 128 元,玩具 196 元,衣服满 500 打八折后为 400 元;小计 874 元,满 800 打九折后为 786.6 元;已确认下雨,再打 85 折,实付 668.61 元,最终剩余 331.39 元。"}"#,
            ),
    );

    let orchestrator_llm = Arc::new(
        MockLlmClient::new()
            .with_model_name("orchestrator-mock")
            .then_tool_call(
                "main-1",
                "agent_tool",
                r#"{
                    "agent_name":"weather-agent",
                    "task":"先向用户确认购物当天是否下雨,并只返回与折扣判断相关的结论。"
                }"#,
            )
            .then_tool_call(
                "main-2",
                "agent_tool",
                r#"{
                    "agent_name":"math-agent",
                    "task":"预算是 1000 元。已确认今天下雨。请按先单品折扣、再满减折扣、最后雨天折扣的顺序计算剩余金额,并返回清晰的计算结论。"
                }"#,
            )
            .then_tool_call(
                "main-final-1",
                "final_answer",
                r#"{"answer":"weather-agent 先通过 human_in_loop 完成了天气澄清,确认今天下雨;math-agent 随后完成折扣计算,最终实付 668.61 元,还剩 331.39 元。"}"#,
            ),
    );

    let weather_agent = ReactAgentBuilder::new()
        .name("weather-agent")
        .model("weather-agent-mock")
        .system_prompt(
            "你负责识别是否缺少天气上下文。若用户未说明是否下雨,必须先调用 human_in_loop 获取补充信息,再给出简洁结论。",
        )
        .enable_tools()
        .enable_human_in_loop()
        .approval_provider(human_loop.clone() as Arc<dyn echo_agent::human_loop::HumanLoopProvider>)
        .llm_client(weather_llm)
        .max_iterations(6)
        .build()?;

    let mut math_agent = ReactAgentBuilder::new()
        .name("math-agent")
        .model("math-agent-mock")
        .system_prompt("你是折扣计算专家,只使用数学工具逐步计算,再给出最终金额。")
        .enable_tools()
        .llm_client(math_llm)
        .max_iterations(16)
        .build()?;
    math_agent.add_tools(math_tools());

    let mut main_agent = ReactAgentBuilder::new()
        .model("orchestrator-mock")
        .name("main_agent")
        .system_prompt(
            "你是主编排 Agent。遇到缺失上下文的专业问题先分派给对应 subagent,再汇总结果,不要自己直接计算。",
        )
        .role(AgentRole::Orchestrator)
        .enable_subagent()
        .llm_client(orchestrator_llm)
        .max_iterations(8)
        .enable_tools()
        .build()?;

    main_agent.register_agent(Box::new(weather_agent));
    main_agent.register_agent(Box::new(math_agent));

    let result = main_agent
        .execute(
            "我有1000元,要买 10 个 15 元的本子、16 个 8 元的笔、2 个 98 元的玩具和一件 500 元的衣服。单品满 500 打八折,总价满 800 打九折;如果当天下雨,总价再打 85 折。请问我还剩多少?",
        )
        .await?;

    if result.trim().is_empty() {
        return Err(echo_agent::error::ReactError::Other(
            "demo04 验收失败:SubAgent 编排示例返回空结果".to_string(),
        ));
    }
    if input_count.load(Ordering::Relaxed) == 0 {
        return Err(echo_agent::error::ReactError::Other(
            "demo04 验收失败:未触发 human_in_loop 输入请求".to_string(),
        ));
    }
    if !result.contains("331.39") {
        return Err(echo_agent::error::ReactError::Other(
            "demo04 验收失败:最终结果未包含预期剩余金额 331.39".to_string(),
        ));
    }

    println!("\n✅ 最终结果:\n{}", result);

    worker.abort();
    Ok(())
}