steer_core/tools/
fetch.rs1use crate::config::LlmConfigProvider;
2use schemars::JsonSchema;
3use serde::Deserialize;
4use std::sync::Arc;
5use steer_macros::tool_external as tool;
6use steer_tools::ToolError;
7
8#[derive(Deserialize, Debug, JsonSchema)]
9pub struct FetchParams {
10 pub url: String,
12 pub prompt: String,
14}
15
16tool! {
17 pub struct FetchTool {
18 pub llm_config_provider: Arc<LlmConfigProvider>,
19 } {
20 params: FetchParams,
21 output: steer_tools::result::FetchResult,
22 variant: Fetch,
23 description: r#"- Fetches content from a specified URL and processes it using an AI model
24- Takes a URL and a prompt as input
25- Fetches the URL content, converts HTML to markdown
26- Processes the content with the prompt using a small, fast model
27- Returns the model's response about the content
28- Use this tool when you need to retrieve and analyze web content
29
30Usage notes:
31 - 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__".
32 - The URL must be a fully-formed valid URL
33 - HTTP URLs will be automatically upgraded to HTTPS
34 - 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.
35 - The prompt should describe what information you want to extract from the page
36 - This tool is read-only and does not modify any files
37 - Results may be summarized if the content is very large
38 - Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL"#,
39 name: "web_fetch",
40 require_approval: true
41 }
42
43 async fn run(
44 tool: &FetchTool,
45 params: FetchParams,
46 context: &steer_tools::ExecutionContext,
47 ) -> Result<steer_tools::result::FetchResult, ToolError> {
48 let token = Some(context.cancellation_token.clone());
49 let client = reqwest::Client::new();
51
52 let request = client.get(¶ms.url);
54
55 let response = if let Some(ref token) = token {
57 tokio::select! {
58 result = request.send() => result,
59 _ = token.cancelled() => return Err(ToolError::Cancelled("Fetch".to_string())),
60 }
61 } else {
62 request.send().await
63 };
64
65 match response {
67 Ok(response) => {
68 let status = response.status();
69 let url = response.url().to_string();
70
71 if !status.is_success() {
72 return Err(ToolError::execution(
73 "Fetch",
74 format!("HTTP error: {status} when fetching URL: {url}")
75 ));
76 }
77
78 let text = if let Some(ref token) = token {
80 tokio::select! {
81 result = response.text() => result,
82 _ = token.cancelled() => return Err(ToolError::Cancelled("Fetch".to_string())),
83 }
84 } else {
85 response.text().await
86 };
87
88 match text {
89 Ok(content) => {
90 process_web_page_content(tool, content, params.prompt.clone(), token).await
91 .map(|payload| steer_tools::result::FetchResult {
92 url: params.url,
93 content: payload,
94 })
95 }
96 Err(e) => Err(ToolError::execution(
97 "Fetch",
98 format!("Failed to read response body from {url}: {e}")
99 )),
100 }
101 }
102 Err(e) => Err(ToolError::execution(
103 "Fetch",
104 format!("Request to URL {} failed: {}", params.url, e)
105 )),
106 }
107 }
108}
109
110impl FetchTool {
112 pub fn is_read_only(&self) -> bool {
113 true
114 }
115}
116
117async fn process_web_page_content(
118 tool: &FetchTool,
119 content: String,
120 prompt: String,
121 token: Option<tokio_util::sync::CancellationToken>,
122) -> Result<String, ToolError> {
123 let model_registry = std::sync::Arc::new(
125 crate::model_registry::ModelRegistry::load(&[]).map_err(|e| {
126 ToolError::execution("fetch", format!("Failed to load model registry: {e}"))
127 })?,
128 );
129 let provider_registry =
130 std::sync::Arc::new(crate::auth::ProviderRegistry::load(&[]).map_err(|e| {
131 ToolError::execution("fetch", format!("Failed to load provider registry: {e}"))
132 })?);
133
134 let client = crate::api::Client::new_with_deps(
135 (*tool.llm_config_provider).clone(),
136 provider_registry,
137 model_registry,
138 );
139 let user_message = format!(
140 r#"Web page content:
141---
142{content}
143---
144
145{prompt}
146
147Provide a concise response based only on the content above.
148"#
149 );
150
151 let messages = vec![crate::app::conversation::Message {
152 data: crate::app::conversation::MessageData::User {
153 content: vec![crate::app::conversation::UserContent::Text { text: user_message }],
154 },
155 timestamp: crate::app::conversation::Message::current_timestamp(),
156 id: crate::app::conversation::Message::generate_id(
157 "user",
158 crate::app::conversation::Message::current_timestamp(),
159 ),
160 parent_message_id: None,
161 }];
162
163 let token = if let Some(ref token) = token {
164 token.clone()
165 } else {
166 tokio_util::sync::CancellationToken::new()
167 };
168
169 match client
170 .complete(
171 &crate::config::model::builtin::claude_3_5_haiku_20241022(),
172 messages,
173 None,
174 None,
175 None,
176 token,
177 )
178 .await
179 {
180 Ok(response) => {
181 let prefix = response.extract_text();
182 Ok(prefix.trim().to_string())
183 }
184 Err(e) => Err(ToolError::execution(
185 "Fetch",
186 format!("Failed to process web page content: {e}"),
187 )),
188 }
189}