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:
dienakdz
2026-04-09 14:30:51 +07:00
parent 103055b3df
commit 87f2845483
157 changed files with 19026 additions and 17773 deletions
+189 -148
View File
@@ -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