141 lines
5.4 KiB
Rust
141 lines
5.4 KiB
Rust
//! 端到端集成:Agent + AnthropicProvider + mock transport 跑完整工具回合。
|
||
//! End-to-end integration: Agent + AnthropicProvider + mock transport running
|
||
//! a full tool-call turn.
|
||
|
||
mod common;
|
||
|
||
use focus_core::event::VecSink;
|
||
use focus_core::model::*;
|
||
use focus_core::tool::{Tool, ToolEffects, ToolRegistry, ToolResult, ToolUpdateSink};
|
||
use focus_core::{Agent, AgentConfig, CoreError};
|
||
use focus_json::JsonValue;
|
||
use focus_providers::anthropic::AnthropicProvider;
|
||
use focus_providers::config::ProviderConfig;
|
||
use std::sync::Arc;
|
||
|
||
/// 记录调用参数的 echo 工具。
|
||
/// An echo tool that records its arguments.
|
||
#[derive(Debug, Default)]
|
||
struct EchoTool {
|
||
calls: Arc<std::sync::Mutex<Vec<JsonValue>>>,
|
||
}
|
||
|
||
impl Clone for EchoTool {
|
||
fn clone(&self) -> Self {
|
||
Self {
|
||
calls: self.calls.clone(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Tool for EchoTool {
|
||
fn name(&self) -> &str {
|
||
"echo"
|
||
}
|
||
fn description(&self) -> &str {
|
||
"echoes the arguments back"
|
||
}
|
||
fn parameters(&self) -> JsonValue {
|
||
let mut o = JsonValue::obj();
|
||
o.insert("type", "object".into()).ok();
|
||
let mut props = JsonValue::obj();
|
||
props.insert("text", JsonValue::Str("the text".into())).ok();
|
||
o.insert("properties", props).ok();
|
||
o
|
||
}
|
||
fn effects(&self) -> ToolEffects {
|
||
ToolEffects::READ
|
||
}
|
||
fn execute(
|
||
&self,
|
||
_id: &str,
|
||
args: &JsonValue,
|
||
_u: Option<&ToolUpdateSink>,
|
||
) -> Result<ToolResult, CoreError> {
|
||
self.calls.lock().unwrap().push(args.clone());
|
||
Ok(ToolResult::text(format!(
|
||
"echo: {}",
|
||
args.get_str("text").unwrap_or("")
|
||
)))
|
||
}
|
||
}
|
||
|
||
/// 回合 1:模型调用 echo 工具;回合 2:模型给出最终文本。
|
||
/// Turn 1: the model calls the echo tool; turn 2: the model gives final text.
|
||
#[test]
|
||
fn agent_runs_full_tool_turn_against_anthropic() {
|
||
let mut mock = common::MockTransport::new();
|
||
// 回合 1:tool_use → 回合 2:文本回答。
|
||
// Turn 1: tool_use → turn 2: text answer.
|
||
mock.push_body(
|
||
"event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":20}}}\n\n\
|
||
event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_1\",\"name\":\"echo\",\"input\":{}}}\n\n\
|
||
event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"text\\\":\\\"world\\\"}\"}}\n\n\
|
||
event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n\
|
||
event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"output_tokens\":8}}\n\n\
|
||
event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
|
||
);
|
||
mock.push_body(
|
||
"event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":30}}}\n\n\
|
||
event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n\
|
||
event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"all done\"}}\n\n\
|
||
event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n\
|
||
event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":4}}\n\n\
|
||
event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
|
||
);
|
||
|
||
let provider =
|
||
AnthropicProvider::with_transport(ProviderConfig::new("sk-test"), Arc::new(mock.clone()));
|
||
let config = AgentConfig {
|
||
model: "claude-sonnet-4".into(),
|
||
system_prompt: "You are helpful.".into(),
|
||
max_tokens: Some(1024),
|
||
..Default::default()
|
||
};
|
||
let echo = EchoTool::default();
|
||
let calls = echo.calls.clone();
|
||
let tools = ToolRegistry::with(Box::new(echo));
|
||
let mut agent = Agent::new(config, Box::new(provider), tools);
|
||
let mut sink = VecSink::new();
|
||
|
||
agent
|
||
.prompt("please echo world", &mut sink)
|
||
.expect("run failed");
|
||
|
||
// 两次回合、一次工具执行。
|
||
// Two turns, one tool execution.
|
||
assert_eq!(sink.count("turn_end"), 2);
|
||
assert_eq!(sink.count("tool_execution_end"), 1);
|
||
|
||
let msgs = agent.messages();
|
||
assert_eq!(msgs.len(), 4);
|
||
assert!(matches!(msgs[0], Message::User(_)));
|
||
assert!(matches!(
|
||
&msgs[1],
|
||
Message::Assistant(a) if a.content.iter().any(|c| c.as_tool_call().is_some())
|
||
));
|
||
assert!(matches!(
|
||
&msgs[2],
|
||
Message::ToolResult(tr) if tr.tool_name == "echo" && !tr.is_error
|
||
));
|
||
let final_text = match &msgs[3] {
|
||
Message::Assistant(a) => a
|
||
.content
|
||
.iter()
|
||
.find_map(|c| c.as_text())
|
||
.map(|t| t.text.clone()),
|
||
_ => None,
|
||
};
|
||
assert_eq!(final_text.as_deref(), Some("all done"));
|
||
|
||
// 工具确实收到了模型传入的参数。
|
||
// The tool actually received the model's arguments.
|
||
let recorded = calls.lock().unwrap();
|
||
assert_eq!(recorded.len(), 1);
|
||
assert_eq!(recorded[0].get_str("text"), Some("world"));
|
||
|
||
// 两个回合都发出了 HTTP 请求。
|
||
// Both turns issued HTTP requests.
|
||
assert_eq!(mock.request_count(), 2);
|
||
}
|