|
| 1 | +//! Integration test for `tool_fn` - stateless concurrent tools |
| 2 | +//! |
| 3 | +//! This test verifies that `tool_fn` works correctly for stateless tools |
| 4 | +//! that don't need mutable state. |
| 5 | +
|
| 6 | +use sacp::Component; |
| 7 | +use sacp::ProxyToConductor; |
| 8 | +use sacp::mcp_server::McpServer; |
| 9 | +use sacp_conductor::Conductor; |
| 10 | +use schemars::JsonSchema; |
| 11 | +use serde::{Deserialize, Serialize}; |
| 12 | +use tokio::io::duplex; |
| 13 | +use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; |
| 14 | + |
| 15 | +/// Input for the greet tool |
| 16 | +#[derive(Debug, Serialize, Deserialize, JsonSchema)] |
| 17 | +struct GreetInput { |
| 18 | + name: String, |
| 19 | +} |
| 20 | + |
| 21 | +/// Create a proxy that provides an MCP server with a stateless greet tool |
| 22 | +fn create_greet_proxy() -> Result<sacp::DynComponent, sacp::Error> { |
| 23 | + // Create MCP server with a stateless greet tool using tool_fn |
| 24 | + let mcp_server = McpServer::builder("greet_server".to_string()) |
| 25 | + .instructions("Test MCP server with stateless greet tool") |
| 26 | + .tool_fn( |
| 27 | + "greet", |
| 28 | + "Greet someone by name", |
| 29 | + async |input: GreetInput, _context| Ok(format!("Hello, {}!", input.name)), |
| 30 | + sacp::tool_fn!(), |
| 31 | + ) |
| 32 | + .build(); |
| 33 | + |
| 34 | + // Create proxy component |
| 35 | + Ok(sacp::DynComponent::new(ProxyWithGreetServer { mcp_server })) |
| 36 | +} |
| 37 | + |
| 38 | +struct ProxyWithGreetServer<R: sacp::JrResponder<ProxyToConductor>> { |
| 39 | + mcp_server: McpServer<ProxyToConductor, R>, |
| 40 | +} |
| 41 | + |
| 42 | +impl<R: sacp::JrResponder<ProxyToConductor> + 'static + Send> Component |
| 43 | + for ProxyWithGreetServer<R> |
| 44 | +{ |
| 45 | + async fn serve(self, client: impl Component) -> Result<(), sacp::Error> { |
| 46 | + ProxyToConductor::builder() |
| 47 | + .name("greet-proxy") |
| 48 | + .with_mcp_server(self.mcp_server) |
| 49 | + .serve(client) |
| 50 | + .await |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +/// Elizacp agent component wrapper for testing |
| 55 | +struct ElizacpAgentComponent; |
| 56 | + |
| 57 | +impl Component for ElizacpAgentComponent { |
| 58 | + async fn serve(self, client: impl Component) -> Result<(), sacp::Error> { |
| 59 | + // Create duplex channels for bidirectional communication |
| 60 | + let (elizacp_write, client_read) = duplex(8192); |
| 61 | + let (client_write, elizacp_read) = duplex(8192); |
| 62 | + |
| 63 | + let elizacp_transport = |
| 64 | + sacp::ByteStreams::new(elizacp_write.compat_write(), elizacp_read.compat()); |
| 65 | + |
| 66 | + let client_transport = |
| 67 | + sacp::ByteStreams::new(client_write.compat_write(), client_read.compat()); |
| 68 | + |
| 69 | + // Spawn elizacp in a background task |
| 70 | + tokio::spawn(async move { |
| 71 | + if let Err(e) = elizacp::ElizaAgent::new().serve(elizacp_transport).await { |
| 72 | + tracing::error!("Elizacp error: {}", e); |
| 73 | + } |
| 74 | + }); |
| 75 | + |
| 76 | + // Serve the client with the transport connected to elizacp |
| 77 | + client_transport.serve(client).await |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +#[tokio::test] |
| 82 | +async fn test_tool_fn_greet() -> Result<(), sacp::Error> { |
| 83 | + let result = yopo::prompt( |
| 84 | + Conductor::new( |
| 85 | + "test-conductor".to_string(), |
| 86 | + vec![ |
| 87 | + create_greet_proxy()?, |
| 88 | + sacp::DynComponent::new(ElizacpAgentComponent), |
| 89 | + ], |
| 90 | + Default::default(), |
| 91 | + ), |
| 92 | + r#"Use tool greet_server::greet with {"name": "World"}"#, |
| 93 | + ) |
| 94 | + .await?; |
| 95 | + |
| 96 | + expect_test::expect![[r#" |
| 97 | + "OK: CallToolResult { content: [Annotated { raw: Text(RawTextContent { text: \"\\\"Hello, World!\\\"\", meta: None }), annotations: None }], structured_content: Some(String(\"Hello, World!\")), is_error: Some(false), meta: None }" |
| 98 | + "#]].assert_debug_eq(&result); |
| 99 | + |
| 100 | + Ok(()) |
| 101 | +} |
0 commit comments