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;
12
13pub struct CryptoDataTool;
14
15#[async_trait]
16impl Tool for CryptoDataTool {
17 fn name(&self) -> &str {
18 "get_crypto_data"
19 }
20
21 fn description(&self) -> &str {
22 "Get cryptocurrency data with enhanced search parameters including prices, market cap, trading volumes with intelligent filtering"
23 }
24
25 fn input_schema(&self) -> Value {
26 json!({
27 "type": "object",
28 "properties": {
29 "query": {
30 "type": "string",
31 "description": "Crypto symbol (BTC, ETH, ADA), crypto name (Bitcoin, Ethereum), comparison query (BTC vs ETH), or market overview (crypto market today, top cryptocurrencies)"
32 },
33 "page": {
34 "type": "integer",
35 "description": "Page number for pagination (1-based)",
36 "minimum": 1,
37 "default": 1
38 },
39 "num_results": {
40 "type": "integer",
41 "description": "Number of results per page (5-50)",
42 "minimum": 5,
43 "maximum": 50,
44 "default": 20
45 },
46 "time_filter": {
47 "type": "string",
48 "enum": ["any", "hour", "day", "week", "month", "year"],
49 "description": "Time-based filter for crypto data",
50 "default": "day"
51 },
52 "crypto_type": {
53 "type": "string",
54 "enum": ["any", "bitcoin", "altcoins", "defi", "nft", "stablecoins"],
55 "description": "Type of cryptocurrency to focus on",
56 "default": "any"
57 },
58 "data_points": {
59 "type": "array",
60 "items": {
61 "type": "string",
62 "enum": ["price", "market_cap", "volume", "change", "supply", "dominance", "fear_greed"]
63 },
64 "description": "Specific data points to focus on",
65 "default": ["price", "market_cap", "volume"]
66 },
67 "safe_search": {
68 "type": "string",
69 "enum": ["off", "moderate", "strict"],
70 "description": "Safe search filter level",
71 "default": "moderate"
72 },
73 "use_serp_api": {
74 "type": "boolean",
75 "description": "Use enhanced SERP API with advanced parameters",
76 "default": true
77 }
78 },
79 "required": ["query"]
80 })
81 }
82
83 async fn execute_internal(&self, parameters: Value) -> Result<ToolResult, BrightDataError> {
89 let query = parameters
90 .get("query")
91 .and_then(|v| v.as_str())
92 .ok_or_else(|| BrightDataError::ToolError("Missing 'query' parameter".into()))?;
93
94 let page = parameters
95 .get("page")
96 .and_then(|v| v.as_i64())
97 .unwrap_or(1) as u32;
98
99 let num_results = parameters
100 .get("num_results")
101 .and_then(|v| v.as_i64())
102 .unwrap_or(20) as u32;
103
104 let time_filter = parameters
105 .get("time_filter")
106 .and_then(|v| v.as_str())
107 .unwrap_or("day");
108
109 let crypto_type = parameters
110 .get("crypto_type")
111 .and_then(|v| v.as_str())
112 .unwrap_or("any");
113
114 let data_points = parameters
115 .get("data_points")
116 .and_then(|v| v.as_array())
117 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
118 .unwrap_or_else(|| vec!["price", "market_cap", "volume"]);
119
120 let safe_search = parameters
121 .get("safe_search")
122 .and_then(|v| v.as_str())
123 .unwrap_or("moderate");
124
125 let use_serp_api = parameters
126 .get("use_serp_api")
127 .and_then(|v| v.as_bool())
128 .unwrap_or(true);
129
130 let query_priority = ResponseStrategy::classify_query_priority(query);
132 let recommended_tokens = ResponseStrategy::get_recommended_token_allocation(query);
133
134 if std::env::var("TRUNCATE_FILTER")
136 .map(|v| v.to_lowercase() == "true")
137 .unwrap_or(false) {
138
139 let (_, remaining_tokens) = ResponseStrategy::get_token_budget_status();
141 if remaining_tokens < 150 && !matches!(query_priority, crate::filters::strategy::QueryPriority::Critical) {
142 return Ok(ResponseStrategy::create_response("", query, "crypto", "budget_limit", json!({}), ResponseType::Skip));
143 }
144 }
145
146 let execution_id = format!("crypto_{}", chrono::Utc::now().format("%Y%m%d_%H%M%S%.3f"));
147
148 let result = if use_serp_api {
149 self.fetch_crypto_data_enhanced_with_priority(
150 query, page, num_results, time_filter, crypto_type, &data_points,
151 safe_search, query_priority, recommended_tokens, &execution_id
152 ).await?
153 } else {
154 self.fetch_crypto_data_legacy_with_priority(query, query_priority, recommended_tokens, &execution_id).await?
155 };
156
157 let content = result.get("content").and_then(|c| c.as_str()).unwrap_or("");
158 let source_used = if use_serp_api { "Enhanced SERP" } else { "Legacy" };
159
160 let tool_result = if std::env::var("TRUNCATE_FILTER")
162 .map(|v| v.to_lowercase() == "true")
163 .unwrap_or(false) {
164
165 ResponseStrategy::create_financial_response(
166 "crypto", query, "crypto", source_used, content, result.clone()
167 )
168 } else {
169 let content_text = if use_serp_api {
171 result.get("formatted_content").and_then(|c| c.as_str()).unwrap_or(content)
172 } else {
173 content
174 };
175
176 let mcp_content = vec![McpContent::text(format!(
177 "💰 **Enhanced Crypto Data for {}**\n\nCrypto Type: {} | Priority: {:?} | Tokens: {} | Data Points: {:?}\nPage: {} | Results: {} | Time Filter: {} | Safe Search: {}\nExecution ID: {}\n\n{}",
178 query, crypto_type, query_priority, recommended_tokens, data_points, page, num_results, time_filter, safe_search, execution_id, content_text
179 ))];
180 ToolResult::success_with_raw(mcp_content, result)
181 };
182
183 if std::env::var("TRUNCATE_FILTER")
185 .map(|v| v.to_lowercase() == "true")
186 .unwrap_or(false) {
187 Ok(ResponseStrategy::apply_size_limits(tool_result))
188 } else {
189 Ok(tool_result)
190 }
191 }
192}
193
194impl CryptoDataTool {
195 async fn fetch_crypto_data_enhanced_with_priority(
197 &self,
198 query: &str,
199 page: u32,
200 num_results: u32,
201 time_filter: &str,
202 crypto_type: &str,
203 data_points: &[&str],
204 safe_search: &str,
205 query_priority: crate::filters::strategy::QueryPriority,
206 token_budget: usize,
207 execution_id: &str
208 ) -> Result<Value, BrightDataError> {
209 let api_token = env::var("BRIGHTDATA_API_TOKEN")
210 .or_else(|_| env::var("API_TOKEN"))
211 .map_err(|_| BrightDataError::ToolError("Missing BRIGHTDATA_API_TOKEN".into()))?;
212
213 let base_url = env::var("BRIGHTDATA_BASE_URL")
214 .unwrap_or_else(|_| "https://api.brightdata.com".to_string());
215
216 let zone = env::var("BRIGHTDATA_SERP_ZONE")
217 .unwrap_or_else(|_| "serp_api2".to_string());
218
219 let search_query = self.build_priority_aware_crypto_query(query, crypto_type, data_points, query_priority);
221
222 let effective_num_results = match query_priority {
224 crate::filters::strategy::QueryPriority::Critical => num_results,
225 crate::filters::strategy::QueryPriority::High => std::cmp::min(num_results, 25),
226 crate::filters::strategy::QueryPriority::Medium => std::cmp::min(num_results, 15),
227 crate::filters::strategy::QueryPriority::Low => std::cmp::min(num_results, 10),
228 };
229
230 let mut query_params = HashMap::new();
232 query_params.insert("q".to_string(), search_query.clone());
233
234 if page > 1 {
236 let start = (page - 1) * effective_num_results;
237 query_params.insert("start".to_string(), start.to_string());
238 }
239 query_params.insert("num".to_string(), effective_num_results.to_string());
240
241 query_params.insert("gl".to_string(), "us".to_string());
243 query_params.insert("hl".to_string(), "en".to_string());
244
245 let safe_value = match safe_search {
247 "off" => "off",
248 "strict" => "strict",
249 _ => "moderate"
250 };
251 query_params.insert("safe".to_string(), safe_value.to_string());
252
253 if time_filter != "any" && !matches!(query_priority, crate::filters::strategy::QueryPriority::Low) {
255 let tbs_value = match time_filter {
256 "hour" => "qdr:h",
257 "day" => "qdr:d",
258 "week" => "qdr:w",
259 "month" => "qdr:m",
260 "year" => "qdr:y",
261 _ => ""
262 };
263 if !tbs_value.is_empty() {
264 query_params.insert("tbs".to_string(), tbs_value.to_string());
265 }
266 }
267
268 let mut search_url = "https://www.google.com/search".to_string();
270 let query_string = query_params.iter()
271 .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
272 .collect::<Vec<_>>()
273 .join("&");
274
275 if !query_string.is_empty() {
276 search_url = format!("{}?{}", search_url, query_string);
277 }
278
279 let mut payload = json!({
280 "url": search_url,
281 "zone": zone,
282 "format": "raw",
283 "render": true,
284 "data_format": "markdown"
285 });
286
287 if std::env::var("TRUNCATE_FILTER")
289 .map(|v| v.to_lowercase() == "true")
290 .unwrap_or(false) {
291
292 payload["processing_priority"] = json!(format!("{:?}", query_priority));
293 payload["token_budget"] = json!(token_budget);
294 payload["focus_data_points"] = json!(data_points);
295 payload["crypto_focus"] = json!(crypto_type);
296 }
297
298 let client = Client::builder()
299 .timeout(Duration::from_secs(120))
300 .build()
301 .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
302
303 let response = client
304 .post(&format!("{}/request", base_url))
305 .header("Authorization", format!("Bearer {}", api_token))
306 .header("Content-Type", "application/json")
307 .json(&payload)
308 .send()
309 .await
310 .map_err(|e| BrightDataError::ToolError(format!("Enhanced crypto request failed: {}", e)))?;
311
312 let status = response.status().as_u16();
313 let response_headers: HashMap<String, String> = response
314 .headers()
315 .iter()
316 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
317 .collect();
318
319 if let Err(e) = JSON_LOGGER.log_brightdata_request(
321 execution_id,
322 &zone,
323 &format!("Enhanced Crypto: {} ({})", search_query, crypto_type),
324 payload.clone(),
325 status,
326 response_headers,
327 "markdown"
328 ).await {
329 log::warn!("Failed to log BrightData request: {}", e);
330 }
331
332 if !response.status().is_success() {
333 let error_text = response.text().await.unwrap_or_default();
334 return Err(BrightDataError::ToolError(format!(
335 "BrightData enhanced crypto error {}: {}",
336 status, error_text
337 )));
338 }
339
340 let raw_content = response.text().await
341 .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
342
343 let filtered_content = if std::env::var("TRUNCATE_FILTER")
345 .map(|v| v.to_lowercase() == "true")
346 .unwrap_or(false) {
347
348 if ResponseFilter::is_error_page(&raw_content) {
349 return Err(BrightDataError::ToolError("Enhanced crypto search returned error page".into()));
350 } else if ResponseStrategy::should_try_next_source(&raw_content) {
351 return Err(BrightDataError::ToolError("Content quality too low".into()));
352 } else {
353 let max_tokens = token_budget / 3; ResponseFilter::extract_high_value_financial_data(&raw_content, max_tokens)
356 }
357 } else {
358 raw_content.clone()
359 };
360
361 let formatted_content = self.format_crypto_results_with_priority(&filtered_content, query, crypto_type, data_points, page, effective_num_results, time_filter, query_priority);
363
364 Ok(json!({
365 "content": filtered_content,
366 "formatted_content": formatted_content,
367 "query": query,
368 "search_query": search_query,
369 "crypto_type": crypto_type,
370 "data_points": data_points,
371 "priority": format!("{:?}", query_priority),
372 "token_budget": token_budget,
373 "page": page,
374 "num_results": effective_num_results,
375 "time_filter": time_filter,
376 "safe_search": safe_search,
377 "zone": zone,
378 "execution_id": execution_id,
379 "raw_response": raw_content,
380 "success": true,
381 "api_type": "enhanced_priority_serp"
382 }))
383 }
384
385 async fn fetch_crypto_data_legacy_with_priority(&self, query: &str, priority: crate::filters::strategy::QueryPriority, token_budget: usize, execution_id: &str) -> Result<Value, BrightDataError> {
386 let api_token = env::var("BRIGHTDATA_API_TOKEN")
387 .or_else(|_| env::var("API_TOKEN"))
388 .map_err(|_| BrightDataError::ToolError("Missing BRIGHTDATA_API_TOKEN".into()))?;
389
390 let base_url = env::var("BRIGHTDATA_BASE_URL")
391 .unwrap_or_else(|_| "https://api.brightdata.com".to_string());
392
393 let zone = env::var("WEB_UNLOCKER_ZONE")
394 .unwrap_or_else(|_| "default".to_string());
395
396 let search_url = format!(
397 "https://www.google.com/search?q={} cryptocurrency price market cap coinmarketcap",
398 urlencoding::encode(query)
399 );
400
401 let mut payload = json!({
402 "url": search_url,
403 "zone": zone,
404 "format": "raw",
405 "data_format": "markdown"
406 });
407
408 if std::env::var("TRUNCATE_FILTER")
410 .map(|v| v.to_lowercase() == "true")
411 .unwrap_or(false) {
412
413 payload["processing_priority"] = json!(format!("{:?}", priority));
414 payload["token_budget"] = json!(token_budget);
415 }
416
417 let client = Client::builder()
418 .timeout(Duration::from_secs(120))
419 .build()
420 .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
421
422 let response = client
423 .post(&format!("{}/request", base_url))
424 .header("Authorization", format!("Bearer {}", api_token))
425 .header("Content-Type", "application/json")
426 .json(&payload)
427 .send()
428 .await
429 .map_err(|e| BrightDataError::ToolError(format!("Crypto data request failed: {}", e)))?;
430
431 let status = response.status().as_u16();
432 let response_headers: HashMap<String, String> = response
433 .headers()
434 .iter()
435 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
436 .collect();
437
438 if let Err(e) = JSON_LOGGER.log_brightdata_request(
440 execution_id,
441 &zone,
442 &search_url,
443 payload.clone(),
444 status,
445 response_headers,
446 "markdown"
447 ).await {
448 log::warn!("Failed to log BrightData request: {}", e);
449 }
450
451 if !response.status().is_success() {
452 let error_text = response.text().await.unwrap_or_default();
453 return Err(BrightDataError::ToolError(format!(
454 "BrightData crypto data error {}: {}",
455 status, error_text
456 )));
457 }
458
459 let raw_content = response.text().await
460 .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
461
462 let filtered_content = if std::env::var("TRUNCATE_FILTER")
464 .map(|v| v.to_lowercase() == "true")
465 .unwrap_or(false) {
466
467 if ResponseFilter::is_error_page(&raw_content) {
468 return Err(BrightDataError::ToolError("Crypto search returned error page".into()));
469 } else {
470 let max_tokens = token_budget / 2;
471 ResponseFilter::extract_high_value_financial_data(&raw_content, max_tokens)
472 }
473 } else {
474 raw_content.clone()
475 };
476
477 Ok(json!({
478 "content": filtered_content,
479 "query": query,
480 "priority": format!("{:?}", priority),
481 "token_budget": token_budget,
482 "execution_id": execution_id,
483 "success": true,
484 "api_type": "legacy_priority"
485 }))
486 }
487
488 fn build_priority_aware_crypto_query(&self, query: &str, crypto_type: &str, data_points: &[&str], priority: crate::filters::strategy::QueryPriority) -> String {
490 let mut search_terms = vec![query.to_string()];
491
492 search_terms.push("cryptocurrency".to_string());
494
495 match priority {
497 crate::filters::strategy::QueryPriority::Critical => {
498 search_terms.extend_from_slice(&["price".to_string(), "current".to_string(), "live".to_string()]);
500 for data_point in data_points.iter().take(2) { match *data_point {
502 "price" => search_terms.push("price".to_string()),
503 "market_cap" => search_terms.push("market cap".to_string()),
504 _ => {}
505 }
506 }
507 }
508 crate::filters::strategy::QueryPriority::High => {
509 search_terms.extend_from_slice(&["price".to_string(), "market cap".to_string()]);
511 for data_point in data_points.iter().take(3) {
512 match *data_point {
513 "volume" => search_terms.push("volume".to_string()),
514 "change" => search_terms.push("change".to_string()),
515 _ => {}
516 }
517 }
518 }
519 crate::filters::strategy::QueryPriority::Medium => {
520 search_terms.extend_from_slice(&["price".to_string(), "market".to_string()]);
522 if crypto_type != "any" {
523 search_terms.push(crypto_type.to_string());
524 }
525 }
526 crate::filters::strategy::QueryPriority::Low => {
527 if crypto_type != "any" {
529 search_terms.push(crypto_type.to_string());
530 }
531 search_terms.push("overview".to_string());
532 }
533 }
534
535 if crypto_type != "any" && !matches!(priority, crate::filters::strategy::QueryPriority::Low) {
537 match crypto_type {
538 "bitcoin" => search_terms.push("bitcoin BTC".to_string()),
539 "altcoins" => search_terms.push("altcoins ethereum".to_string()),
540 "defi" => search_terms.push("DeFi decentralized finance".to_string()),
541 "nft" => search_terms.push("NFT non-fungible token".to_string()),
542 "stablecoins" => search_terms.push("stablecoin USDT USDC".to_string()),
543 _ => {}
544 }
545 }
546
547 if matches!(priority, crate::filters::strategy::QueryPriority::Critical | crate::filters::strategy::QueryPriority::High) {
549 search_terms.extend_from_slice(&[
550 "coinmarketcap".to_string(),
551 "coingecko".to_string()
552 ]);
553 }
554
555 search_terms.join(" ")
556 }
557
558 fn format_crypto_results_with_priority(&self, content: &str, query: &str, crypto_type: &str, data_points: &[&str], page: u32, num_results: u32, time_filter: &str, _priority: crate::filters::strategy::QueryPriority) -> String {
560 if std::env::var("TRUNCATE_FILTER")
562 .map(|v| v.to_lowercase() == "true")
563 .unwrap_or(false) {
564
565 return format!("💰 {}: {}",
567 ResponseStrategy::ultra_abbreviate_query(query),
568 content
569 );
570 }
571
572 self.format_crypto_results(content, query, crypto_type, data_points, page, num_results, time_filter)
574 }
575
576 fn format_crypto_results(&self, content: &str, query: &str, crypto_type: &str, data_points: &[&str], page: u32, num_results: u32, time_filter: &str) -> String {
577 let mut formatted = String::new();
578
579 formatted.push_str(&format!("# Cryptocurrency Data: {}\n\n", query));
581 formatted.push_str(&format!("**Crypto Type**: {} | **Data Points**: {:?} | **Time Filter**: {}\n",
582 crypto_type, data_points, time_filter));
583 formatted.push_str(&format!("**Page**: {} | **Results**: {}\n\n", page, num_results));
584
585 if let Ok(json_data) = serde_json::from_str::<Value>(content) {
587 if let Some(results) = json_data.get("organic_results").and_then(|r| r.as_array()) {
589 formatted.push_str("## Cryptocurrency Information\n\n");
590 for (i, result) in results.iter().take(num_results as usize).enumerate() {
591 let title = result.get("title").and_then(|t| t.as_str()).unwrap_or("No title");
592 let link = result.get("link").and_then(|l| l.as_str()).unwrap_or("");
593 let snippet = result.get("snippet").and_then(|s| s.as_str()).unwrap_or("");
594
595 formatted.push_str(&format!("### {}. {}\n", i + 1, title));
596 if !link.is_empty() {
597 formatted.push_str(&format!("**Source**: {}\n", link));
598 }
599 if !snippet.is_empty() {
600 formatted.push_str(&format!("**Details**: {}\n", snippet));
601 }
602
603 self.highlight_crypto_data_points(&mut formatted, snippet, data_points);
605 formatted.push_str("\n");
606 }
607 } else {
608 formatted.push_str("## Crypto Data\n\n");
610 formatted.push_str("```json\n");
611 formatted.push_str(&serde_json::to_string_pretty(&json_data).unwrap_or_else(|_| content.to_string()));
612 formatted.push_str("\n```\n");
613 }
614 } else {
615 formatted.push_str("## Cryptocurrency Information\n\n");
617 formatted.push_str(content);
618 }
619
620 if page > 1 || num_results < 100 {
622 formatted.push_str(&format!("\n---\n*Page {} of cryptocurrency results*\n", page));
623 if page > 1 {
624 formatted.push_str("💡 *To get more results, use page parameter*\n");
625 }
626 }
627
628 formatted
629 }
630
631 fn highlight_crypto_data_points(&self, formatted: &mut String, snippet: &str, data_points: &[&str]) {
632 let snippet_lower = snippet.to_lowercase();
633 let mut found_points = Vec::new();
634
635 for data_point in data_points {
636 match *data_point {
637 "price" if snippet_lower.contains("price") || snippet_lower.contains("$") || snippet_lower.contains("usd") => {
638 found_points.push("💰 Price data detected");
639 }
640 "market_cap" if snippet_lower.contains("market cap") || snippet_lower.contains("mcap") => {
641 found_points.push("📊 Market cap information");
642 }
643 "volume" if snippet_lower.contains("volume") || snippet_lower.contains("traded") => {
644 found_points.push("📈 Volume data found");
645 }
646 "change" if snippet_lower.contains("%") || snippet_lower.contains("change") || snippet_lower.contains("gain") => {
647 found_points.push("📊 Price change information");
648 }
649 "supply" if snippet_lower.contains("supply") || snippet_lower.contains("circulation") => {
650 found_points.push("🪙 Supply data available");
651 }
652 "dominance" if snippet_lower.contains("dominance") => {
653 found_points.push("👑 Market dominance data");
654 }
655 "fear_greed" if snippet_lower.contains("fear") || snippet_lower.contains("greed") => {
656 found_points.push("😰 Fear & Greed index");
657 }
658 _ => {}
659 }
660 }
661
662 if !found_points.is_empty() {
663 formatted.push_str("**Key Data Points**: ");
664 formatted.push_str(&found_points.join(" | "));
665 formatted.push_str("\n");
666 }
667 }
668}