steer_core/tools/static_tools/
fetch.rs1use 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(¶ms.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}