use agy_bridge::{AgyBridge, config::AgentConfig, prelude::*, tools::ToolRegistry};
use serde::Serialize;
#[derive(Serialize)]
struct Match {
path: String,
}
#[derive(Serialize)]
struct SearchResult {
matches: Vec<Match>,
query: String,
}
#[llm_tool(
prompt_file = "examples/getting_started/tools/search_files.tmpl.md",
response_file = "examples/getting_started/tools/search_results.tmpl.md",
params(pattern = "*", directory = "/project")
)]
fn search_files(
pattern: &str,
directory: &str,
) -> Result<SearchResult, String> {
let fake_matches = vec![
Match {
path: format!("{directory}/README.md"),
},
Match {
path: format!("{directory}/src/main.rs"),
},
Match {
path: format!("{directory}/docs/{pattern}.md"),
},
];
Ok(SearchResult {
matches: fake_matches,
query: pattern.to_string(),
})
}
#[tokio::main]
async fn main() -> Result<(), agy_bridge::error::Error> {
agy_bridge::load_dotenv();
let bridge = AgyBridge::builder().build()?;
let mut registry = ToolRegistry::new();
registry.register(SearchFiles);
let config = AgentConfig::builder().build();
let agent = bridge.agent(config).tools(registry).await?;
let prompt = "Find all markdown files in /project";
println!(" User: {prompt}");
let response_text = agent.chat(prompt).await?.text().await?;
println!(" Agent: {response_text}");
agent.shutdown().await?;
Ok(())
}
#[cfg(test)]
mod tests {
use llm_tool::{RustTool, ToolContext};
use super::*;
#[test]
fn test_search_files_description() {
let desc = <SearchFiles as RustTool>::DESCRIPTION;
assert!(desc.contains("Search for files matching a pattern in a directory."));
}
#[tokio::test]
async fn test_search_files_execution() {
let tool = SearchFiles;
let ctx = ToolContext::new(None);
let output = tool
.call(
SearchFilesParams {
pattern: "main.rs".to_string(),
directory: "/app".to_string(),
},
&ctx,
)
.await
.unwrap();
let content = output.content();
assert!(content.contains("Results for \"main.rs\":"));
assert!(content.contains("- /app/src/main.rs"));
}
#[tokio::test]
async fn test_search_files_exact_output() {
let tool = SearchFiles;
let ctx = ToolContext::new(None);
let output = tool
.call(
SearchFilesParams {
pattern: "test.rs".to_string(),
directory: "/dir".to_string(),
},
&ctx,
)
.await
.unwrap();
let content = output.content();
let expected = "\nResults for \"test.rs\":\n- /dir/README.md\n- /dir/src/main.rs\n- /dir/docs/test.rs.md\n";
assert_eq!(content, expected);
}
#[tokio::test]
async fn test_search_files_stress_edge_cases() {
let tool = SearchFiles;
let ctx = ToolContext::new(None);
let injection_pattern = "{{ 7 * 7 }} {% for x in y %} <script>";
let output = tool
.call(
SearchFilesParams {
pattern: injection_pattern.to_string(),
directory: "/sec".to_string(),
},
&ctx,
)
.await
.unwrap();
let content = output.content();
assert!(
content.contains(injection_pattern),
"Should treat pattern as literal string without double evaluation"
);
let unicode_pattern = "🦀_test_\"quotes\"_\n_newline";
let output = tool
.call(
SearchFilesParams {
pattern: unicode_pattern.to_string(),
directory: "/🦀".to_string(),
},
&ctx,
)
.await
.unwrap();
let content = output.content();
assert!(content.contains("🦀_test_\"quotes\"_\n_newline"));
assert!(content.contains("- /🦀/README.md"));
let output = tool
.call(
SearchFilesParams {
pattern: "".to_string(),
directory: "".to_string(),
},
&ctx,
)
.await
.unwrap();
let content = output.content();
assert!(content.contains("Results for \"\":"));
assert!(content.contains("- /README.md"));
}
}