use async_trait::async_trait;
use pe_core::{Message, ToolSchema};
#[async_trait]
pub trait ToolSelector: Send + Sync + 'static {
async fn select(&self, available: &[ToolSchema], messages: &[Message]) -> Vec<String>;
}
#[derive(Debug, Clone, Default)]
pub struct AllToolsSelector;
#[async_trait]
impl ToolSelector for AllToolsSelector {
async fn select(&self, available: &[ToolSchema], _messages: &[Message]) -> Vec<String> {
available.iter().map(|t| t.name.clone()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use pe_core::Message;
use serde_json::json;
fn make_tool(name: &str) -> ToolSchema {
ToolSchema {
name: name.into(),
description: format!("{name} tool"),
parameters: json!({}),
strict: false,
}
}
#[tokio::test]
async fn all_tools_selector_returns_every_tool_name() {
let selector = AllToolsSelector;
let tools = vec![
make_tool("search"),
make_tool("calculator"),
make_tool("email"),
];
let selected = selector.select(&tools, &[]).await;
assert_eq!(selected, vec!["search", "calculator", "email"]);
}
#[tokio::test]
async fn all_tools_selector_empty_input_returns_empty() {
let selector = AllToolsSelector;
let selected = selector.select(&[], &[]).await;
assert!(selected.is_empty());
}
#[tokio::test]
async fn all_tools_selector_ignores_messages() {
let selector = AllToolsSelector;
let tools = vec![make_tool("read_file")];
let messages = vec![
Message::human("Please read my file"),
Message::ai("I'll use the read_file tool"),
];
let selected = selector.select(&tools, &messages).await;
assert_eq!(selected, vec!["read_file"]);
}
#[tokio::test]
async fn custom_selector_filters_by_conversation() {
struct MentionedToolsSelector;
#[async_trait]
impl ToolSelector for MentionedToolsSelector {
async fn select(&self, available: &[ToolSchema], messages: &[Message]) -> Vec<String> {
let text: String = messages
.iter()
.filter_map(|m| match m {
Message::Human(h) => h.content.as_text().map(|s| s.to_owned()),
Message::Ai(a) => a.content.as_text().map(|s| s.to_owned()),
Message::System(s) => Some(s.content.clone()),
Message::Tool(t) => Some(t.content.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join(" ");
available
.iter()
.filter(|t| text.contains(&t.name))
.map(|t| t.name.clone())
.collect()
}
}
let selector = MentionedToolsSelector;
let tools = vec![
make_tool("search"),
make_tool("calculator"),
make_tool("email"),
];
let messages = vec![Message::human("I need to search for something")];
let selected = selector.select(&tools, &messages).await;
assert_eq!(selected, vec!["search"]);
}
#[tokio::test]
async fn custom_selector_can_return_empty() {
struct NoToolsSelector;
#[async_trait]
impl ToolSelector for NoToolsSelector {
async fn select(
&self,
_available: &[ToolSchema],
_messages: &[Message],
) -> Vec<String> {
vec![]
}
}
let selector = NoToolsSelector;
let tools = vec![make_tool("search"), make_tool("calculator")];
let selected = selector.select(&tools, &[]).await;
assert!(selected.is_empty());
}
}