diff --git a/SPARC/api.py b/SPARC/api.py index a78c132..3163a8a 100644 --- a/SPARC/api.py +++ b/SPARC/api.py @@ -41,6 +41,7 @@ class CompanyAnalysisResponse(BaseModel): patent_count: int success: bool error: str | None = None + model: str | None = None timestamp: datetime @@ -54,6 +55,15 @@ class BatchAnalysisResponse(BaseModel): timestamp: datetime +class CompanyAnalysisRequest(BaseModel): + """Request model for single company analysis with optional model selection.""" + + model: str | None = Field( + default=None, + description="LLM model to use (e.g. 'anthropic/claude-3.5-sonnet', 'openai/gpt-4o'). Defaults to server config.", + ) + + class BatchAnalysisRequest(BaseModel): """Request model for batch company analysis.""" @@ -63,6 +73,10 @@ class BatchAnalysisRequest(BaseModel): max_workers: int = Field( default=3, ge=1, le=5, description="Max concurrent analyses" ) + model: str | None = Field( + default=None, + description="LLM model to use for all analyses in this batch. Defaults to server config.", + ) class JobStatus(BaseModel): @@ -133,6 +147,7 @@ def _convert_result(result: CompanyAnalysisResult) -> CompanyAnalysisResponse: patent_count=result.patent_count, success=result.success, error=result.error, + model=result.model, timestamp=result.timestamp, ) @@ -389,6 +404,32 @@ async def get_analytics( ) +# ============== Model Selection Endpoints ============== + +# Supported models via OpenRouter +SUPPORTED_MODELS = [ + {"id": "anthropic/claude-3.5-sonnet", "name": "Claude 3.5 Sonnet", "provider": "Anthropic"}, + {"id": "openai/gpt-4o", "name": "GPT-4o", "provider": "OpenAI"}, + {"id": "openai/gpt-4o-mini", "name": "GPT-4o Mini", "provider": "OpenAI"}, + {"id": "google/gemini-pro-1.5", "name": "Gemini Pro 1.5", "provider": "Google"}, + {"id": "meta-llama/llama-3.1-70b-instruct", "name": "Llama 3.1 70B", "provider": "Meta"}, +] + + +@app.get("/models", tags=["System"]) +async def list_models(): + """List supported LLM models for analysis. + + Returns the available models that can be passed as the `model` field + in analysis requests. The default model is determined by the `MODEL` + environment variable on the server. + """ + return { + "models": SUPPORTED_MODELS, + "default": config.model, + } + + # ============== System Endpoints ============== diff --git a/SPARC/llm.py b/SPARC/llm.py index 707a0d6..9214cee 100644 --- a/SPARC/llm.py +++ b/SPARC/llm.py @@ -40,12 +40,13 @@ class LLMAnalyzer: else: self.client = None - def analyze_patent_content(self, patent_content: str, company_name: str) -> str: + def analyze_patent_content(self, patent_content: str, company_name: str, model: str | None = None) -> str: """Analyze patent content to estimate company innovation and performance. Args: patent_content: Minimized patent text (abstract, claims, summary) company_name: Name of the company for context + model: Optional model override (e.g. "openai/gpt-4o"). Defaults to config. Returns: Analysis text describing innovation quality and potential impact @@ -63,6 +64,8 @@ Patent Content: Provide a concise analysis (2-3 paragraphs) focusing on what this patent reveals about the company's technical direction and competitive advantage.""" + effective_model = model or self.model + if self.test_mode: logger.debug("TEST MODE - Prompt that would be sent to LLM:\n%s", prompt) return "[TEST MODE - No API call made]" @@ -81,7 +84,7 @@ Provide a concise analysis (2-3 paragraphs) focusing on what this patent reveals response=cached["response"], company_name=company_name, analysis_type="single_patent", - model=self.model, + model=effective_model, metadata={ "patent_content_length": len(patent_content), "cache_hit": True, @@ -94,7 +97,7 @@ Provide a concise analysis (2-3 paragraphs) focusing on what this patent reveals # Call API if no cache hit and client is available if self.client: response = self.client.chat.completions.create( - model=self.model, + model=effective_model, max_tokens=1024, messages=[{"role": "user", "content": prompt}], ) @@ -106,7 +109,7 @@ Provide a concise analysis (2-3 paragraphs) focusing on what this patent reveals response=response_text, company_name=company_name, analysis_type="single_patent", - model=self.model, + model=effective_model, metadata={"patent_content_length": len(patent_content)}, token_usage={ "prompt_tokens": response.usage.prompt_tokens, @@ -124,13 +127,13 @@ Provide a concise analysis (2-3 paragraphs) focusing on what this patent reveals response=placeholder, company_name=company_name, analysis_type="single_patent", - model=self.model, + model=effective_model, metadata={"patent_content_length": len(patent_content), "pending": True} ) return placeholder - + def analyze_patent_portfolio( - self, patents_data: list[Dict[str, str]], company_name: str + self, patents_data: list[Dict[str, str]], company_name: str, model: str | None = None ) -> str: """Analyze multiple patents to estimate overall company performance. @@ -165,13 +168,16 @@ Patent Portfolio: Provide a comprehensive analysis (4-5 paragraphs) with a final verdict on the company's innovation strength and performance outlook.""" + effective_model = model or self.model + if self.test_mode: logger.debug("TEST MODE - Portfolio prompt:\n%s", prompt) return "[TEST MODE]" metadata = { "patent_count": len(patents_data), - "patent_ids": [p['patent_id'] for p in patents_data] + "patent_ids": [p['patent_id'] for p in patents_data], + "model": effective_model, } # Check cache first @@ -188,7 +194,7 @@ Provide a comprehensive analysis (4-5 paragraphs) with a final verdict on the co response=cached["response"], company_name=company_name, analysis_type="portfolio", - model=self.model, + model=effective_model, metadata={ **metadata, "cache_hit": True, @@ -202,7 +208,7 @@ Provide a comprehensive analysis (4-5 paragraphs) with a final verdict on the co if self.client: try: response = self.client.chat.completions.create( - model=self.model, + model=effective_model, max_tokens=2048, messages=[{"role": "user", "content": prompt}], ) @@ -215,7 +221,7 @@ Provide a comprehensive analysis (4-5 paragraphs) with a final verdict on the co response=response_text, company_name=company_name, analysis_type="portfolio", - model=self.model, + model=effective_model, metadata=metadata, token_usage={ "prompt_tokens": response.usage.prompt_tokens, @@ -235,7 +241,7 @@ Provide a comprehensive analysis (4-5 paragraphs) with a final verdict on the co response=placeholder, company_name=company_name, analysis_type="portfolio", - model=self.model, + model=effective_model, metadata={**metadata, "pending": True} ) return placeholder diff --git a/SPARC/types.py b/SPARC/types.py index 5bb692b..fd11073 100644 --- a/SPARC/types.py +++ b/SPARC/types.py @@ -24,6 +24,7 @@ class CompanyAnalysisResult: patent_count: int success: bool error: str | None = None + model: str | None = None timestamp: datetime = field(default_factory=datetime.now)