Spaces:
Paused
Paused
| #!/usr/bin/env python3 | |
| """ | |
| Comprehensive Test Suite for Web Tools Module | |
| This script tests all web tools functionality to ensure they work correctly. | |
| Run this after any updates to the web_tools.py module or backend libraries. | |
| Usage: | |
| python test_web_tools.py # Run all tests | |
| python test_web_tools.py --no-llm # Skip LLM processing tests | |
| python test_web_tools.py --verbose # Show detailed output | |
| Requirements: | |
| - PARALLEL_API_KEY or FIRECRAWL_API_KEY environment variable must be set | |
| - An auxiliary LLM provider (OPENROUTER_API_KEY or Nous Portal auth) (optional, for LLM tests) | |
| """ | |
| import pytest | |
| pytestmark = pytest.mark.integration | |
| import json | |
| import asyncio | |
| import sys | |
| import os | |
| import argparse | |
| from datetime import datetime | |
| from typing import List | |
| # Import the web tools to test (updated path after moving tools/) | |
| from tools.web_tools import ( | |
| web_search_tool, | |
| web_extract_tool, | |
| web_crawl_tool, | |
| check_firecrawl_api_key, | |
| check_web_api_key, | |
| check_auxiliary_model, | |
| get_debug_session_info, | |
| _get_backend, | |
| ) | |
| class Colors: | |
| """ANSI color codes for terminal output""" | |
| HEADER = '\033[95m' | |
| BLUE = '\033[94m' | |
| CYAN = '\033[96m' | |
| GREEN = '\033[92m' | |
| WARNING = '\033[93m' | |
| FAIL = '\033[91m' | |
| ENDC = '\033[0m' | |
| BOLD = '\033[1m' | |
| UNDERLINE = '\033[4m' | |
| def print_header(text: str): | |
| """Print a formatted header""" | |
| print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.ENDC}") | |
| print(f"{Colors.HEADER}{Colors.BOLD}{text}{Colors.ENDC}") | |
| print(f"{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.ENDC}") | |
| def print_section(text: str): | |
| """Print a formatted section header""" | |
| print(f"\n{Colors.CYAN}{Colors.BOLD}📌 {text}{Colors.ENDC}") | |
| print(f"{Colors.CYAN}{'-'*50}{Colors.ENDC}") | |
| def print_success(text: str): | |
| """Print success message""" | |
| print(f"{Colors.GREEN}✅ {text}{Colors.ENDC}") | |
| def print_error(text: str): | |
| """Print error message""" | |
| print(f"{Colors.FAIL}❌ {text}{Colors.ENDC}") | |
| def print_warning(text: str): | |
| """Print warning message""" | |
| print(f"{Colors.WARNING}⚠️ {text}{Colors.ENDC}") | |
| def print_info(text: str, indent: int = 0): | |
| """Print info message""" | |
| indent_str = " " * indent | |
| print(f"{indent_str}{Colors.BLUE}ℹ️ {text}{Colors.ENDC}") | |
| class WebToolsTester: | |
| """Test suite for web tools""" | |
| def __init__(self, verbose: bool = False, test_llm: bool = True): | |
| self.verbose = verbose | |
| self.test_llm = test_llm | |
| self.test_results = { | |
| "passed": [], | |
| "failed": [], | |
| "skipped": [] | |
| } | |
| self.start_time = None | |
| self.end_time = None | |
| def log_result(self, test_name: str, status: str, details: str = ""): | |
| """Log test result""" | |
| result = { | |
| "test": test_name, | |
| "status": status, | |
| "details": details, | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| if status == "passed": | |
| self.test_results["passed"].append(result) | |
| print_success(f"{test_name}: {details}" if details else test_name) | |
| elif status == "failed": | |
| self.test_results["failed"].append(result) | |
| print_error(f"{test_name}: {details}" if details else test_name) | |
| elif status == "skipped": | |
| self.test_results["skipped"].append(result) | |
| print_warning(f"{test_name} skipped: {details}" if details else f"{test_name} skipped") | |
| def test_environment(self) -> bool: | |
| """Test environment setup and API keys""" | |
| print_section("Environment Check") | |
| # Check web backend API key (Parallel or Firecrawl) | |
| if not check_web_api_key(): | |
| self.log_result("Web Backend API Key", "failed", "PARALLEL_API_KEY or FIRECRAWL_API_KEY not set") | |
| return False | |
| else: | |
| backend = _get_backend() | |
| self.log_result("Web Backend API Key", "passed", f"Using {backend} backend") | |
| # Check auxiliary LLM provider (optional) | |
| if not check_auxiliary_model(): | |
| self.log_result("Auxiliary LLM", "skipped", "No auxiliary LLM provider available (LLM tests will be skipped)") | |
| self.test_llm = False | |
| else: | |
| self.log_result("Auxiliary LLM", "passed", "Found") | |
| # Check debug mode | |
| debug_info = get_debug_session_info() | |
| if debug_info["enabled"]: | |
| print_info(f"Debug mode enabled - Session: {debug_info['session_id']}") | |
| print_info(f"Debug log: {debug_info['log_path']}") | |
| return True | |
| def test_web_search(self) -> List[str]: | |
| """Test web search functionality""" | |
| print_section("Test 1: Web Search") | |
| test_queries = [ | |
| ("Python web scraping tutorial", 5), | |
| ("Firecrawl API documentation", 3), | |
| ("inflammatory arthritis symptoms treatment", 8) # Test medical query from your example | |
| ] | |
| extracted_urls = [] | |
| for query, limit in test_queries: | |
| try: | |
| print(f"\n Testing search: '{query}' (limit={limit})") | |
| if self.verbose: | |
| print(f" Calling web_search_tool(query='{query}', limit={limit})") | |
| # Perform search | |
| result = web_search_tool(query, limit) | |
| # Parse result | |
| try: | |
| data = json.loads(result) | |
| except json.JSONDecodeError as e: | |
| self.log_result(f"Search: {query[:30]}...", "failed", f"Invalid JSON: {e}") | |
| if self.verbose: | |
| print(f" Raw response (first 500 chars): {result[:500]}...") | |
| continue | |
| if "error" in data: | |
| self.log_result(f"Search: {query[:30]}...", "failed", f"API error: {data['error']}") | |
| continue | |
| # Check structure | |
| if "success" not in data or "data" not in data: | |
| self.log_result(f"Search: {query[:30]}...", "failed", "Missing success or data fields") | |
| if self.verbose: | |
| print(f" Response keys: {list(data.keys())}") | |
| continue | |
| web_results = data.get("data", {}).get("web", []) | |
| if not web_results: | |
| self.log_result(f"Search: {query[:30]}...", "failed", "Empty web results array") | |
| if self.verbose: | |
| print(f" data.web content: {data.get('data', {}).get('web')}") | |
| continue | |
| # Validate each result | |
| valid_results = 0 | |
| missing_fields = [] | |
| for i, result in enumerate(web_results): | |
| required_fields = ["url", "title", "description"] | |
| has_all_fields = all(key in result for key in required_fields) | |
| if has_all_fields: | |
| valid_results += 1 | |
| # Collect URLs for extraction test | |
| if len(extracted_urls) < 3: | |
| extracted_urls.append(result["url"]) | |
| if self.verbose: | |
| print(f" Result {i+1}: ✓ {result['title'][:50]}...") | |
| print(f" URL: {result['url'][:60]}...") | |
| else: | |
| missing = [f for f in required_fields if f not in result] | |
| missing_fields.append(f"Result {i+1} missing: {missing}") | |
| if self.verbose: | |
| print(f" Result {i+1}: ✗ Missing fields: {missing}") | |
| # Log results | |
| if valid_results == len(web_results): | |
| self.log_result( | |
| f"Search: {query[:30]}...", | |
| "passed", | |
| f"All {valid_results} results valid" | |
| ) | |
| else: | |
| self.log_result( | |
| f"Search: {query[:30]}...", | |
| "failed", | |
| f"Only {valid_results}/{len(web_results)} valid. Issues: {'; '.join(missing_fields[:3])}" | |
| ) | |
| except Exception as e: | |
| self.log_result(f"Search: {query[:30]}...", "failed", f"Exception: {type(e).__name__}: {str(e)}") | |
| if self.verbose: | |
| import traceback | |
| print(f" Traceback: {traceback.format_exc()}") | |
| if self.verbose and extracted_urls: | |
| print(f"\n URLs collected for extraction test: {len(extracted_urls)}") | |
| for url in extracted_urls: | |
| print(f" - {url}") | |
| return extracted_urls | |
| async def test_web_extract(self, urls: List[str] = None): | |
| """Test web content extraction""" | |
| print_section("Test 2: Web Extract (without LLM)") | |
| # Use provided URLs or defaults | |
| if not urls: | |
| urls = [ | |
| "https://docs.firecrawl.dev/introduction", | |
| "https://www.python.org/about/" | |
| ] | |
| print(f" Using default URLs for testing") | |
| else: | |
| print(f" Using {len(urls)} URLs from search results") | |
| # Test extraction | |
| if urls: | |
| try: | |
| test_urls = urls[:2] # Test with max 2 URLs | |
| print(f"\n Extracting content from {len(test_urls)} URL(s)...") | |
| for url in test_urls: | |
| print(f" - {url}") | |
| if self.verbose: | |
| print(f" Calling web_extract_tool(urls={test_urls}, format='markdown', use_llm_processing=False)") | |
| result = await web_extract_tool( | |
| test_urls, | |
| format="markdown", | |
| use_llm_processing=False | |
| ) | |
| # Parse result | |
| try: | |
| data = json.loads(result) | |
| except json.JSONDecodeError as e: | |
| self.log_result("Extract (no LLM)", "failed", f"Invalid JSON: {e}") | |
| if self.verbose: | |
| print(f" Raw response (first 500 chars): {result[:500]}...") | |
| return | |
| if "error" in data: | |
| self.log_result("Extract (no LLM)", "failed", f"API error: {data['error']}") | |
| return | |
| results = data.get("results", []) | |
| if not results: | |
| self.log_result("Extract (no LLM)", "failed", "No results in response") | |
| if self.verbose: | |
| print(f" Response keys: {list(data.keys())}") | |
| return | |
| # Validate each result | |
| valid_results = 0 | |
| failed_results = 0 | |
| total_content_length = 0 | |
| extraction_details = [] | |
| for i, result in enumerate(results): | |
| title = result.get("title", "No title") | |
| content = result.get("content", "") | |
| error = result.get("error") | |
| if error: | |
| failed_results += 1 | |
| extraction_details.append(f"Page {i+1}: ERROR - {error}") | |
| if self.verbose: | |
| print(f" Page {i+1}: ✗ Error - {error}") | |
| elif content: | |
| content_len = len(content) | |
| total_content_length += content_len | |
| valid_results += 1 | |
| extraction_details.append(f"Page {i+1}: {title[:40]}... ({content_len} chars)") | |
| if self.verbose: | |
| print(f" Page {i+1}: ✓ {title[:50]}... - {content_len} characters") | |
| print(f" First 100 chars: {content[:100]}...") | |
| else: | |
| extraction_details.append(f"Page {i+1}: {title[:40]}... (EMPTY)") | |
| if self.verbose: | |
| print(f" Page {i+1}: ⚠ {title[:50]}... - Empty content") | |
| # Log results | |
| if valid_results > 0: | |
| self.log_result( | |
| "Extract (no LLM)", | |
| "passed", | |
| f"{valid_results}/{len(results)} pages extracted, {total_content_length} total chars" | |
| ) | |
| else: | |
| self.log_result( | |
| "Extract (no LLM)", | |
| "failed", | |
| f"No valid content. {failed_results} errors, {len(results) - failed_results} empty" | |
| ) | |
| if self.verbose: | |
| print(f"\n Extraction details:") | |
| for detail in extraction_details: | |
| print(f" {detail}") | |
| except Exception as e: | |
| self.log_result("Extract (no LLM)", "failed", f"Exception: {type(e).__name__}: {str(e)}") | |
| if self.verbose: | |
| import traceback | |
| print(f" Traceback: {traceback.format_exc()}") | |
| async def test_web_extract_with_llm(self, urls: List[str] = None): | |
| """Test web extraction with LLM processing""" | |
| print_section("Test 3: Web Extract (with Gemini LLM)") | |
| if not self.test_llm: | |
| self.log_result("Extract (with LLM)", "skipped", "LLM testing disabled") | |
| return | |
| # Use a URL likely to have substantial content | |
| test_url = urls[0] if urls else "https://docs.firecrawl.dev/features/scrape" | |
| try: | |
| print(f"\n Extracting and processing: {test_url}") | |
| result = await web_extract_tool( | |
| [test_url], | |
| format="markdown", | |
| use_llm_processing=True, | |
| min_length=1000 # Lower threshold for testing | |
| ) | |
| data = json.loads(result) | |
| if "error" in data: | |
| self.log_result("Extract (with LLM)", "failed", data["error"]) | |
| return | |
| results = data.get("results", []) | |
| if not results: | |
| self.log_result("Extract (with LLM)", "failed", "No results returned") | |
| return | |
| result = results[0] | |
| content = result.get("content", "") | |
| if content: | |
| content_len = len(content) | |
| # Check if content was actually processed (should be shorter than typical raw content) | |
| if content_len > 0: | |
| self.log_result( | |
| "Extract (with LLM)", | |
| "passed", | |
| f"Content processed: {content_len} chars" | |
| ) | |
| if self.verbose: | |
| print(f"\n First 300 chars of processed content:") | |
| print(f" {content[:300]}...") | |
| else: | |
| self.log_result("Extract (with LLM)", "failed", "No content after processing") | |
| else: | |
| self.log_result("Extract (with LLM)", "failed", "No content field in result") | |
| except json.JSONDecodeError as e: | |
| self.log_result("Extract (with LLM)", "failed", f"Invalid JSON: {e}") | |
| except Exception as e: | |
| self.log_result("Extract (with LLM)", "failed", str(e)) | |
| async def test_web_crawl(self): | |
| """Test web crawling functionality""" | |
| print_section("Test 4: Web Crawl") | |
| test_sites = [ | |
| ("https://docs.firecrawl.dev", None, 2), # Test docs site | |
| ("https://firecrawl.dev", None, 3), # Test main site | |
| ] | |
| for url, instructions, expected_min_pages in test_sites: | |
| try: | |
| print(f"\n Testing crawl of: {url}") | |
| if instructions: | |
| print(f" Instructions: {instructions}") | |
| else: | |
| print(f" No instructions (general crawl)") | |
| print(f" Expected minimum pages: {expected_min_pages}") | |
| # Show what's being called | |
| if self.verbose: | |
| print(f" Calling web_crawl_tool(url='{url}', instructions={instructions}, use_llm_processing=False)") | |
| result = await web_crawl_tool( | |
| url, | |
| instructions=instructions, | |
| use_llm_processing=False # Disable LLM for faster testing | |
| ) | |
| # Check if result is valid JSON | |
| try: | |
| data = json.loads(result) | |
| except json.JSONDecodeError as e: | |
| self.log_result(f"Crawl: {url}", "failed", f"Invalid JSON response: {e}") | |
| if self.verbose: | |
| print(f" Raw response (first 500 chars): {result[:500]}...") | |
| continue | |
| # Check for errors | |
| if "error" in data: | |
| self.log_result(f"Crawl: {url}", "failed", f"API error: {data['error']}") | |
| continue | |
| # Get results | |
| results = data.get("results", []) | |
| if not results: | |
| self.log_result(f"Crawl: {url}", "failed", "No pages in results array") | |
| if self.verbose: | |
| print(f" Full response: {json.dumps(data, indent=2)[:1000]}...") | |
| continue | |
| # Analyze pages | |
| valid_pages = 0 | |
| empty_pages = 0 | |
| total_content = 0 | |
| page_details = [] | |
| for i, page in enumerate(results): | |
| content = page.get("content", "") | |
| title = page.get("title", "Untitled") | |
| error = page.get("error") | |
| if error: | |
| page_details.append(f"Page {i+1}: ERROR - {error}") | |
| elif content: | |
| valid_pages += 1 | |
| content_len = len(content) | |
| total_content += content_len | |
| page_details.append(f"Page {i+1}: {title[:40]}... ({content_len} chars)") | |
| else: | |
| empty_pages += 1 | |
| page_details.append(f"Page {i+1}: {title[:40]}... (EMPTY)") | |
| # Show detailed results if verbose | |
| if self.verbose: | |
| print(f"\n Crawl Results:") | |
| print(f" Total pages returned: {len(results)}") | |
| print(f" Valid pages (with content): {valid_pages}") | |
| print(f" Empty pages: {empty_pages}") | |
| print(f" Total content size: {total_content} characters") | |
| print(f"\n Page Details:") | |
| for detail in page_details[:10]: # Show first 10 pages | |
| print(f" - {detail}") | |
| if len(page_details) > 10: | |
| print(f" ... and {len(page_details) - 10} more pages") | |
| # Determine pass/fail | |
| if valid_pages >= expected_min_pages: | |
| self.log_result( | |
| f"Crawl: {url}", | |
| "passed", | |
| f"{valid_pages}/{len(results)} valid pages, {total_content} chars total" | |
| ) | |
| else: | |
| self.log_result( | |
| f"Crawl: {url}", | |
| "failed", | |
| f"Only {valid_pages} valid pages (expected >= {expected_min_pages}), {empty_pages} empty, {len(results)} total" | |
| ) | |
| except Exception as e: | |
| self.log_result(f"Crawl: {url}", "failed", f"Exception: {type(e).__name__}: {str(e)}") | |
| if self.verbose: | |
| import traceback | |
| print(f" Traceback:") | |
| print(" " + "\n ".join(traceback.format_exc().split("\n"))) | |
| async def run_all_tests(self): | |
| """Run all tests""" | |
| self.start_time = datetime.now() | |
| print_header("WEB TOOLS TEST SUITE") | |
| print(f"Started at: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}") | |
| # Test environment | |
| if not self.test_environment(): | |
| print_error("\nCannot proceed without required API keys!") | |
| return False | |
| # Test search and collect URLs | |
| urls = self.test_web_search() | |
| # Test extraction | |
| await self.test_web_extract(urls if urls else None) | |
| # Test extraction with LLM | |
| if self.test_llm: | |
| await self.test_web_extract_with_llm(urls if urls else None) | |
| # Test crawling | |
| await self.test_web_crawl() | |
| # Print summary | |
| self.end_time = datetime.now() | |
| duration = (self.end_time - self.start_time).total_seconds() | |
| print_header("TEST SUMMARY") | |
| print(f"Duration: {duration:.2f} seconds") | |
| print(f"\n{Colors.GREEN}Passed: {len(self.test_results['passed'])}{Colors.ENDC}") | |
| print(f"{Colors.FAIL}Failed: {len(self.test_results['failed'])}{Colors.ENDC}") | |
| print(f"{Colors.WARNING}Skipped: {len(self.test_results['skipped'])}{Colors.ENDC}") | |
| # List failed tests | |
| if self.test_results["failed"]: | |
| print(f"\n{Colors.FAIL}{Colors.BOLD}Failed Tests:{Colors.ENDC}") | |
| for test in self.test_results["failed"]: | |
| print(f" - {test['test']}: {test['details']}") | |
| # Save results to file | |
| self.save_results() | |
| return len(self.test_results["failed"]) == 0 | |
| def save_results(self): | |
| """Save test results to a JSON file""" | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| filename = f"test_results_web_tools_{timestamp}.json" | |
| results = { | |
| "test_suite": "Web Tools", | |
| "start_time": self.start_time.isoformat() if self.start_time else None, | |
| "end_time": self.end_time.isoformat() if self.end_time else None, | |
| "duration_seconds": (self.end_time - self.start_time).total_seconds() if self.start_time and self.end_time else None, | |
| "summary": { | |
| "passed": len(self.test_results["passed"]), | |
| "failed": len(self.test_results["failed"]), | |
| "skipped": len(self.test_results["skipped"]) | |
| }, | |
| "results": self.test_results, | |
| "environment": { | |
| "web_backend": _get_backend() if check_web_api_key() else None, | |
| "firecrawl_api_key": check_firecrawl_api_key(), | |
| "parallel_api_key": bool(os.getenv("PARALLEL_API_KEY")), | |
| "auxiliary_model": check_auxiliary_model(), | |
| "debug_mode": get_debug_session_info()["enabled"] | |
| } | |
| } | |
| try: | |
| with open(filename, 'w') as f: | |
| json.dump(results, f, indent=2, ensure_ascii=False) | |
| print_info(f"Test results saved to: {filename}") | |
| except Exception as e: | |
| print_warning(f"Failed to save results: {e}") | |
| async def main(): | |
| """Main entry point""" | |
| parser = argparse.ArgumentParser(description="Test Web Tools Module") | |
| parser.add_argument("--no-llm", action="store_true", help="Skip LLM processing tests") | |
| parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed output") | |
| parser.add_argument("--debug", action="store_true", help="Enable debug mode for web tools") | |
| args = parser.parse_args() | |
| # Set debug mode if requested | |
| if args.debug: | |
| os.environ["WEB_TOOLS_DEBUG"] = "true" | |
| print_info("Debug mode enabled for web tools") | |
| # Create tester | |
| tester = WebToolsTester( | |
| verbose=args.verbose, | |
| test_llm=not args.no_llm | |
| ) | |
| # Run tests | |
| success = await tester.run_all_tests() | |
| # Exit with appropriate code | |
| sys.exit(0 if success else 1) | |
| if __name__ == "__main__": | |
| asyncio.run(main()) | |