synaptic_tools/
wikipedia.rs1use async_trait::async_trait;
7use serde_json::{json, Value};
8use synaptic_core::{SynapticError, Tool};
9
10pub struct WikipediaTool {
24 client: reqwest::Client,
25 language: String,
27 max_results: usize,
29}
30
31impl Default for WikipediaTool {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl WikipediaTool {
38 pub fn new() -> Self {
39 Self {
40 client: reqwest::Client::new(),
41 language: "en".to_string(),
42 max_results: 3,
43 }
44 }
45
46 pub fn with_language(mut self, language: impl Into<String>) -> Self {
47 self.language = language.into();
48 self
49 }
50
51 pub fn with_max_results(mut self, max_results: usize) -> Self {
52 self.max_results = max_results;
53 self
54 }
55
56 async fn search_titles(&self, query: &str) -> Result<Vec<String>, SynapticError> {
57 let encoded_query = urlencoding::encode(query);
58 let limit = self.max_results;
59 let url = format!(
60 "https://{lang}.wikipedia.org/w/api.php?action=query&list=search&srsearch={encoded_query}&srlimit={limit}&format=json&utf8=1",
61 lang = self.language,
62 );
63
64 let response = self
65 .client
66 .get(&url)
67 .header(
68 "User-Agent",
69 "synaptic-agent/0.2 (https://github.com/dnw3/synaptic)",
70 )
71 .send()
72 .await
73 .map_err(|e| SynapticError::Tool(format!("Wikipedia search failed: {e}")))?;
74
75 let status = response.status();
76 if !status.is_success() {
77 return Err(SynapticError::Tool(format!(
78 "Wikipedia API error: HTTP {}",
79 status.as_u16()
80 )));
81 }
82
83 let body: Value = response
84 .json()
85 .await
86 .map_err(|e| SynapticError::Tool(format!("Wikipedia parse error: {e}")))?;
87
88 let titles = body["query"]["search"]
89 .as_array()
90 .unwrap_or(&vec![])
91 .iter()
92 .filter_map(|r| r["title"].as_str().map(|s| s.to_string()))
93 .collect();
94
95 Ok(titles)
96 }
97
98 async fn get_summary(&self, title: &str) -> Result<Option<Value>, SynapticError> {
99 let encoded = urlencoding::encode(title);
100 let url = format!(
101 "https://{lang}.wikipedia.org/api/rest_v1/page/summary/{title}",
102 lang = self.language,
103 title = encoded,
104 );
105
106 let response = self
107 .client
108 .get(&url)
109 .header(
110 "User-Agent",
111 "synaptic-agent/0.2 (https://github.com/dnw3/synaptic)",
112 )
113 .send()
114 .await
115 .map_err(|e| SynapticError::Tool(format!("Wikipedia summary request failed: {e}")))?;
116
117 let status = response.status();
118 if status.as_u16() == 404 {
119 return Ok(None);
120 }
121
122 if !status.is_success() {
123 return Err(SynapticError::Tool(format!(
124 "Wikipedia summary error: HTTP {}",
125 status.as_u16()
126 )));
127 }
128
129 let body: Value = response
130 .json()
131 .await
132 .map_err(|e| SynapticError::Tool(format!("Wikipedia summary parse error: {e}")))?;
133
134 Ok(Some(json!({
135 "title": body["title"].as_str().unwrap_or(""),
136 "summary": body["extract"].as_str().unwrap_or(""),
137 "url": body["content_urls"]["desktop"]["page"].as_str().unwrap_or(""),
138 })))
139 }
140}
141
142#[async_trait]
143impl Tool for WikipediaTool {
144 fn name(&self) -> &'static str {
145 "wikipedia_search"
146 }
147
148 fn description(&self) -> &'static str {
149 "Search Wikipedia and retrieve article summaries. \
150 Useful for factual questions about people, places, events, and concepts. \
151 No API key required."
152 }
153
154 fn parameters(&self) -> Option<Value> {
155 Some(json!({
156 "type": "object",
157 "properties": {
158 "query": {
159 "type": "string",
160 "description": "The search query or article title to look up on Wikipedia"
161 }
162 },
163 "required": ["query"]
164 }))
165 }
166
167 async fn call(&self, args: Value) -> Result<Value, SynapticError> {
168 let query = args["query"]
169 .as_str()
170 .ok_or_else(|| SynapticError::Tool("missing 'query' parameter".to_string()))?;
171
172 let titles = self.search_titles(query).await?;
173
174 if titles.is_empty() {
175 return Ok(json!({
176 "query": query,
177 "results": [],
178 "message": "No Wikipedia articles found for this query.",
179 }));
180 }
181
182 let mut results = Vec::new();
183 for title in &titles {
184 if let Some(summary) = self.get_summary(title).await? {
185 results.push(summary);
186 }
187 }
188
189 Ok(json!({
190 "query": query,
191 "results": results,
192 }))
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn tool_metadata() {
202 let tool = WikipediaTool::new();
203 assert_eq!(tool.name(), "wikipedia_search");
204 assert!(!tool.description().is_empty());
205 }
206
207 #[test]
208 fn tool_schema() {
209 let tool = WikipediaTool::new();
210 let schema = tool.parameters().unwrap();
211 assert_eq!(schema["type"], "object");
212 assert!(schema["properties"]["query"].is_object());
213 }
214
215 #[test]
216 fn builder_methods() {
217 let tool = WikipediaTool::new().with_language("de").with_max_results(5);
218 assert_eq!(tool.language, "de");
219 assert_eq!(tool.max_results, 5);
220 }
221
222 #[tokio::test]
223 async fn missing_query_returns_error() {
224 let tool = WikipediaTool::new();
225 let result = tool.call(json!({})).await;
226 assert!(result.is_err());
227 }
228}