cascade_agent/tools/
knowledge_tool.rs1use std::sync::Arc;
4
5use async_trait::async_trait;
6use serde_json::{json, Value};
7
8use super::{Tool, ToolResult};
9
10#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
19pub struct KnowledgeHit {
20 pub text: String,
21 pub score: f32,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub metadata: Option<serde_json::Map<String, Value>>,
24}
25
26#[async_trait]
28pub trait KnowledgeProvider: Send + Sync {
29 async fn query(
31 &self,
32 query: &str,
33 collection: &str,
34 limit: usize,
35 ) -> Result<Vec<KnowledgeHit>, String>;
36}
37
38pub struct KnowledgeQueryTool {
49 provider: Option<Arc<dyn KnowledgeProvider>>,
51 default_collection: String,
52 default_limit: usize,
53}
54
55impl KnowledgeQueryTool {
56 pub fn new(provider: Arc<dyn KnowledgeProvider>) -> Self {
58 Self {
59 provider: Some(provider),
60 default_collection: "general".to_owned(),
61 default_limit: 5,
62 }
63 }
64
65 pub fn with_defaults(
67 provider: Arc<dyn KnowledgeProvider>,
68 default_collection: String,
69 default_limit: usize,
70 ) -> Self {
71 Self {
72 provider: Some(provider),
73 default_collection,
74 default_limit,
75 }
76 }
77
78 pub fn disabled() -> Self {
80 Self {
81 provider: None,
82 default_collection: "general".to_owned(),
83 default_limit: 5,
84 }
85 }
86}
87
88#[async_trait]
89impl Tool for KnowledgeQueryTool {
90 fn name(&self) -> &str {
91 "knowledge_query"
92 }
93
94 fn description(&self) -> &str {
95 "Query the local knowledge base for relevant text snippets. Returns passages ranked by semantic similarity."
96 }
97
98 fn parameters_schema(&self) -> Value {
99 json!({
100 "type": "object",
101 "properties": {
102 "query": {
103 "type": "string",
104 "description": "The query text to search for."
105 },
106 "collection": {
107 "type": "string",
108 "description": "The knowledge collection to search.",
109 "default": self.default_collection
110 },
111 "limit": {
112 "type": "integer",
113 "description": "Maximum number of results to return.",
114 "default": self.default_limit
115 }
116 },
117 "required": ["query"]
118 })
119 }
120
121 async fn execute(&self, args: Value) -> ToolResult {
122 let query = match args.get("query").and_then(|v| v.as_str()) {
123 Some(q) => q,
124 None => return ToolResult::err("Missing required parameter 'query'"),
125 };
126
127 let collection = args
128 .get("collection")
129 .and_then(|v| v.as_str())
130 .unwrap_or(&self.default_collection);
131
132 let limit = args
133 .get("limit")
134 .and_then(|v| v.as_u64())
135 .unwrap_or(self.default_limit as u64) as usize;
136
137 let provider = match &self.provider {
138 Some(p) => p,
139 None => {
140 return ToolResult::err(
141 "Knowledge base is not configured. Set up the knowledge module first.",
142 );
143 }
144 };
145
146 match provider.query(query, collection, limit).await {
147 Ok(hits) => {
148 let results: Vec<Value> = hits
149 .iter()
150 .map(|h| {
151 let mut obj = json!({
152 "text": h.text,
153 "score": h.score,
154 });
155 if let Some(ref meta) = h.metadata {
156 obj.as_object_mut()
157 .unwrap()
158 .insert("metadata".into(), Value::Object(meta.clone()));
159 }
160 obj
161 })
162 .collect();
163
164 ToolResult::ok(json!({
165 "query": query,
166 "collection": collection,
167 "count": results.len(),
168 "results": results,
169 }))
170 }
171 Err(e) => ToolResult::err(format!("Knowledge query failed: {}", e)),
172 }
173 }
174}
175
176#[cfg(test)]
181struct MockKnowledgeProvider;
182
183#[cfg(test)]
184#[async_trait]
185impl KnowledgeProvider for MockKnowledgeProvider {
186 async fn query(
187 &self,
188 query: &str,
189 _collection: &str,
190 limit: usize,
191 ) -> Result<Vec<KnowledgeHit>, String> {
192 Ok(vec![KnowledgeHit {
193 text: format!("Mock result for: {}", query),
194 score: 0.99,
195 metadata: None,
196 }]
197 .into_iter()
198 .cycle()
199 .take(limit)
200 .collect())
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[tokio::test]
209 async fn knowledge_query_works() {
210 let provider = Arc::new(MockKnowledgeProvider);
211 let tool = KnowledgeQueryTool::new(provider);
212 let result = tool.execute(json!({"query": "rust async"})).await;
213 assert_eq!(result.status, super::super::ToolStatus::Success);
214 let data = result.data.as_object().unwrap();
215 assert_eq!(data["count"].as_u64().unwrap(), 5);
216 assert!(data["results"].as_array().unwrap()[0]["text"]
217 .as_str()
218 .unwrap()
219 .contains("Mock result for: rust async"));
220 }
221
222 #[tokio::test]
223 async fn knowledge_query_disabled() {
224 let tool = KnowledgeQueryTool::disabled();
225 let result = tool.execute(json!({"query": "test"})).await;
226 assert_eq!(result.status, super::super::ToolStatus::Error);
227 assert!(result.error.unwrap().contains("not configured"));
228 }
229
230 #[tokio::test]
231 async fn knowledge_query_custom_params() {
232 let provider = Arc::new(MockKnowledgeProvider);
233 let tool = KnowledgeQueryTool::with_defaults(provider, "custom".to_owned(), 3);
234 let result = tool
235 .execute(json!({
236 "query": "test",
237 "collection": "special",
238 "limit": 2
239 }))
240 .await;
241 assert_eq!(result.status, super::super::ToolStatus::Success);
242 assert_eq!(result.data["count"].as_u64().unwrap(), 2);
243 }
244}