forked from 0xWheatyz/SPARC
feat(backend): add response caching and user management
Replace USE_DATABASE toggle with USE_CACHE for smarter LLM response handling: - Add prompt hashing for efficient cache lookups - Cache API responses in database to reduce token usage - Always store responses for analytics (cache or fresh) Add user authentication infrastructure: - User table with bcrypt password hashing - CRUD operations for user management - Role-based access control (admin/user) Dependencies: add bcrypt and PyJWT for auth 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+107
-69
@@ -9,31 +9,29 @@ from typing import Dict
|
||||
class LLMAnalyzer:
|
||||
"""Handles LLM-based analysis of patent content."""
|
||||
|
||||
def __init__(self, api_key: str | None = None, test_mode: bool = False, use_database: bool | None = None):
|
||||
def __init__(self, api_key: str | None = None, test_mode: bool = False, use_cache: bool | None = None):
|
||||
"""Initialize the LLM analyzer.
|
||||
|
||||
Args:
|
||||
api_key: OpenRouter API key. If None, will attempt to load from config.
|
||||
test_mode: If True, print prompts instead of making API calls
|
||||
use_database: If True, store messages in database instead of calling API.
|
||||
If None, will use config.use_database
|
||||
use_cache: If True, check database cache before making API calls.
|
||||
If None, uses config.use_cache (default: True)
|
||||
"""
|
||||
self.test_mode = test_mode
|
||||
self.use_database = use_database if use_database is not None else config.use_database
|
||||
self.db_client = None
|
||||
self.use_cache = use_cache if use_cache is not None else config.use_cache
|
||||
self.model = "anthropic/claude-3.5-sonnet"
|
||||
|
||||
# Initialize database client if in database mode
|
||||
if self.use_database:
|
||||
self.db_client = DatabaseClient(config.database_url)
|
||||
self.db_client.initialize_schema()
|
||||
# Always initialize database client for storage and caching
|
||||
self.db_client = DatabaseClient(config.database_url)
|
||||
self.db_client.initialize_schema()
|
||||
|
||||
# Initialize OpenRouter client if not in database mode
|
||||
if (api_key or config.openrouter_api_key) and not test_mode and not self.use_database:
|
||||
# Initialize OpenRouter client if API key is available
|
||||
if (api_key or config.openrouter_api_key) and not test_mode:
|
||||
self.client = OpenAI(
|
||||
api_key=api_key or config.openrouter_api_key,
|
||||
base_url="https://openrouter.ai/api/v1"
|
||||
)
|
||||
self.model = "anthropic/claude-3.5-sonnet"
|
||||
else:
|
||||
self.client = None
|
||||
|
||||
@@ -68,22 +66,31 @@ Provide a concise analysis (2-3 paragraphs) focusing on what this patent reveals
|
||||
print("=" * 80)
|
||||
return "[TEST MODE - No API call made]"
|
||||
|
||||
# Database mode: store the prompt and return a placeholder response
|
||||
if self.use_database:
|
||||
response_text = "[DATABASE MODE] Message stored for testing/analytics. Enable API mode to get actual analysis."
|
||||
|
||||
self.db_client.store_message(
|
||||
# Check cache first
|
||||
if self.use_cache:
|
||||
cached = self.db_client.get_cached_response(
|
||||
prompt=prompt,
|
||||
response=response_text,
|
||||
company_name=company_name,
|
||||
analysis_type="single_patent",
|
||||
model=self.model if hasattr(self, 'model') else None,
|
||||
metadata={"patent_content_length": len(patent_content)}
|
||||
analysis_type="single_patent"
|
||||
)
|
||||
if cached:
|
||||
# Log the cache hit
|
||||
self.db_client.store_message(
|
||||
prompt=prompt,
|
||||
response=cached["response"],
|
||||
company_name=company_name,
|
||||
analysis_type="single_patent",
|
||||
model=self.model,
|
||||
metadata={
|
||||
"patent_content_length": len(patent_content),
|
||||
"cache_hit": True,
|
||||
"original_message_id": cached["id"]
|
||||
},
|
||||
is_cached=True
|
||||
)
|
||||
return cached["response"]
|
||||
|
||||
return response_text
|
||||
|
||||
# API mode: send to OpenRouter
|
||||
# Call API if no cache hit and client is available
|
||||
if self.client:
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
@@ -92,23 +99,34 @@ Provide a concise analysis (2-3 paragraphs) focusing on what this patent reveals
|
||||
)
|
||||
response_text = response.choices[0].message.content
|
||||
|
||||
# Store in database if db_client is available (for logging even in API mode)
|
||||
if self.db_client:
|
||||
self.db_client.store_message(
|
||||
prompt=prompt,
|
||||
response=response_text,
|
||||
company_name=company_name,
|
||||
analysis_type="single_patent",
|
||||
model=self.model,
|
||||
metadata={"patent_content_length": len(patent_content)},
|
||||
token_usage={
|
||||
"prompt_tokens": response.usage.prompt_tokens,
|
||||
"completion_tokens": response.usage.completion_tokens,
|
||||
"total_tokens": response.usage.total_tokens
|
||||
} if hasattr(response, 'usage') else None
|
||||
)
|
||||
# Store in database for future cache lookups
|
||||
self.db_client.store_message(
|
||||
prompt=prompt,
|
||||
response=response_text,
|
||||
company_name=company_name,
|
||||
analysis_type="single_patent",
|
||||
model=self.model,
|
||||
metadata={"patent_content_length": len(patent_content)},
|
||||
token_usage={
|
||||
"prompt_tokens": response.usage.prompt_tokens,
|
||||
"completion_tokens": response.usage.completion_tokens,
|
||||
"total_tokens": response.usage.total_tokens
|
||||
} if hasattr(response, 'usage') else None
|
||||
)
|
||||
|
||||
return response_text
|
||||
|
||||
# No API client available - store prompt for later processing
|
||||
placeholder = "[NO API] Prompt stored in database. Configure OPENROUTER_API_KEY to enable analysis."
|
||||
self.db_client.store_message(
|
||||
prompt=prompt,
|
||||
response=placeholder,
|
||||
company_name=company_name,
|
||||
analysis_type="single_patent",
|
||||
model=self.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
|
||||
@@ -150,46 +168,54 @@ Provide a comprehensive analysis (4-5 paragraphs) with a final verdict on the co
|
||||
print(prompt)
|
||||
return "[TEST MODE]"
|
||||
|
||||
# Database mode: store the prompt and return a placeholder response
|
||||
if self.use_database:
|
||||
response_text = "[DATABASE MODE] Message stored for testing/analytics. Enable API mode to get actual analysis."
|
||||
metadata = {
|
||||
"patent_count": len(patents_data),
|
||||
"patent_ids": [p['patent_id'] for p in patents_data]
|
||||
}
|
||||
|
||||
self.db_client.store_message(
|
||||
# Check cache first
|
||||
if self.use_cache:
|
||||
cached = self.db_client.get_cached_response(
|
||||
prompt=prompt,
|
||||
response=response_text,
|
||||
company_name=company_name,
|
||||
analysis_type="portfolio",
|
||||
model=self.model if hasattr(self, 'model') else None,
|
||||
metadata={
|
||||
"patent_count": len(patents_data),
|
||||
"patent_ids": [p['patent_id'] for p in patents_data]
|
||||
}
|
||||
analysis_type="portfolio"
|
||||
)
|
||||
if cached:
|
||||
# Log the cache hit
|
||||
self.db_client.store_message(
|
||||
prompt=prompt,
|
||||
response=cached["response"],
|
||||
company_name=company_name,
|
||||
analysis_type="portfolio",
|
||||
model=self.model,
|
||||
metadata={
|
||||
**metadata,
|
||||
"cache_hit": True,
|
||||
"original_message_id": cached["id"]
|
||||
},
|
||||
is_cached=True
|
||||
)
|
||||
return cached["response"]
|
||||
|
||||
return response_text
|
||||
# Call API if no cache hit and client is available
|
||||
if self.client:
|
||||
try:
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
max_tokens=2048,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
|
||||
# API mode: send to OpenRouter
|
||||
try:
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
max_tokens=2048,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
response_text = response.choices[0].message.content
|
||||
|
||||
response_text = response.choices[0].message.content
|
||||
|
||||
# Store in database if db_client is available (for logging even in API mode)
|
||||
if self.db_client:
|
||||
# Store in database for future cache lookups
|
||||
self.db_client.store_message(
|
||||
prompt=prompt,
|
||||
response=response_text,
|
||||
company_name=company_name,
|
||||
analysis_type="portfolio",
|
||||
model=self.model,
|
||||
metadata={
|
||||
"patent_count": len(patents_data),
|
||||
"patent_ids": [p['patent_id'] for p in patents_data]
|
||||
},
|
||||
metadata=metadata,
|
||||
token_usage={
|
||||
"prompt_tokens": response.usage.prompt_tokens,
|
||||
"completion_tokens": response.usage.completion_tokens,
|
||||
@@ -197,7 +223,19 @@ Provide a comprehensive analysis (4-5 paragraphs) with a final verdict on the co
|
||||
} if hasattr(response, 'usage') else None
|
||||
)
|
||||
|
||||
return response_text
|
||||
except AttributeError:
|
||||
return prompt
|
||||
return response_text
|
||||
except AttributeError:
|
||||
return prompt
|
||||
|
||||
# No API client available - store prompt for later processing
|
||||
placeholder = "[NO API] Prompt stored in database. Configure OPENROUTER_API_KEY to enable analysis."
|
||||
self.db_client.store_message(
|
||||
prompt=prompt,
|
||||
response=placeholder,
|
||||
company_name=company_name,
|
||||
analysis_type="portfolio",
|
||||
model=self.model,
|
||||
metadata={**metadata, "pending": True}
|
||||
)
|
||||
return placeholder
|
||||
|
||||
|
||||
Reference in New Issue
Block a user