Skip to main content

steer_core/tools/static_tools/
fetch.rs

1use async_trait::async_trait;
2
3use crate::app::conversation::{Message, MessageData, UserContent};
4use crate::config::model::builtin::claude_haiku_4_5 as summarization_model;
5use crate::tools::capability::Capabilities;
6use crate::tools::services::ModelCallError;
7use crate::tools::static_tool::{StaticTool, StaticToolContext, StaticToolError};
8use steer_tools::result::FetchResult;
9use steer_tools::tools::fetch::{FetchError, FetchParams, FetchToolSpec};
10
11const DESCRIPTION: &str = r#"- Fetches content from a specified URL and processes it using an AI model
12- Takes a URL and a prompt as input
13- Fetches the URL content and passes it to a small, fast model for analysis
14- Returns the model's response about the content
15- Use this tool when you need to retrieve and analyze web content
16
17Usage notes:
18  - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with "mcp__".
19  - The URL must be a fully-formed valid URL
20  - HTTP URLs will be automatically upgraded to HTTPS
21  - For security reasons, the URL's domain must have been provided directly by the user, unless it's on a small pre-approved set of the top few dozen hosts for popular coding resources, like react.dev.
22  - The prompt should describe what information you want to extract from the page
23  - This tool is read-only and does not modify any files
24  - Results may be summarized if the content is very large"#;
25
26pub struct FetchTool;
27
28#[async_trait]
29impl StaticTool for FetchTool {
30    type Params = FetchParams;
31    type Output = FetchResult;
32    type Spec = FetchToolSpec;
33
34    const DESCRIPTION: &'static str = DESCRIPTION;
35    const REQUIRES_APPROVAL: bool = true;
36    const REQUIRED_CAPABILITIES: Capabilities = Capabilities::from_bits_truncate(
37        Capabilities::NETWORK.bits() | Capabilities::MODEL_CALLER.bits(),
38    );
39
40    async fn execute(
41        &self,
42        params: Self::Params,
43        ctx: &StaticToolContext,
44    ) -> Result<Self::Output, StaticToolError<FetchError>> {
45        let model_caller = ctx
46            .services
47            .model_caller()
48            .ok_or_else(|| StaticToolError::missing_capability("model_caller"))?;
49
50        let content = fetch_url(&params.url, &ctx.cancellation_token).await?;
51
52        let user_message = format!(
53            r"Web page content:
54---
55{content}
56---
57
58{}
59
60Provide a concise response based only on the content above.
61",
62            params.prompt
63        );
64
65        let messages = vec![Message {
66            data: MessageData::User {
67                content: vec![UserContent::Text { text: user_message }],
68            },
69            timestamp: Message::current_timestamp(),
70            id: Message::generate_id("user", Message::current_timestamp()),
71            parent_message_id: None,
72        }];
73
74        let response = model_caller
75            .call(
76                &summarization_model(),
77                messages,
78                None,
79                ctx.cancellation_token.clone(),
80            )
81            .await
82            .map_err(|e| match e {
83                ModelCallError::Api(msg) => {
84                    StaticToolError::execution(FetchError::ModelCallFailed { message: msg })
85                }
86                ModelCallError::Cancelled => StaticToolError::Cancelled,
87            })?;
88
89        let result_content = response.extract_text().trim().to_string();
90
91        Ok(FetchResult {
92            url: params.url,
93            content: result_content,
94        })
95    }
96}
97
98async fn fetch_url(
99    url: &str,
100    token: &tokio_util::sync::CancellationToken,
101) -> Result<String, StaticToolError<FetchError>> {
102    let client = reqwest::Client::new();
103    let request = client.get(url);
104
105    let response = tokio::select! {
106        result = request.send() => result,
107        () = token.cancelled() => return Err(StaticToolError::Cancelled),
108    };
109
110    match response {
111        Ok(response) => {
112            let status = response.status();
113            let url = response.url().to_string();
114
115            if !status.is_success() {
116                return Err(StaticToolError::execution(FetchError::Http {
117                    status: status.as_u16(),
118                    url,
119                }));
120            }
121
122            let text = tokio::select! {
123                result = response.text() => result,
124                () = token.cancelled() => return Err(StaticToolError::Cancelled),
125            };
126
127            match text {
128                Ok(content) => Ok(content),
129                Err(e) => Err(StaticToolError::execution(FetchError::ReadFailed {
130                    url,
131                    message: e.to_string(),
132                })),
133            }
134        }
135        Err(e) => Err(StaticToolError::execution(FetchError::RequestFailed {
136            message: format!("Request to URL {url} failed: {e}"),
137        })),
138    }
139}