1use crate::tool::{Tool, ToolResult, McpContent};
3use crate::error::BrightDataError;
4use crate::extras::logger::JSON_LOGGER;
5use crate::filters::{ResponseFilter, ResponseStrategy, ResponseType};
6use async_trait::async_trait;
7use reqwest::Client;
8use serde_json::{json, Value};
9use std::env;
10use std::time::Duration;
11use std::collections::HashMap;
12use log::info;
13
14pub struct CommodityDataTool;
15
16#[async_trait]
17impl Tool for CommodityDataTool {
18 fn name(&self) -> &str {
19 "get_commodity_data"
20 }
21
22 fn description(&self) -> &str {
23 "Get comprehensive commodity prices and market data with real-time updates and market-specific sources"
24 }
25
26 fn input_schema(&self) -> Value {
27 json!({
28 "type": "object",
29 "properties": {
30 "query": {
31 "type": "string",
32 "description": "Commodity name (gold, silver, crude oil), commodity symbol (GC, SI, CL), or commodity market overview"
33 },
34 "commodity_type": {
35 "type": "string",
36 "enum": ["precious_metals", "energy", "agricultural", "industrial_metals", "livestock", "all"],
37 "default": "all",
38 "description": "Category of commodity for targeted data sources"
39 },
40 "market_region": {
41 "type": "string",
42 "enum": ["global", "us", "asia", "europe", "india"],
43 "default": "global",
44 "description": "Regional market focus"
45 },
46 "data_source": {
47 "type": "string",
48 "enum": ["search", "direct", "auto"],
49 "default": "auto",
50 "description": "Data source strategy - search (SERP), direct (commodity exchanges), auto (smart selection)"
51 },
52 "time_range": {
53 "type": "string",
54 "enum": ["realtime", "day", "week", "month", "year"],
55 "default": "realtime",
56 "description": "Time range for price data"
57 },
58 "include_futures": {
59 "type": "boolean",
60 "default": true,
61 "description": "Include futures contract prices"
62 },
63 "include_analysis": {
64 "type": "boolean",
65 "default": false,
66 "description": "Include market analysis and trends"
67 },
68 "currency": {
69 "type": "string",
70 "enum": ["USD", "EUR", "INR", "CNY", "JPY"],
71 "default": "USD",
72 "description": "Currency for price display"
73 }
74 },
75 "required": ["query"]
76 })
77 }
78
79 async fn execute_internal(&self, parameters: Value) -> Result<ToolResult, BrightDataError> {
85 let query = parameters
86 .get("query")
87 .and_then(|v| v.as_str())
88 .ok_or_else(|| BrightDataError::ToolError("Missing 'query' parameter".into()))?;
89
90 let commodity_type = parameters
91 .get("commodity_type")
92 .and_then(|v| v.as_str())
93 .unwrap_or("all");
94
95 let market_region = parameters
96 .get("market_region")
97 .and_then(|v| v.as_str())
98 .unwrap_or("global");
99
100 let data_source = parameters
101 .get("data_source")
102 .and_then(|v| v.as_str())
103 .unwrap_or("auto");
104
105 let time_range = parameters
106 .get("time_range")
107 .and_then(|v| v.as_str())
108 .unwrap_or("realtime");
109
110 let include_futures = parameters
111 .get("include_futures")
112 .and_then(|v| v.as_bool())
113 .unwrap_or(true);
114
115 let include_analysis = parameters
116 .get("include_analysis")
117 .and_then(|v| v.as_bool())
118 .unwrap_or(false);
119
120 let currency = parameters
121 .get("currency")
122 .and_then(|v| v.as_str())
123 .unwrap_or("USD");
124
125 let query_priority = ResponseStrategy::classify_query_priority(query);
127 let recommended_tokens = ResponseStrategy::get_recommended_token_allocation(query);
128
129 if std::env::var("TRUNCATE_FILTER")
131 .map(|v| v.to_lowercase() == "true")
132 .unwrap_or(false) {
133
134 let (_, remaining_tokens) = ResponseStrategy::get_token_budget_status();
136 if remaining_tokens < 150 && !matches!(query_priority, crate::filters::strategy::QueryPriority::Critical) {
137 return Ok(ResponseStrategy::create_response("", query, "commodity", "budget_limit", json!({}), ResponseType::Skip));
138 }
139 }
140
141 let execution_id = format!("commodity_{}", chrono::Utc::now().format("%Y%m%d_%H%M%S%.3f"));
142
143 match self.fetch_commodity_data_with_fallbacks_and_priority(
144 query, commodity_type, market_region, data_source, time_range,
145 include_futures, include_analysis, currency, query_priority, recommended_tokens, &execution_id
146 ).await {
147 Ok(result) => {
148 let content = result.get("content").and_then(|c| c.as_str()).unwrap_or("");
149 let source_used = result.get("source_used").and_then(|s| s.as_str()).unwrap_or("Unknown");
150
151 let tool_result = if std::env::var("TRUNCATE_FILTER")
153 .map(|v| v.to_lowercase() == "true")
154 .unwrap_or(false) {
155
156 ResponseStrategy::create_financial_response(
157 "commodity", query, market_region, source_used, content, result.clone()
158 )
159 } else {
160 let mcp_content = vec![McpContent::text(format!(
162 "🥇 **Commodity Data for: {}**\n\nType: {} | Region: {} | Priority: {:?} | Tokens: {}\nSource: {} | Time Range: {} | Currency: {} | Futures: {} | Analysis: {}\nExecution ID: {}\n\n{}",
163 query, commodity_type, market_region, query_priority, recommended_tokens, source_used, time_range, currency, include_futures, include_analysis, execution_id, content
164 ))];
165 ToolResult::success_with_raw(mcp_content, result)
166 };
167
168 if std::env::var("TRUNCATE_FILTER")
169 .map(|v| v.to_lowercase() == "true")
170 .unwrap_or(false) {
171 Ok(ResponseStrategy::apply_size_limits(tool_result))
172 } else {
173 Ok(tool_result)
174 }
175 }
176 Err(e) => {
177 if std::env::var("TRUNCATE_FILTER")
178 .map(|v| v.to_lowercase() == "true")
179 .unwrap_or(false) {
180 Ok(ResponseStrategy::create_error_response(query, &e.to_string()))
181 } else {
182 Err(e)
183 }
184 }
185 }
186 }
187}
188
189impl CommodityDataTool {
190 async fn fetch_commodity_data_with_fallbacks_and_priority(
192 &self,
193 query: &str,
194 commodity_type: &str,
195 market_region: &str,
196 data_source: &str,
197 time_range: &str,
198 include_futures: bool,
199 include_analysis: bool,
200 currency: &str,
201 query_priority: crate::filters::strategy::QueryPriority,
202 token_budget: usize,
203 execution_id: &str,
204 ) -> Result<Value, BrightDataError> {
205 let sources_to_try = self.build_prioritized_sources_with_priority(query, commodity_type, market_region, data_source, query_priority);
206 let mut last_error = None;
207
208 for (sequence, (source_type, url_or_query, source_name)) in sources_to_try.iter().enumerate() {
209 match source_type.as_str() {
210 "direct" => {
211 match self.fetch_direct_commodity_data_with_priority(
212 url_or_query, query, commodity_type, market_region,
213 source_name, query_priority, token_budget, execution_id, sequence as u64
214 ).await {
215 Ok(mut result) => {
216 let content = result.get("content").and_then(|c| c.as_str()).unwrap_or("");
217
218 if std::env::var("TRUNCATE_FILTER")
219 .map(|v| v.to_lowercase() == "true")
220 .unwrap_or(false) {
221
222 if ResponseStrategy::should_try_next_source(content) {
223 last_error = Some(BrightDataError::ToolError(format!(
224 "{} returned low-quality content", source_name
225 )));
226 continue;
227 }
228 }
229
230 result["source_used"] = json!(source_name);
231 result["data_source_type"] = json!("direct");
232 result["priority"] = json!(format!("{:?}", query_priority));
233 return Ok(result);
234 }
235 Err(e) => last_error = Some(e),
236 }
237 }
238 "search" => {
239 match self.fetch_search_commodity_data_with_priority(
240 url_or_query, commodity_type, market_region, time_range,
241 include_futures, include_analysis, currency, source_name,
242 query_priority, token_budget, execution_id, sequence as u64
243 ).await {
244 Ok(mut result) => {
245 let content = result.get("content").and_then(|c| c.as_str()).unwrap_or("");
246
247 if std::env::var("TRUNCATE_FILTER")
248 .map(|v| v.to_lowercase() == "true")
249 .unwrap_or(false) {
250
251 if ResponseStrategy::should_try_next_source(content) {
252 last_error = Some(BrightDataError::ToolError(format!(
253 "{} returned low-quality content", source_name
254 )));
255 continue;
256 }
257 }
258
259 result["source_used"] = json!(source_name);
260 result["data_source_type"] = json!("search");
261 result["priority"] = json!(format!("{:?}", query_priority));
262 return Ok(result);
263 }
264 Err(e) => last_error = Some(e),
265 }
266 }
267 _ => continue,
268 }
269 }
270
271 Err(last_error.unwrap_or_else(|| BrightDataError::ToolError("All commodity data sources failed".into())))
272 }
273
274 fn build_prioritized_sources_with_priority(&self, query: &str, commodity_type: &str, market_region: &str, data_source: &str, priority: crate::filters::strategy::QueryPriority) -> Vec<(String, String, String)> {
276 let mut sources = Vec::new();
277 let query_lower = query.to_lowercase();
278
279 match data_source {
280 "direct" => {
281 sources.extend(self.get_direct_sources_with_priority(commodity_type, market_region, priority));
282 }
283 "search" => {
284 sources.extend(self.get_search_sources_with_priority(query, commodity_type, market_region, priority));
285 }
286 "auto" | _ => {
287 match priority {
289 crate::filters::strategy::QueryPriority::Critical => {
290 sources.extend(self.get_direct_sources_with_priority(commodity_type, market_region, priority));
292 sources.extend(self.get_search_sources_with_priority(query, commodity_type, market_region, priority));
293 }
294 _ => {
295 if query_lower.contains("price") || query_lower.contains("futures") || query_lower.contains("contract") {
297 sources.extend(self.get_direct_sources_with_priority(commodity_type, market_region, priority));
298 sources.extend(self.get_search_sources_with_priority(query, commodity_type, market_region, priority));
299 } else {
300 sources.extend(self.get_search_sources_with_priority(query, commodity_type, market_region, priority));
301 sources.extend(self.get_direct_sources_with_priority(commodity_type, market_region, priority));
302 }
303 }
304 }
305 }
306 }
307
308 let max_sources = match priority {
310 crate::filters::strategy::QueryPriority::Critical => sources.len(), crate::filters::strategy::QueryPriority::High => std::cmp::min(sources.len(), 3),
312 crate::filters::strategy::QueryPriority::Medium => std::cmp::min(sources.len(), 2),
313 crate::filters::strategy::QueryPriority::Low => std::cmp::min(sources.len(), 1),
314 };
315
316 sources.truncate(max_sources);
317 sources
318 }
319
320 fn get_direct_sources_with_priority(&self, commodity_type: &str, market_region: &str, priority: crate::filters::strategy::QueryPriority) -> Vec<(String, String, String)> {
321 let mut sources = Vec::new();
322
323 match market_region {
324 "india" => {
325 sources.push(("direct".to_string(), "https://www.mcxindia.com/market-data/live-rates".to_string(), "MCX India".to_string()));
326 if matches!(priority, crate::filters::strategy::QueryPriority::Critical | crate::filters::strategy::QueryPriority::High) {
327 sources.push(("direct".to_string(), "https://www.ncdex.com/market/live-rates".to_string(), "NCDEX".to_string()));
328 }
329 if commodity_type == "precious_metals" || commodity_type == "all" {
330 sources.push(("direct".to_string(), "https://www.goldpriceindia.com/".to_string(), "Gold Price India".to_string()));
331 }
332 }
333 "us" => {
334 sources.push(("direct".to_string(), "https://www.cmegroup.com/markets.html".to_string(), "CME Group".to_string()));
335 if matches!(priority, crate::filters::strategy::QueryPriority::Critical | crate::filters::strategy::QueryPriority::High) {
336 sources.push(("direct".to_string(), "https://www.theice.com/market-data".to_string(), "ICE Markets".to_string()));
337 }
338 if commodity_type == "energy" || commodity_type == "all" {
339 sources.push(("direct".to_string(), "https://www.eia.gov/petroleum/".to_string(), "EIA Energy".to_string()));
340 }
341 }
342 "europe" => {
343 sources.push(("direct".to_string(), "https://www.theice.com/market-data/dashboard".to_string(), "ICE Europe".to_string()));
344 if !matches!(priority, crate::filters::strategy::QueryPriority::Low) {
345 sources.push(("direct".to_string(), "https://www.lme.com/Metals".to_string(), "London Metal Exchange".to_string()));
346 }
347 }
348 "asia" => {
349 sources.push(("direct".to_string(), "https://www.tocom.or.jp/market/".to_string(), "TOCOM".to_string()));
350 if !matches!(priority, crate::filters::strategy::QueryPriority::Low) {
351 sources.push(("direct".to_string(), "https://www.shfe.com.cn/en/".to_string(), "Shanghai Futures".to_string()));
352 }
353 }
354 "global" | _ => {
355 sources.push(("direct".to_string(), "https://www.investing.com/commodities/".to_string(), "Investing.com Commodities".to_string()));
357 if matches!(priority, crate::filters::strategy::QueryPriority::Critical | crate::filters::strategy::QueryPriority::High) {
358 sources.push(("direct".to_string(), "https://www.bloomberg.com/markets/commodities".to_string(), "Bloomberg Commodities".to_string()));
359 sources.push(("direct".to_string(), "https://www.marketwatch.com/investing/commodities".to_string(), "MarketWatch Commodities".to_string()));
360 }
361
362 if !matches!(priority, crate::filters::strategy::QueryPriority::Low) {
364 match commodity_type {
365 "precious_metals" => {
366 sources.push(("direct".to_string(), "https://www.kitco.com/market/".to_string(), "Kitco Metals".to_string()));
367 sources.push(("direct".to_string(), "https://www.lbma.org.uk/prices-and-data".to_string(), "LBMA".to_string()));
368 }
369 "energy" => {
370 sources.push(("direct".to_string(), "https://oilprice.com/".to_string(), "Oil Price".to_string()));
371 sources.push(("direct".to_string(), "https://www.eia.gov/petroleum/".to_string(), "EIA".to_string()));
372 }
373 "agricultural" => {
374 sources.push(("direct".to_string(), "https://www.cbot.com/".to_string(), "CBOT".to_string()));
375 sources.push(("direct".to_string(), "https://www.usda.gov/topics/data".to_string(), "USDA".to_string()));
376 }
377 "industrial_metals" => {
378 sources.push(("direct".to_string(), "https://www.lme.com/Metals".to_string(), "LME".to_string()));
379 }
380 _ => {}
381 }
382 }
383 }
384 }
385
386 sources
387 }
388
389 fn get_search_sources_with_priority(&self, query: &str, commodity_type: &str, market_region: &str, priority: crate::filters::strategy::QueryPriority) -> Vec<(String, String, String)> {
390 let mut sources = Vec::new();
391
392 let region_terms = match market_region {
393 "india" => "india MCX NCDEX commodity exchange rupee INR",
394 "us" => "united states CME CBOT futures contract dollar USD",
395 "europe" => "europe ICE LME futures contract euro EUR",
396 "asia" => "asia TOCOM shanghai futures contract",
397 "global" => "global international commodity futures trading",
398 _ => "commodity futures trading market price"
399 };
400
401 let commodity_terms = match commodity_type {
402 "precious_metals" => "gold silver platinum palladium precious metals spot price",
403 "energy" => "crude oil natural gas gasoline heating oil energy futures",
404 "agricultural" => "wheat corn soybeans rice agricultural commodity farming",
405 "industrial_metals" => "copper aluminum zinc nickel industrial metals LME",
406 "livestock" => "cattle hogs pork livestock futures meat",
407 _ => "commodity futures spot price market trading"
408 };
409
410 match priority {
412 crate::filters::strategy::QueryPriority::Critical => {
413 sources.push(("search".to_string(),
414 format!("{} {} live current real-time price", query, region_terms),
415 "Critical Commodity Search".to_string()));
416 }
417 crate::filters::strategy::QueryPriority::High => {
418 sources.push(("search".to_string(),
419 format!("{} {} {} current price futures today", query, region_terms, commodity_terms),
420 "High Priority Commodity Search".to_string()));
421 sources.push(("search".to_string(),
422 format!("{} {} latest trading data market analysis", query, commodity_terms),
423 "Commodity Market Analysis".to_string()));
424 }
425 _ => {
426 sources.push(("search".to_string(),
427 format!("{} {} price chart trends technical analysis", query, region_terms),
428 "Commodity Trends Search".to_string()));
429 }
430 }
431
432 sources
433 }
434
435 async fn fetch_direct_commodity_data_with_priority(
437 &self,
438 url: &str,
439 query: &str,
440 commodity_type: &str,
441 market_region: &str,
442 source_name: &str,
443 priority: crate::filters::strategy::QueryPriority,
444 token_budget: usize,
445 execution_id: &str,
446 sequence: u64,
447 ) -> Result<Value, BrightDataError> {
448 let api_token = env::var("BRIGHTDATA_API_TOKEN")
449 .or_else(|_| env::var("API_TOKEN"))
450 .map_err(|_| BrightDataError::ToolError("Missing BRIGHTDATA_API_TOKEN".into()))?;
451
452 let base_url = env::var("BRIGHTDATA_BASE_URL")
453 .unwrap_or_else(|_| "https://api.brightdata.com".to_string());
454
455 let zone = env::var("WEB_UNLOCKER_ZONE")
456 .unwrap_or_else(|_| "default".to_string());
457
458 info!("🥇 Priority {} direct commodity data fetch from {} using zone: {} (execution: {})",
459 format!("{:?}", priority), source_name, zone, execution_id);
460
461 let mut payload = json!({
462 "url": url,
463 "zone": zone,
464 "format": "raw",
465 "data_format": "markdown",
466 "render": true
467 });
468
469 if std::env::var("TRUNCATE_FILTER")
471 .map(|v| v.to_lowercase() == "true")
472 .unwrap_or(false) {
473
474 payload["processing_priority"] = json!(format!("{:?}", priority));
475 payload["token_budget"] = json!(token_budget);
476 }
477
478 let client = Client::builder()
479 .timeout(Duration::from_secs(120))
480 .build()
481 .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
482
483 let response = client
484 .post(&format!("{}/request", base_url))
485 .header("Authorization", format!("Bearer {}", api_token))
486 .header("Content-Type", "application/json")
487 .json(&payload)
488 .send()
489 .await
490 .map_err(|e| BrightDataError::ToolError(format!("Direct commodity data request failed: {}", e)))?;
491
492 let status = response.status().as_u16();
493 let response_headers: HashMap<String, String> = response
494 .headers()
495 .iter()
496 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
497 .collect();
498
499 if let Err(e) = JSON_LOGGER.log_brightdata_request(
501 &format!("{}_{}", execution_id, sequence),
502 &zone,
503 url,
504 payload.clone(),
505 status,
506 response_headers,
507 "markdown"
508 ).await {
509 log::warn!("Failed to log BrightData request: {}", e);
510 }
511
512 if !response.status().is_success() {
513 let error_text = response.text().await.unwrap_or_default();
514 return Err(BrightDataError::ToolError(format!(
515 "BrightData direct commodity data error {}: {}",
516 status, error_text
517 )));
518 }
519
520 let raw_content = response.text().await
521 .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
522
523 let filtered_content = if std::env::var("TRUNCATE_FILTER")
525 .map(|v| v.to_lowercase() == "true")
526 .unwrap_or(false) {
527
528 if ResponseFilter::is_error_page(&raw_content) {
529 return Err(BrightDataError::ToolError(format!("{} returned error page", source_name)));
530 } else {
531 let max_tokens = token_budget / 2; ResponseFilter::extract_high_value_financial_data(&raw_content, max_tokens)
533 }
534 } else {
535 raw_content.clone()
536 };
537
538 Ok(json!({
539 "content": filtered_content,
540 "query": query,
541 "commodity_type": commodity_type,
542 "market_region": market_region,
543 "priority": format!("{:?}", priority),
544 "token_budget": token_budget,
545 "execution_id": execution_id,
546 "sequence": sequence,
547 "success": true
548 }))
549 }
550
551 async fn fetch_search_commodity_data_with_priority(
553 &self,
554 search_query: &str,
555 commodity_type: &str,
556 market_region: &str,
557 time_range: &str,
558 include_futures: bool,
559 include_analysis: bool,
560 currency: &str,
561 source_name: &str,
562 priority: crate::filters::strategy::QueryPriority,
563 token_budget: usize,
564 execution_id: &str,
565 sequence: u64,
566 ) -> Result<Value, BrightDataError> {
567 let api_token = env::var("BRIGHTDATA_API_TOKEN")
568 .or_else(|_| env::var("API_TOKEN"))
569 .map_err(|_| BrightDataError::ToolError("Missing BRIGHTDATA_API_TOKEN".into()))?;
570
571 let base_url = env::var("BRIGHTDATA_BASE_URL")
572 .unwrap_or_else(|_| "https://api.brightdata.com".to_string());
573
574 let zone = env::var("BRIGHTDATA_SERP_ZONE")
575 .unwrap_or_else(|_| "serp_api2".to_string());
576
577 let mut enhanced_query = search_query.to_string();
579
580 match priority {
582 crate::filters::strategy::QueryPriority::Critical => {
583 enhanced_query.push_str(" live current real time");
584 }
585 crate::filters::strategy::QueryPriority::High => {
586 if include_futures {
587 enhanced_query.push_str(" futures contract trading");
588 }
589 if include_analysis {
590 enhanced_query.push_str(" market analysis trends forecast");
591 }
592 }
593 _ => {
594 enhanced_query.push_str(" overview");
596 }
597 }
598
599 if currency != "USD" && !matches!(priority, crate::filters::strategy::QueryPriority::Low) {
600 enhanced_query.push_str(&format!(" price {}", currency));
601 }
602
603 if !matches!(priority, crate::filters::strategy::QueryPriority::Low) {
605 match time_range {
606 "realtime" => enhanced_query.push_str(" live current real time"),
607 "day" => enhanced_query.push_str(" today daily"),
608 "week" => enhanced_query.push_str(" this week weekly"),
609 "month" => enhanced_query.push_str(" this month monthly"),
610 "year" => enhanced_query.push_str(" this year annual"),
611 _ => {}
612 }
613 }
614
615 let mut query_params = HashMap::new();
617 query_params.insert("q".to_string(), enhanced_query.clone());
618
619 let num_results = match priority {
621 crate::filters::strategy::QueryPriority::Critical => "20",
622 crate::filters::strategy::QueryPriority::High => "15",
623 crate::filters::strategy::QueryPriority::Medium => "10",
624 crate::filters::strategy::QueryPriority::Low => "5",
625 };
626 query_params.insert("num".to_string(), num_results.to_string());
627
628 let country_code = match market_region {
630 "india" => "in",
631 "us" => "us",
632 "europe" => "de", "asia" => "jp", _ => "us" };
636 query_params.insert("gl".to_string(), country_code.to_string());
637 query_params.insert("hl".to_string(), "en".to_string());
638
639 if time_range != "year" && !matches!(priority, crate::filters::strategy::QueryPriority::Low) {
641 let tbs_value = match time_range {
642 "realtime" | "day" => "qdr:d",
643 "week" => "qdr:w",
644 "month" => "qdr:m",
645 _ => ""
646 };
647 if !tbs_value.is_empty() {
648 query_params.insert("tbs".to_string(), tbs_value.to_string());
649 }
650 }
651
652 info!("🔍 Priority {} enhanced commodity search: {} using zone: {} (execution: {})",
653 format!("{:?}", priority), enhanced_query.clone(), zone.clone(), execution_id.clone());
654
655 let mut payload = json!({
656 "zone": zone,
657 "url": "https://www.google.com/search",
658 "format": "json",
659 "query_params": query_params,
660 "render": true,
661 "data_format": "markdown"
662 });
663
664 if std::env::var("TRUNCATE_FILTER")
666 .map(|v| v.to_lowercase() == "true")
667 .unwrap_or(false) {
668
669 payload["processing_priority"] = json!(format!("{:?}", priority));
670 payload["token_budget"] = json!(token_budget);
671 }
672
673 let client = Client::builder()
674 .timeout(Duration::from_secs(120))
675 .build()
676 .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
677
678 let response = client
679 .post(&format!("{}/request", base_url))
680 .header("Authorization", format!("Bearer {}", api_token))
681 .header("Content-Type", "application/json")
682 .json(&payload)
683 .send()
684 .await
685 .map_err(|e| BrightDataError::ToolError(format!("Commodity search request failed: {}", e)))?;
686
687 let status = response.status().as_u16();
688 let response_headers: HashMap<String, String> = response
689 .headers()
690 .iter()
691 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
692 .collect();
693
694 if let Err(e) = JSON_LOGGER.log_brightdata_request(
696 &format!("{}_{}", execution_id, sequence),
697 &zone,
698 &format!("Commodity Search: {}", enhanced_query),
699 payload.clone(),
700 status,
701 response_headers,
702 "markdown"
703 ).await {
704 log::warn!("Failed to log BrightData request: {}", e);
705 }
706
707 if !response.status().is_success() {
708 let error_text = response.text().await.unwrap_or_default();
709 return Err(BrightDataError::ToolError(format!(
710 "BrightData commodity search error {}: {}",
711 status, error_text
712 )));
713 }
714
715 let raw_content = response.text().await
716 .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
717
718 let filtered_content = if std::env::var("TRUNCATE_FILTER")
720 .map(|v| v.to_lowercase() == "true")
721 .unwrap_or(false) {
722
723 if ResponseFilter::is_error_page(&raw_content) {
724 return Err(BrightDataError::ToolError(format!("{} search returned error page", source_name)));
725 } else {
726 let max_tokens = token_budget / 3; ResponseFilter::extract_high_value_financial_data(&raw_content, max_tokens)
728 }
729 } else {
730 raw_content.clone()
731 };
732
733 Ok(json!({
734 "content": filtered_content,
735 "search_query": enhanced_query,
736 "commodity_type": commodity_type,
737 "market_region": market_region,
738 "time_range": time_range,
739 "currency": currency,
740 "priority": format!("{:?}", priority),
741 "token_budget": token_budget,
742 "execution_id": execution_id,
743 "sequence": sequence,
744 "success": true
745 }))
746 }
747}