Skip to main content

synaptic_tools/
wikipedia.rs

1//! Wikipedia search and summary tool.
2//!
3//! Uses the Wikipedia REST API — no API key required.
4//! Searches Wikipedia and returns article summaries.
5
6use async_trait::async_trait;
7use serde_json::{json, Value};
8use synaptic_core::{SynapticError, Tool};
9
10/// A tool that searches Wikipedia and returns article summaries.
11///
12/// Uses the free Wikipedia REST API — no API key required.
13///
14/// # Example
15///
16/// ```rust,ignore
17/// use synaptic_tools::WikipediaTool;
18/// use synaptic_core::Tool;
19///
20/// let tool = WikipediaTool::new();
21/// let result = tool.call(serde_json::json!({"query": "Rust programming language"})).await?;
22/// ```
23pub struct WikipediaTool {
24    client: reqwest::Client,
25    /// Wikipedia language code (default: `"en"`).
26    language: String,
27    /// Maximum number of search results to return (default: 3).
28    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}