Skip to main content

llimorse_tools/
web_search.rs

1//! A web_search tool using SearXNG
2
3use 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    /// Look up information on the web. Returns the top results with titles, URLs, and snippets.
13    #[derive(Debug)]
14    'params: pub struct WebSearchParams {
15        /// Search query
16        query: String,
17
18        /// Limit the number of results
19        max_results: Option<usize>,
20    }
21
22    /// Return web search results.
23    #[derive(Debug)]
24    'result: pub struct WebSearchResults {
25        /// Original query
26        query: String,
27
28        /// Search results
29        results: Vec<WebSearchResult>,
30    }
31
32    /// Execute a web search via SearXNG.
33    #[derive(Clone, Debug)]
34    'state: pub struct WebSearch {
35        /// URL base to query the SearXNG instance
36        searxng_url_base: String,
37    }
38}
39
40/// A single web search result
41#[derive(Debug, Deserialize, Serialize)]
42struct WebSearchResult {
43    /// The page title
44    title: String,
45
46    /// The source URL
47    url: String,
48
49    /// A snippet summarizing the content
50    snippet: String,
51}
52
53impl WebSearch {
54    /// Create a new [`WebSearch`] tool instance, accessing the given URL for queries.
55    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}