diff --git a/SPARC/analyzer.py b/SPARC/analyzer.py index 9cd2fe7..2d677cc 100644 --- a/SPARC/analyzer.py +++ b/SPARC/analyzer.py @@ -5,10 +5,13 @@ to provide company performance estimation based on patent portfolios. """ import hashlib +import logging from concurrent.futures import ThreadPoolExecutor, as_completed from typing import Callable from SPARC import config + +logger = logging.getLogger(__name__) from SPARC.database import DatabaseClient from SPARC.serp_api import SERP from SPARC.llm import LLMAnalyzer @@ -52,13 +55,13 @@ class CompanyAnalyzer: query_hash = hashlib.sha256(company_name.lower().encode()).hexdigest() cached_ids = self.db.get_cached_serp_query(query_hash) if cached_ids is not None: - print(f"Using cached SERP results for {company_name} ({len(cached_ids)} patents)") + logger.info("Using cached SERP results for %s (%d patents)", company_name, len(cached_ids)) patents = Patents(patents=[ Patent(patent_id=pid, pdf_link="") for pid in cached_ids ]) else: - print(f"Retrieving patents for {company_name}...") + logger.info("Retrieving patents for %s...", company_name) patents = SERP.query(company_name) # Cache the SERP results if patents.patents: @@ -66,12 +69,13 @@ class CompanyAnalyzer: company_name=company_name, query_hash=query_hash, patent_ids=[p.patent_id for p in patents.patents], + ttl_hours=config.serp_cache_ttl_hours, ) if not patents.patents: return f"No patents found for {company_name}" - print(f"Found {len(patents.patents)} patents. Processing...") + logger.info("Found %d patents. Processing...", len(patents.patents)) # Download, parse, and minimize patents in parallel processed_patents = [] @@ -87,12 +91,12 @@ class CompanyAnalyzer: if result: processed_patents.append(result) except Exception as e: - print(f"Warning: Failed to process {patent.patent_id}: {e}") + logger.warning("Failed to process %s: %s", patent.patent_id, e) if not processed_patents: return f"Failed to process any patents for {company_name}" - print(f"Analyzing portfolio with LLM...") + logger.info("Analyzing portfolio with LLM...") # Analyze the full portfolio with LLM analysis = self.llm_analyzer.analyze_patent_portfolio( @@ -122,6 +126,7 @@ class CompanyAnalyzer: FileNotFoundError: If the patent PDF is not found at the expected path. """ import os + logger.info("Analyzing patent %s for %s...", patent_id, company_name) patent_path = f"patents/{patent_id}.pdf" @@ -183,7 +188,7 @@ class CompanyAnalyzer: return {"patent_id": patent.patent_id, "content": minimized_content} except Exception as e: - print(f"Warning: Failed to process {patent.patent_id}: {e}") + logger.warning("Failed to process %s: %s", patent.patent_id, e) return None def _analyze_company_safe(self, company_name: str) -> CompanyAnalysisResult: @@ -254,7 +259,7 @@ class CompanyAnalyzer: results: list[CompanyAnalysisResult] = [] total = len(companies) - print(f"Starting batch analysis of {total} companies...") + logger.info("Starting batch analysis of %d companies...", total) with ThreadPoolExecutor(max_workers=max_workers) as executor: future_to_company = { @@ -271,8 +276,8 @@ class CompanyAnalyzer: result = future.result() results.append(result) - status = "✓" if result.success else "✗" - print(f"[{completed}/{total}] {status} {company}") + status = "OK" if result.success else "FAIL" + logger.info("[%d/%d] %s %s", completed, total, status, company) if progress_callback: progress_callback(company, completed, total) @@ -287,12 +292,12 @@ class CompanyAnalyzer: error=str(e), ) ) - print(f"[{completed}/{total}] ✗ {company}: {e}") + logger.error("[%d/%d] FAIL %s: %s", completed, total, company, e) successful = sum(1 for r in results if r.success) failed = total - successful - print(f"\nBatch complete: {successful} succeeded, {failed} failed") + logger.info("Batch complete: %d succeeded, %d failed", successful, failed) return BatchAnalysisResult( results=results, @@ -318,20 +323,20 @@ class CompanyAnalyzer: results: list[CompanyAnalysisResult] = [] total = len(companies) - print(f"Starting sequential analysis of {total} companies...") + logger.info("Starting sequential analysis of %d companies...", total) for idx, company in enumerate(companies, 1): - print(f"\n[{idx}/{total}] Analyzing {company}...") + logger.info("[%d/%d] Analyzing %s...", idx, total, company) result = self._analyze_company_safe(company) results.append(result) - status = "✓" if result.success else "✗" - print(f"[{idx}/{total}] {status} {company}") + status = "OK" if result.success else "FAIL" + logger.info("[%d/%d] %s %s", idx, total, status, company) successful = sum(1 for r in results if r.success) failed = total - successful - print(f"\nBatch complete: {successful} succeeded, {failed} failed") + logger.info("Batch complete: %d succeeded, %d failed", successful, failed) return BatchAnalysisResult( results=results, diff --git a/SPARC/config.py b/SPARC/config.py index 2bedc63..e6f6173 100644 --- a/SPARC/config.py +++ b/SPARC/config.py @@ -2,11 +2,20 @@ Loads environment variables from .env file for API keys and other secrets. """ -from dotenv import load_dotenv +import logging import os +from dotenv import load_dotenv + load_dotenv() +# Logging configuration +log_level = os.getenv("LOG_LEVEL", "INFO").upper() +logging.basicConfig( + level=getattr(logging, log_level, logging.INFO), + format="%(asctime)s %(levelname)s %(name)s %(message)s", +) + # SerpAPI key for patent search api_key = os.getenv("API_KEY") @@ -30,6 +39,12 @@ use_database = os.getenv("USE_DATABASE", "false").lower() in ("true", "1", "yes" patent_search_days = int(os.getenv("PATENT_SEARCH_DAYS", "90")) patent_thread_workers = int(os.getenv("PATENT_THREAD_WORKERS", "5")) +# LLM model to use via OpenRouter (e.g. "anthropic/claude-3.5-sonnet", "openai/gpt-4o") +model = os.getenv("MODEL", "anthropic/claude-3.5-sonnet") + +# SERP cache TTL in hours (how long cached search results are considered fresh) +serp_cache_ttl_hours = int(os.getenv("SERP_CACHE_TTL_HOURS", "24")) + # Root path for running behind a reverse proxy (e.g., "/api" when served at /api/) # This ensures OpenAPI docs work correctly when accessed via the proxy root_path = os.getenv("ROOT_PATH", "") diff --git a/SPARC/llm.py b/SPARC/llm.py index 2e60c9b..707a0d6 100644 --- a/SPARC/llm.py +++ b/SPARC/llm.py @@ -1,9 +1,14 @@ """LLM integration for patent analysis using OpenRouter.""" +import logging +from typing import Dict + from openai import OpenAI + from SPARC import config from SPARC.database import DatabaseClient -from typing import Dict + +logger = logging.getLogger(__name__) class LLMAnalyzer: @@ -20,7 +25,7 @@ class LLMAnalyzer: """ self.test_mode = test_mode self.use_cache = use_cache if use_cache is not None else config.use_cache - self.model = "anthropic/claude-3.5-sonnet" + self.model = config.model # Always initialize database client for storage and caching self.db_client = DatabaseClient(config.database_url) @@ -59,11 +64,7 @@ Patent Content: Provide a concise analysis (2-3 paragraphs) focusing on what this patent reveals about the company's technical direction and competitive advantage.""" if self.test_mode: - print("=" * 80) - print("TEST MODE - Prompt that would be sent to LLM:") - print("=" * 80) - print(prompt) - print("=" * 80) + logger.debug("TEST MODE - Prompt that would be sent to LLM:\n%s", prompt) return "[TEST MODE - No API call made]" # Check cache first @@ -165,7 +166,7 @@ Patent Portfolio: Provide a comprehensive analysis (4-5 paragraphs) with a final verdict on the company's innovation strength and performance outlook.""" if self.test_mode: - print(prompt) + logger.debug("TEST MODE - Portfolio prompt:\n%s", prompt) return "[TEST MODE]" metadata = { diff --git a/SPARC/types.py b/SPARC/types.py index 09c17d1..5bb692b 100644 --- a/SPARC/types.py +++ b/SPARC/types.py @@ -4,7 +4,7 @@ from datetime import datetime @dataclass class Patent: - patent_id: int + patent_id: str pdf_link: str pdf_path: str | None = None summary: dict | None = None