focus/crates/focus-providers/tests/agent_integration.rs

141 lines
5.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 端到端集成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();
// 回合 1tool_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);
}