Refactor code for improved readability and consistency
- Cleaned up whitespace and formatting in various files including http.py, language.py, logger.py, safe_exec.py, and SQL migration scripts. - Consolidated import statements and removed unnecessary blank lines. - Updated logging configuration for better clarity. - Enhanced the safe execution code with improved error handling and logging. - Removed commented-out code and unnecessary variables in backfill_zero_trades.py and other scripts. - Added a pyproject.toml for Ruff and Vulture configuration. - Introduced requirements-dev.txt for development dependencies. - Removed commented-out stock entries in init.sql for cleaner migration scripts.
This commit is contained in:
@@ -3,21 +3,24 @@ LLM service.
|
||||
Supports multiple providers: OpenRouter, OpenAI, Google Gemini, DeepSeek, Grok.
|
||||
Kept separate from AnalysisService to avoid circular imports.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
from typing import Dict, Any, Optional, List
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from app.config import APIKeys
|
||||
from app.utils.config_loader import load_addon_config
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class LLMProvider(Enum):
|
||||
"""Supported LLM providers"""
|
||||
|
||||
OPENROUTER = "openrouter"
|
||||
OPENAI = "openai"
|
||||
GOOGLE = "google"
|
||||
@@ -61,7 +64,7 @@ class LLMService:
|
||||
def __init__(self, provider: str = None):
|
||||
"""
|
||||
Initialize LLM service.
|
||||
|
||||
|
||||
Args:
|
||||
provider: Override the default provider (openrouter, openai, google, deepseek, grok)
|
||||
"""
|
||||
@@ -75,11 +78,11 @@ class LLMService:
|
||||
return LLMProvider(self._provider_override.lower())
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
# Check env/config for provider selection
|
||||
config = load_addon_config()
|
||||
provider_name = config.get('llm', {}).get('provider') or os.getenv('LLM_PROVIDER', '')
|
||||
|
||||
provider_name = config.get("llm", {}).get("provider") or os.getenv("LLM_PROVIDER", "")
|
||||
|
||||
if provider_name:
|
||||
try:
|
||||
# Explicit selection should always be respected.
|
||||
@@ -88,7 +91,7 @@ class LLMService:
|
||||
return selected
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
# Auto-detect: find any provider with a configured API key
|
||||
# Priority: DeepSeek > Grok > OpenAI > Google > OpenRouter
|
||||
priority_order = [
|
||||
@@ -98,19 +101,19 @@ class LLMService:
|
||||
LLMProvider.GOOGLE,
|
||||
LLMProvider.OPENROUTER,
|
||||
]
|
||||
|
||||
|
||||
for p in priority_order:
|
||||
if self.get_api_key(p):
|
||||
logger.info(f"Auto-detected LLM provider: {p.value}")
|
||||
return p
|
||||
|
||||
|
||||
# Fallback to OpenRouter (will fail later if no key)
|
||||
return LLMProvider.OPENROUTER
|
||||
|
||||
def get_api_key(self, provider: LLMProvider = None) -> str:
|
||||
"""Get API key for the specified provider."""
|
||||
p = provider or self.provider
|
||||
|
||||
|
||||
key_map = {
|
||||
LLMProvider.OPENROUTER: APIKeys.OPENROUTER_API_KEY,
|
||||
LLMProvider.OPENAI: APIKeys.OPENAI_API_KEY,
|
||||
@@ -124,32 +127,32 @@ class LLMService:
|
||||
"""Get base URL for the specified provider."""
|
||||
p = provider or self.provider
|
||||
config = load_addon_config()
|
||||
|
||||
|
||||
# Check for custom base URL in config
|
||||
provider_config = config.get(p.value, {})
|
||||
custom_url = provider_config.get('base_url') or os.getenv(f'{p.value.upper()}_BASE_URL', '').strip()
|
||||
|
||||
custom_url = provider_config.get("base_url") or os.getenv(f"{p.value.upper()}_BASE_URL", "").strip()
|
||||
|
||||
if custom_url:
|
||||
return custom_url.rstrip('/')
|
||||
|
||||
return custom_url.rstrip("/")
|
||||
|
||||
return PROVIDER_CONFIGS[p]["base_url"]
|
||||
|
||||
def get_default_model(self, provider: LLMProvider = None) -> str:
|
||||
"""Get default model for the specified provider."""
|
||||
p = provider or self.provider
|
||||
config = load_addon_config()
|
||||
|
||||
|
||||
provider_config = config.get(p.value, {})
|
||||
custom_model = provider_config.get('model') or os.getenv(f'{p.value.upper()}_MODEL', '').strip()
|
||||
|
||||
custom_model = provider_config.get("model") or os.getenv(f"{p.value.upper()}_MODEL", "").strip()
|
||||
|
||||
if custom_model:
|
||||
return custom_model
|
||||
|
||||
|
||||
return PROVIDER_CONFIGS[p]["default_model"]
|
||||
|
||||
def get_code_generation_model(self, provider: LLMProvider = None) -> str:
|
||||
"""Get model for AI code generation; fallback to provider default when unset."""
|
||||
model = os.getenv('AI_CODE_GEN_MODEL', '').strip()
|
||||
model = os.getenv("AI_CODE_GEN_MODEL", "").strip()
|
||||
if model:
|
||||
return model
|
||||
return self.get_default_model(provider)
|
||||
@@ -163,17 +166,24 @@ class LLMService:
|
||||
def base_url(self):
|
||||
return self.get_base_url()
|
||||
|
||||
def _call_openai_compatible(self, messages: list, model: str, temperature: float,
|
||||
api_key: str, base_url: str, timeout: int,
|
||||
use_json_mode: bool = True) -> str:
|
||||
def _call_openai_compatible(
|
||||
self,
|
||||
messages: list,
|
||||
model: str,
|
||||
temperature: float,
|
||||
api_key: str,
|
||||
base_url: str,
|
||||
timeout: int,
|
||||
use_json_mode: bool = True,
|
||||
) -> str:
|
||||
"""Call OpenAI-compatible API (OpenAI, DeepSeek, Grok, OpenRouter)."""
|
||||
url = f"{base_url}/chat/completions"
|
||||
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
# OpenRouter specific headers
|
||||
if "openrouter" in base_url:
|
||||
headers["HTTP-Referer"] = "https://quantdinger.com"
|
||||
@@ -184,12 +194,12 @@ class LLMService:
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
}
|
||||
|
||||
|
||||
if use_json_mode:
|
||||
data["response_format"] = {"type": "json_object"}
|
||||
|
||||
response = requests.post(url, headers=headers, json=data, timeout=timeout)
|
||||
|
||||
|
||||
# Handle non-2xx with provider/model-aware details
|
||||
if response.status_code >= 400:
|
||||
provider_name = "OpenRouter" if "openrouter" in (base_url or "").lower() else "LLM"
|
||||
@@ -211,6 +221,7 @@ class LLMService:
|
||||
# OpenRouter targeted hints
|
||||
if "openrouter" in (base_url or "").lower():
|
||||
from app.config.api_keys import APIKeys
|
||||
|
||||
if not APIKeys.OPENROUTER_API_KEY:
|
||||
error_msg += ". OPENROUTER_API_KEY is not configured. Set it in backend_api_python/.env"
|
||||
elif response.status_code == 403:
|
||||
@@ -219,7 +230,7 @@ class LLMService:
|
||||
error_msg += ". Possible causes: the model is unavailable or blocked by account privacy/data-policy settings. Check https://openrouter.ai/settings/privacy"
|
||||
|
||||
raise ValueError(error_msg)
|
||||
|
||||
|
||||
result = response.json()
|
||||
if "choices" in result and len(result["choices"]) > 0:
|
||||
content = result["choices"][0]["message"]["content"]
|
||||
@@ -229,42 +240,43 @@ class LLMService:
|
||||
else:
|
||||
raise ValueError("API response is missing 'choices'")
|
||||
|
||||
def _call_google_gemini(self, messages: list, model: str, temperature: float,
|
||||
api_key: str, base_url: str, timeout: int) -> str:
|
||||
def _call_google_gemini(
|
||||
self, messages: list, model: str, temperature: float, api_key: str, base_url: str, timeout: int
|
||||
) -> str:
|
||||
"""Call Google Gemini API."""
|
||||
url = f"{base_url}/models/{model}:generateContent?key={api_key}"
|
||||
|
||||
|
||||
# Convert OpenAI message format to Gemini format
|
||||
contents = []
|
||||
system_instruction = None
|
||||
|
||||
|
||||
for msg in messages:
|
||||
role = msg["role"]
|
||||
content = msg["content"]
|
||||
|
||||
|
||||
if role == "system":
|
||||
system_instruction = content
|
||||
elif role == "user":
|
||||
contents.append({"role": "user", "parts": [{"text": content}]})
|
||||
elif role == "assistant":
|
||||
contents.append({"role": "model", "parts": [{"text": content}]})
|
||||
|
||||
|
||||
data = {
|
||||
"contents": contents,
|
||||
"generationConfig": {
|
||||
"temperature": temperature,
|
||||
"responseMimeType": "application/json",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if system_instruction:
|
||||
data["systemInstruction"] = {"parts": [{"text": system_instruction}]}
|
||||
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
|
||||
response = requests.post(url, headers=headers, json=data, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
result = response.json()
|
||||
if "candidates" in result and len(result["candidates"]) > 0:
|
||||
candidate = result["candidates"][0]
|
||||
@@ -272,54 +284,54 @@ class LLMService:
|
||||
text = candidate["content"]["parts"][0].get("text", "")
|
||||
if text:
|
||||
return text
|
||||
|
||||
|
||||
raise ValueError("Gemini API response is missing content")
|
||||
|
||||
def _normalize_model_for_provider(self, model: str, provider: LLMProvider) -> str:
|
||||
"""
|
||||
Normalize model name for the target provider.
|
||||
|
||||
|
||||
Frontend may send OpenRouter-style model names (e.g., 'openai/gpt-4o').
|
||||
This converts them to the correct format for each provider.
|
||||
"""
|
||||
if not model:
|
||||
return self.get_default_model(provider)
|
||||
|
||||
|
||||
model = model.strip()
|
||||
|
||||
|
||||
# If using OpenRouter, keep the original format
|
||||
if provider == LLMProvider.OPENROUTER:
|
||||
return model
|
||||
|
||||
|
||||
# For direct providers, extract the model name from OpenRouter format
|
||||
# e.g., 'openai/gpt-4o' -> 'gpt-4o'
|
||||
# 'google/gemini-1.5-flash' -> 'gemini-1.5-flash'
|
||||
# 'deepseek/deepseek-chat' -> 'deepseek-chat'
|
||||
# 'x-ai/grok-beta' -> 'grok-beta'
|
||||
|
||||
if '/' in model:
|
||||
prefix, actual_model = model.split('/', 1)
|
||||
|
||||
if "/" in model:
|
||||
prefix, actual_model = model.split("/", 1)
|
||||
prefix_lower = prefix.lower()
|
||||
|
||||
|
||||
# Map OpenRouter prefixes to providers
|
||||
prefix_to_provider = {
|
||||
'openai': LLMProvider.OPENAI,
|
||||
'google': LLMProvider.GOOGLE,
|
||||
'deepseek': LLMProvider.DEEPSEEK,
|
||||
'x-ai': LLMProvider.GROK,
|
||||
'xai': LLMProvider.GROK,
|
||||
"openai": LLMProvider.OPENAI,
|
||||
"google": LLMProvider.GOOGLE,
|
||||
"deepseek": LLMProvider.DEEPSEEK,
|
||||
"x-ai": LLMProvider.GROK,
|
||||
"xai": LLMProvider.GROK,
|
||||
}
|
||||
|
||||
|
||||
# If the model prefix matches the current provider, use the extracted model name
|
||||
matched_provider = prefix_to_provider.get(prefix_lower)
|
||||
if matched_provider == provider:
|
||||
return actual_model
|
||||
|
||||
|
||||
# If model prefix doesn't match current provider, use provider's default model
|
||||
# This prevents sending 'gpt-4o' to DeepSeek, etc.
|
||||
logger.warning(f"Model '{model}' doesn't match provider '{provider.value}', using default model")
|
||||
return self.get_default_model(provider)
|
||||
|
||||
|
||||
# Model name without prefix - use as is
|
||||
return model
|
||||
|
||||
@@ -328,30 +340,37 @@ class LLMService:
|
||||
Detect which provider a model belongs to based on its name.
|
||||
Returns None if detection fails.
|
||||
"""
|
||||
if not model or '/' not in model:
|
||||
if not model or "/" not in model:
|
||||
return None
|
||||
|
||||
prefix = model.split('/')[0].lower()
|
||||
|
||||
|
||||
prefix = model.split("/")[0].lower()
|
||||
|
||||
prefix_to_provider = {
|
||||
'openai': LLMProvider.OPENAI,
|
||||
'google': LLMProvider.GOOGLE,
|
||||
'deepseek': LLMProvider.DEEPSEEK,
|
||||
'x-ai': LLMProvider.GROK,
|
||||
'xai': LLMProvider.GROK,
|
||||
'anthropic': LLMProvider.OPENROUTER, # Anthropic only via OpenRouter
|
||||
'meta': LLMProvider.OPENROUTER, # Meta/Llama only via OpenRouter
|
||||
'mistral': LLMProvider.OPENROUTER, # Mistral only via OpenRouter
|
||||
"openai": LLMProvider.OPENAI,
|
||||
"google": LLMProvider.GOOGLE,
|
||||
"deepseek": LLMProvider.DEEPSEEK,
|
||||
"x-ai": LLMProvider.GROK,
|
||||
"xai": LLMProvider.GROK,
|
||||
"anthropic": LLMProvider.OPENROUTER, # Anthropic only via OpenRouter
|
||||
"meta": LLMProvider.OPENROUTER, # Meta/Llama only via OpenRouter
|
||||
"mistral": LLMProvider.OPENROUTER, # Mistral only via OpenRouter
|
||||
}
|
||||
|
||||
|
||||
return prefix_to_provider.get(prefix)
|
||||
|
||||
def call_llm_api(self, messages: list, model: str = None, temperature: float = 0.7,
|
||||
use_fallback: bool = True, provider: LLMProvider = None,
|
||||
use_json_mode: bool = True, try_alternative_providers: bool = True) -> str:
|
||||
def call_llm_api(
|
||||
self,
|
||||
messages: list,
|
||||
model: str = None,
|
||||
temperature: float = 0.7,
|
||||
use_fallback: bool = True,
|
||||
provider: LLMProvider = None,
|
||||
use_json_mode: bool = True,
|
||||
try_alternative_providers: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Call LLM API with the specified or default provider.
|
||||
|
||||
|
||||
Args:
|
||||
messages: List of message dicts with 'role' and 'content'
|
||||
model: Model name (uses provider default if not specified). Supports OpenRouter format (e.g., 'openai/gpt-4o')
|
||||
@@ -360,10 +379,10 @@ class LLMService:
|
||||
provider: Override the service's default provider
|
||||
use_json_mode: Whether to request JSON output format (default True for analysis, False for code generation)
|
||||
try_alternative_providers: Whether to try alternative providers when current provider fails with 403/402
|
||||
|
||||
|
||||
Returns:
|
||||
Generated text content
|
||||
|
||||
|
||||
Model Resolution Priority:
|
||||
1. If model is specified and matches a direct provider (openai/, google/, deepseek/, x-ai/),
|
||||
use that provider directly if its API key is configured
|
||||
@@ -378,10 +397,12 @@ class LLMService:
|
||||
if self.get_api_key(detected_provider):
|
||||
provider = detected_provider
|
||||
logger.debug(f"Auto-detected provider '{provider.value}' from model '{model}'")
|
||||
|
||||
|
||||
p = provider or self.provider
|
||||
cfg = load_addon_config()
|
||||
explicit_provider_name = str(cfg.get('llm', {}).get('provider') or os.getenv('LLM_PROVIDER', '')).strip().lower()
|
||||
explicit_provider_name = (
|
||||
str(cfg.get("llm", {}).get("provider") or os.getenv("LLM_PROVIDER", "")).strip().lower()
|
||||
)
|
||||
explicit_provider = None
|
||||
if explicit_provider_name:
|
||||
try:
|
||||
@@ -389,7 +410,7 @@ class LLMService:
|
||||
except ValueError:
|
||||
explicit_provider = None
|
||||
api_key = self.get_api_key(p)
|
||||
|
||||
|
||||
if not api_key:
|
||||
# If provider is explicitly configured by user, don't silently switch.
|
||||
if explicit_provider is not None and p == explicit_provider:
|
||||
@@ -399,102 +420,106 @@ class LLMService:
|
||||
)
|
||||
# If no API key for current provider, try to find any available provider
|
||||
if try_alternative_providers:
|
||||
for alt_provider in [LLMProvider.DEEPSEEK, LLMProvider.GROK, LLMProvider.OPENAI, LLMProvider.GOOGLE, LLMProvider.OPENROUTER]:
|
||||
for alt_provider in [
|
||||
LLMProvider.DEEPSEEK,
|
||||
LLMProvider.GROK,
|
||||
LLMProvider.OPENAI,
|
||||
LLMProvider.GOOGLE,
|
||||
LLMProvider.OPENROUTER,
|
||||
]:
|
||||
if alt_provider != p and self.get_api_key(alt_provider):
|
||||
logger.warning(f"No API key for {p.value}, switching to {alt_provider.value}")
|
||||
p = alt_provider
|
||||
api_key = self.get_api_key(p)
|
||||
break
|
||||
|
||||
|
||||
if not api_key:
|
||||
raise ValueError(f"API key not configured for provider: {p.value}. Please configure at least one LLM provider API key.")
|
||||
|
||||
raise ValueError(
|
||||
f"API key not configured for provider: {p.value}. Please configure at least one LLM provider API key."
|
||||
)
|
||||
|
||||
base_url = self.get_base_url(p)
|
||||
|
||||
|
||||
# Normalize model name for the provider
|
||||
original_model = model
|
||||
model = self._normalize_model_for_provider(model, p)
|
||||
|
||||
|
||||
config = load_addon_config()
|
||||
timeout = int(config.get(p.value, {}).get('timeout', 120))
|
||||
|
||||
timeout = int(config.get(p.value, {}).get("timeout", 120))
|
||||
|
||||
# Build model candidates
|
||||
models_to_try = [model]
|
||||
provider_default_model = PROVIDER_CONFIGS[p]["default_model"]
|
||||
if use_fallback:
|
||||
fallback = PROVIDER_CONFIGS[p].get("fallback_model")
|
||||
if fallback and fallback != model:
|
||||
models_to_try.append(fallback)
|
||||
|
||||
|
||||
last_error = None
|
||||
last_status_code = None
|
||||
|
||||
|
||||
for current_model in models_to_try:
|
||||
try:
|
||||
if p == LLMProvider.GOOGLE:
|
||||
return self._call_google_gemini(
|
||||
messages, current_model, temperature,
|
||||
api_key, base_url, timeout
|
||||
)
|
||||
return self._call_google_gemini(messages, current_model, temperature, api_key, base_url, timeout)
|
||||
else:
|
||||
# OpenAI-compatible providers
|
||||
return self._call_openai_compatible(
|
||||
messages, current_model, temperature,
|
||||
api_key, base_url, timeout,
|
||||
use_json_mode=use_json_mode
|
||||
messages, current_model, temperature, api_key, base_url, timeout, use_json_mode=use_json_mode
|
||||
)
|
||||
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
error_detail = e.response.text if e.response else str(e)
|
||||
status_code = e.response.status_code if e.response else None
|
||||
last_status_code = status_code
|
||||
|
||||
|
||||
logger.error(f"{p.value} API HTTP error ({current_model}): {status_code} - {error_detail}")
|
||||
last_error = str(e)
|
||||
|
||||
|
||||
# 403/402 errors usually mean API key issue - try alternative provider
|
||||
if status_code in (402, 403) and try_alternative_providers and current_model == models_to_try[-1]:
|
||||
# Only try alternative providers after all models in current provider failed
|
||||
logger.warning(f"{p.value} returned {status_code} (likely API key issue). Trying alternative providers...")
|
||||
return self._try_alternative_providers(
|
||||
messages, original_model, temperature,
|
||||
use_json_mode, excluded_provider=p
|
||||
logger.warning(
|
||||
f"{p.value} returned {status_code} (likely API key issue). Trying alternative providers..."
|
||||
)
|
||||
|
||||
return self._try_alternative_providers(
|
||||
messages, original_model, temperature, use_json_mode, excluded_provider=p
|
||||
)
|
||||
|
||||
# Check for recoverable errors - try fallback model
|
||||
# 402: Payment required, 403: Forbidden (invalid key), 404: Model not found, 429: Rate limit
|
||||
if status_code in (402, 403, 404, 429):
|
||||
logger.warning(f"{p.value} returned {status_code} for model {current_model}; trying fallback...")
|
||||
continue
|
||||
|
||||
|
||||
if not use_fallback or current_model == models_to_try[-1]:
|
||||
raise
|
||||
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"{p.value} API request error ({current_model}): {str(e)}")
|
||||
last_error = str(e)
|
||||
if not use_fallback or current_model == models_to_try[-1]:
|
||||
raise
|
||||
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"Model {current_model} returned invalid data: {str(e)}")
|
||||
last_error = str(e)
|
||||
if current_model == models_to_try[-1]:
|
||||
raise
|
||||
|
||||
|
||||
error_msg = f"All model calls failed for {p.value}. Last error: {last_error}"
|
||||
if last_status_code in (402, 403):
|
||||
error_msg += f"\nStatus {last_status_code} usually means: API key invalid/expired, insufficient balance, or no access to model."
|
||||
error_msg += f"\nPlease check your {p.value} API key configuration and account balance."
|
||||
|
||||
|
||||
logger.error(error_msg)
|
||||
raise Exception(error_msg)
|
||||
|
||||
def _try_alternative_providers(self, messages: list, model: str, temperature: float,
|
||||
use_json_mode: bool, excluded_provider: LLMProvider = None) -> str:
|
||||
|
||||
def _try_alternative_providers(
|
||||
self, messages: list, model: str, temperature: float, use_json_mode: bool, excluded_provider: LLMProvider = None
|
||||
) -> str:
|
||||
"""
|
||||
Try alternative providers when current provider fails.
|
||||
|
||||
|
||||
Priority: DeepSeek > Grok > OpenAI > Google > OpenRouter
|
||||
"""
|
||||
priority_order = [
|
||||
@@ -504,91 +529,107 @@ class LLMService:
|
||||
LLMProvider.GOOGLE,
|
||||
LLMProvider.OPENROUTER,
|
||||
]
|
||||
|
||||
|
||||
for alt_provider in priority_order:
|
||||
if alt_provider == excluded_provider:
|
||||
continue
|
||||
|
||||
|
||||
api_key = self.get_api_key(alt_provider)
|
||||
if not api_key:
|
||||
continue
|
||||
|
||||
|
||||
logger.info(f"Trying alternative provider: {alt_provider.value}")
|
||||
try:
|
||||
return self.call_llm_api(
|
||||
messages, model, temperature,
|
||||
use_fallback=True, provider=alt_provider,
|
||||
messages,
|
||||
model,
|
||||
temperature,
|
||||
use_fallback=True,
|
||||
provider=alt_provider,
|
||||
use_json_mode=use_json_mode,
|
||||
try_alternative_providers=False # Prevent infinite recursion
|
||||
try_alternative_providers=False, # Prevent infinite recursion
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Alternative provider {alt_provider.value} also failed: {str(e)}")
|
||||
continue
|
||||
|
||||
raise Exception(f"All LLM providers failed. Please check your API key configurations.")
|
||||
|
||||
raise Exception("All LLM providers failed. Please check your API key configurations.")
|
||||
|
||||
# Legacy method for backward compatibility
|
||||
def call_openrouter_api(self, messages: list, model: str = None, temperature: float = 0.7, use_fallback: bool = True) -> str:
|
||||
def call_openrouter_api(
|
||||
self, messages: list, model: str = None, temperature: float = 0.7, use_fallback: bool = True
|
||||
) -> str:
|
||||
"""Call LLM API (legacy method name for backward compatibility)."""
|
||||
return self.call_llm_api(messages, model, temperature, use_fallback)
|
||||
|
||||
def safe_call_llm(self, system_prompt: str, user_prompt: str, default_structure: Dict[str, Any],
|
||||
model: str = None, provider: LLMProvider = None) -> Dict[str, Any]:
|
||||
def safe_call_llm(
|
||||
self,
|
||||
system_prompt: str,
|
||||
user_prompt: str,
|
||||
default_structure: Dict[str, Any],
|
||||
model: str = None,
|
||||
provider: LLMProvider = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Safe LLM call with robust JSON parsing and fallback structure."""
|
||||
response_text = ""
|
||||
try:
|
||||
response_text = self.call_llm_api([
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt}
|
||||
], model=model, provider=provider)
|
||||
|
||||
response_text = self.call_llm_api(
|
||||
[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}],
|
||||
model=model,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
# Strip markdown fences if present
|
||||
clean_text = response_text.strip()
|
||||
if clean_text.startswith("```"):
|
||||
first_newline = clean_text.find("\n")
|
||||
if first_newline != -1:
|
||||
clean_text = clean_text[first_newline+1:]
|
||||
clean_text = clean_text[first_newline + 1 :]
|
||||
if clean_text.endswith("```"):
|
||||
clean_text = clean_text[:-3]
|
||||
clean_text = clean_text.strip()
|
||||
|
||||
|
||||
# Parse JSON
|
||||
result = json.loads(clean_text)
|
||||
return result
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"JSON parse failed. Raw text: {response_text[:200] if response_text else 'N/A'}")
|
||||
|
||||
|
||||
# Try extracting JSON substring
|
||||
try:
|
||||
if response_text:
|
||||
start = response_text.find('{')
|
||||
end = response_text.rfind('}') + 1
|
||||
start = response_text.find("{")
|
||||
end = response_text.rfind("}") + 1
|
||||
if start >= 0 and end > start:
|
||||
result = json.loads(response_text[start:end])
|
||||
return result
|
||||
except:
|
||||
pass
|
||||
|
||||
default_structure['report'] = f"Failed to parse analysis result JSON. Raw output (partial): {response_text[:500] if response_text else 'N/A'}"
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to extract JSON substring: {e}")
|
||||
|
||||
default_structure["report"] = (
|
||||
f"Failed to parse analysis result JSON. Raw output (partial): {response_text[:500] if response_text else 'N/A'}"
|
||||
)
|
||||
return default_structure
|
||||
except Exception as e:
|
||||
logger.error(f"LLM call failed: {str(e)}")
|
||||
default_structure['report'] = f"Analysis failed: {str(e)}"
|
||||
default_structure["report"] = f"Analysis failed: {str(e)}"
|
||||
return default_structure
|
||||
|
||||
@classmethod
|
||||
def get_available_providers(cls) -> List[Dict[str, Any]]:
|
||||
"""Get list of available (configured) providers."""
|
||||
providers = []
|
||||
|
||||
|
||||
for p in LLMProvider:
|
||||
service = cls()
|
||||
api_key = service.get_api_key(p)
|
||||
providers.append({
|
||||
"id": p.value,
|
||||
"name": p.value.title(),
|
||||
"configured": bool(api_key),
|
||||
"default_model": PROVIDER_CONFIGS[p]["default_model"],
|
||||
})
|
||||
|
||||
providers.append(
|
||||
{
|
||||
"id": p.value,
|
||||
"name": p.value.title(),
|
||||
"configured": bool(api_key),
|
||||
"default_model": PROVIDER_CONFIGS[p]["default_model"],
|
||||
}
|
||||
)
|
||||
|
||||
return providers
|
||||
|
||||
Reference in New Issue
Block a user