llimorse_tools/
web_search.rs1use anyhow::{Context, Result, anyhow};
4use llimorse::{Agent, CallableTool};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::fmt;
8
9llimorse::tool! {
10 'name: "web_search";
11
12 #[derive(Debug)]
14 'params: pub struct WebSearchParams {
15 query: String,
17
18 max_results: Option<usize>,
20 }
21
22 #[derive(Debug)]
24 'result: pub struct WebSearchResults {
25 query: String,
27
28 results: Vec<WebSearchResult>,
30 }
31
32 #[derive(Clone, Debug)]
34 'state: pub struct WebSearch {
35 searxng_url_base: String,
37 }
38}
39
40#[derive(Debug, Deserialize, Serialize)]
42struct WebSearchResult {
43 title: String,
45
46 url: String,
48
49 snippet: String,
51}
52
53impl WebSearch {
54 pub fn new(searxng_url: &str) -> Self {
56 WebSearch {
57 searxng_url_base: searxng_url.trim_end_matches('/').to_string(),
58 }
59 }
60}
61
62impl CallableTool for WebSearch {
63 async fn execute(
64 &self,
65 _agent: &Agent,
66 arguments: WebSearchParams,
67 ) -> Result<WebSearchResults> {
68 let searxng_url = format!(
69 "{}/search?q={}&format=json&categories=general",
70 self.searxng_url_base,
71 urlencoding::encode(&arguments.query),
72 );
73
74 let http = reqwest::Client::new();
75
76 let resp: Value = http
77 .get(&searxng_url)
78 .send()
79 .await
80 .context("SearXNG request failed")?
81 .json()
82 .await
83 .context("SearXNG response not valid JSON")?;
84
85 let mut results: &[Value] = resp["results"]
86 .as_array()
87 .ok_or_else(|| anyhow!("No results returned"))?;
88
89 if let Some(max_results) = arguments.max_results
90 && results.len() > max_results
91 {
92 results = &results[..max_results];
93 }
94
95 let results = results
96 .iter()
97 .map(|r| WebSearchResult {
98 title: r["title"].as_str().unwrap_or("").to_string(),
99 url: r["url"].as_str().unwrap_or("").to_string(),
100 snippet: r["content"].as_str().unwrap_or("").to_string(),
101 })
102 .collect::<Vec<_>>();
103
104 Ok(WebSearchResults {
105 query: arguments.query,
106 results,
107 })
108 }
109}
110
111impl fmt::Display for WebSearchParams {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 write!(f, "query={:?}", self.query)?;
114 if let Some(max_results) = self.max_results {
115 write!(f, " max_results={max_results}")?;
116 }
117 Ok(())
118 }
119}
120
121impl fmt::Display for WebSearchResults {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 write!(f, "[")?;
124 let result_count = self.results.len();
125 for (i, result) in self.results.iter().enumerate() {
126 if i == result_count - 1 {
127 write!(f, "{result}")?;
128 } else {
129 write!(f, "{result}, ")?;
130 }
131 }
132 write!(f, "]")
133 }
134}
135
136impl fmt::Display for WebSearchResult {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 write!(f, "{} ({})", self.title, self.url)
139 }
140}