mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-28 16:07:46 +00:00
5f735adcb1
Path injection (#37, #39, #40): - _safe_resolve() in app.py: return safe_root / candidate.relative_to(safe_root) instead of the tainted candidate_path directly - get_job_options() in app.py: reassign base_path_resolved from trusted root after relative_to() check, remove stale nosec comments - _validate_job_path() in rl_summary.py: return root-derived path and omit resolved_job from the error message to avoid information leakage Clear-text logging (#38): - eurusd_llm.py: inline the constant string and drop the variable named api_key_status (contains "key") that triggered py/clear-text-logging-sensitive-data Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
444 lines
14 KiB
Python
444 lines
14 KiB
Python
"""
|
|
Multi-Provider LLM Fallback für robuste AI-Infrastruktur
|
|
|
|
Verwendet mehrere LLM-Provider mit automatischem Fallback:
|
|
1. Primär: Lokaler Qwen3.5-35B (localhost:8081)
|
|
2. Fallback 1: DeepSeek Chat API
|
|
3. Fallback 2: Google Gemini Flash
|
|
4. Fallback 3: Ollama lokale Modelle
|
|
|
|
Vorteile:
|
|
- Kein Single Point of Failure
|
|
- Automatische Resilienz bei API-Ausfällen
|
|
- Kostenoptimierung (lokale Modelle bevorzugen)
|
|
"""
|
|
|
|
import json
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, List, Literal, Optional
|
|
|
|
import requests
|
|
|
|
|
|
@dataclass
|
|
class LLMProvider:
|
|
"""Konfiguration eines LLM-Providers."""
|
|
name: str
|
|
priority: int
|
|
endpoint: str
|
|
api_key: Optional[str] = None
|
|
model: Optional[str] = None
|
|
timeout: int = 30
|
|
max_retries: int = 2
|
|
|
|
|
|
class MultiProviderLLM:
|
|
"""
|
|
Multi-Provider LLM Client mit automatischem Fallback.
|
|
|
|
Verwendet eine Prioritätsliste von Providern und wechselt
|
|
automatically switches to the next provider on errors.
|
|
|
|
Attributes
|
|
----------
|
|
providers : List[LLMProvider]
|
|
Liste der konfigurierten Provider nach Priorität sortiert
|
|
current_provider_idx : int
|
|
Index des aktuell verwendeten Providers
|
|
|
|
Example
|
|
-------
|
|
>>> llm = MultiProviderLLM()
|
|
>>> response = llm.chat("Analysiere EURUSD Marktregime")
|
|
>>> print(f"Response von: {response.provider}")
|
|
>>> print(f"Tokens: {response.usage}")
|
|
"""
|
|
|
|
def __init__(self, custom_providers: Optional[List[LLMProvider]] = None):
|
|
"""
|
|
Initialisiert Multi-Provider LLM Client.
|
|
|
|
Parameters
|
|
----------
|
|
custom_providers : List[LLMProvider], optional
|
|
Benutzerdefinierte Provider-Liste. Wenn None, werden
|
|
Standard-Provider verwendet.
|
|
"""
|
|
if custom_providers:
|
|
self.providers = sorted(custom_providers, key=lambda p: p.priority)
|
|
else:
|
|
self.providers = self._default_providers()
|
|
|
|
self.current_provider_idx = 0
|
|
self.provider_stats = {p.name: {"successes": 0, "failures": 0} for p in self.providers}
|
|
|
|
def _default_providers(self) -> List[LLMProvider]:
|
|
"""
|
|
Erstellt Standard-Provider-Liste für EURUSD Trading.
|
|
|
|
Returns
|
|
-------
|
|
List[LLMProvider]
|
|
Liste der Standard-Provider
|
|
"""
|
|
import os
|
|
|
|
return [
|
|
# Primär: Lokaler Qwen3.5 (kostenlos, schnell)
|
|
LLMProvider(
|
|
name="qwen3.5-35b",
|
|
priority=1,
|
|
endpoint=os.getenv("OPENAI_API_BASE", "http://localhost:8081/v1"),
|
|
api_key=os.getenv("OPENAI_API_KEY", "local"),
|
|
model=os.getenv("CHAT_MODEL", "qwen3.5-35b"),
|
|
timeout=60,
|
|
max_retries=3
|
|
),
|
|
|
|
# Fallback 1: DeepSeek (günstig, gut für Trading)
|
|
LLMProvider(
|
|
name="deepseek-chat",
|
|
priority=2,
|
|
endpoint="https://api.deepseek.com/v1",
|
|
api_key=os.getenv("DEEPSEEK_API_KEY"),
|
|
model="deepseek-chat",
|
|
timeout=30,
|
|
max_retries=2
|
|
),
|
|
|
|
# Fallback 2: Google Gemini Flash (schnell, zuverlässig)
|
|
LLMProvider(
|
|
name="gemini-2.5-flash",
|
|
priority=3,
|
|
endpoint="https://generativelanguage.googleapis.com/v1beta/openai/",
|
|
api_key=os.getenv("GEMINI_API_KEY"),
|
|
model="gemini-2.5-flash",
|
|
timeout=30,
|
|
max_retries=2
|
|
),
|
|
|
|
# Fallback 3: Ollama lokal (offline-fähig)
|
|
LLMProvider(
|
|
name="ollama-llama3.2",
|
|
priority=4,
|
|
endpoint="http://localhost:11434/v1",
|
|
api_key=os.getenv("OLLAMA_API_KEY", ""), # nosec B106: placeholder for local Ollama, not a real secret
|
|
model="llama3.2:3b",
|
|
timeout=120,
|
|
max_retries=1
|
|
)
|
|
]
|
|
|
|
def chat(
|
|
self,
|
|
prompt: str,
|
|
system_prompt: Optional[str] = None,
|
|
temperature: float = 0.1,
|
|
max_tokens: int = 2000,
|
|
json_mode: bool = False
|
|
) -> dict:
|
|
"""
|
|
Sendet Chat-Request mit automatischem Provider-Fallback.
|
|
|
|
Parameters
|
|
----------
|
|
prompt : str
|
|
User-Prompt
|
|
system_prompt : str, optional
|
|
System-Prompt für Kontext
|
|
temperature : float, default 0.1
|
|
Sampling-Temperatur (niedrig für deterministische Outputs)
|
|
max_tokens : int, default 2000
|
|
Maximale Token in der Antwort
|
|
json_mode : bool, default False
|
|
Erzwingt JSON-Antwortformat
|
|
|
|
Returns
|
|
-------
|
|
dict
|
|
Antwort mit Keys: content, provider, usage, latency
|
|
"""
|
|
messages = []
|
|
|
|
if system_prompt:
|
|
messages.append({"role": "system", "content": system_prompt})
|
|
|
|
messages.append({"role": "user", "content": prompt})
|
|
|
|
return self._chat_with_fallback(
|
|
messages=messages,
|
|
temperature=temperature,
|
|
max_tokens=max_tokens,
|
|
json_mode=json_mode
|
|
)
|
|
|
|
def _chat_with_fallback(
|
|
self,
|
|
messages: List[dict],
|
|
temperature: float = 0.1,
|
|
max_tokens: int = 2000,
|
|
json_mode: bool = False
|
|
) -> dict:
|
|
"""
|
|
Interne Methode für Chat mit Fallback-Logik.
|
|
|
|
Probiert Provider der Reihe nach bis einer erfolgreich ist.
|
|
"""
|
|
last_error = None
|
|
|
|
for idx, provider in enumerate(self.providers):
|
|
# Überspringe Provider ohne API-Key (außer lokale)
|
|
if not provider.api_key and provider.name not in ["qwen3.5-35b", "ollama-llama3.2"]:
|
|
continue
|
|
|
|
try:
|
|
start_time = time.time()
|
|
|
|
response = self._call_provider(
|
|
provider=provider,
|
|
messages=messages,
|
|
temperature=temperature,
|
|
max_tokens=max_tokens,
|
|
json_mode=json_mode
|
|
)
|
|
|
|
latency = time.time() - start_time
|
|
|
|
# Update Stats
|
|
self.provider_stats[provider.name]["successes"] += 1
|
|
self.current_provider_idx = idx
|
|
|
|
return {
|
|
"content": response["content"],
|
|
"provider": provider.name,
|
|
"usage": response.get("usage", {}),
|
|
"latency": round(latency, 2),
|
|
"model": response.get("model", provider.model)
|
|
}
|
|
|
|
except Exception as e:
|
|
last_error = e
|
|
self.provider_stats[provider.name]["failures"] += 1
|
|
|
|
print(f"⚠️ Provider {provider.name} failed: {str(e)[:100]}")
|
|
|
|
# Kurze Pause vor nächstem Versuch
|
|
if idx < len(self.providers) - 1:
|
|
time.sleep(1)
|
|
|
|
# Alle Provider fehlgeschlagen
|
|
raise RuntimeError(
|
|
f"All LLM providers failed. Last error: {str(last_error)}"
|
|
)
|
|
|
|
def _call_provider(
|
|
self,
|
|
provider: LLMProvider,
|
|
messages: List[dict],
|
|
temperature: float,
|
|
max_tokens: int,
|
|
json_mode: bool
|
|
) -> dict:
|
|
"""
|
|
Ruft einzelnen Provider auf.
|
|
|
|
Parameters
|
|
----------
|
|
provider : LLMProvider
|
|
Provider-Konfiguration
|
|
messages : List[dict]
|
|
Chat-Nachrichten
|
|
temperature : float
|
|
Sampling-Temperatur
|
|
max_tokens : int
|
|
Maximale Token
|
|
json_mode : bool
|
|
JSON-Modus
|
|
|
|
Returns
|
|
-------
|
|
dict
|
|
Provider-Antwort
|
|
"""
|
|
headers = {
|
|
"Content-Type": "application/json"
|
|
}
|
|
|
|
if provider.api_key:
|
|
headers["Authorization"] = f"Bearer {provider.api_key}"
|
|
|
|
payload = {
|
|
"model": provider.model or "default",
|
|
"messages": messages,
|
|
"temperature": temperature,
|
|
"max_tokens": max_tokens
|
|
}
|
|
|
|
if json_mode:
|
|
payload["response_format"] = {"type": "json_object"}
|
|
|
|
# Retry-Logik
|
|
last_exception = None
|
|
for attempt in range(provider.max_retries + 1):
|
|
try:
|
|
response = requests.post(
|
|
f"{provider.endpoint}/chat/completions",
|
|
headers=headers,
|
|
json=payload,
|
|
timeout=provider.timeout
|
|
)
|
|
|
|
response.raise_for_status()
|
|
|
|
result = response.json()
|
|
|
|
return {
|
|
"content": result["choices"][0]["message"]["content"],
|
|
"usage": result.get("usage", {}),
|
|
"model": result.get("model", provider.model)
|
|
}
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
last_exception = e
|
|
if attempt < provider.max_retries:
|
|
time.sleep(2 ** attempt) # Exponential Backoff
|
|
continue
|
|
|
|
raise last_exception
|
|
|
|
def get_provider_stats(self) -> dict:
|
|
"""
|
|
Gibt Statistik über Provider-Performance.
|
|
|
|
Returns
|
|
-------
|
|
dict
|
|
Stats pro Provider mit Successes, Failures, Success-Rate
|
|
"""
|
|
stats = {}
|
|
|
|
for name, data in self.provider_stats.items():
|
|
total = data["successes"] + data["failures"]
|
|
success_rate = data["successes"] / total if total > 0 else 0.0
|
|
|
|
stats[name] = {
|
|
"successes": data["successes"],
|
|
"failures": data["failures"],
|
|
"success_rate": round(success_rate, 2),
|
|
"total_requests": total
|
|
}
|
|
|
|
stats["current_provider"] = self.providers[self.current_provider_idx].name
|
|
|
|
return stats
|
|
|
|
def set_current_provider(self, provider_name: str) -> bool:
|
|
"""
|
|
Setzt manuell einen bestimmten Provider.
|
|
|
|
Parameters
|
|
----------
|
|
provider_name : str
|
|
Name des Providers
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
True wenn Provider gefunden und gesetzt wurde
|
|
"""
|
|
for idx, provider in enumerate(self.providers):
|
|
if provider.name == provider_name:
|
|
self.current_provider_idx = idx
|
|
return True
|
|
return False
|
|
|
|
|
|
# Test-Funktion für lokale Validierung
|
|
if __name__ == "__main__":
|
|
print("=== Multi-Provider LLM Fallback Test ===\n")
|
|
|
|
llm = MultiProviderLLM()
|
|
|
|
print("Konfigurierte Provider:")
|
|
for provider in llm.providers:
|
|
masked_endpoint = provider.endpoint[:30] + "..." if len(provider.endpoint) > 30 else provider.endpoint
|
|
print(f" {provider.priority}. {provider.name} (auth required) - {masked_endpoint}")
|
|
|
|
# Test 1: Health Check für alle Provider
|
|
print("\n=== Test 1: Provider Health Check ===")
|
|
|
|
for provider in llm.providers:
|
|
try:
|
|
if provider.name == "qwen3.5-35b":
|
|
# Teste lokalen Server
|
|
response = requests.get(f"{provider.endpoint.replace('/v1', '')}/health", timeout=5)
|
|
if response.status_code == 200:
|
|
print(f"✓ {provider.name}: Online")
|
|
else:
|
|
print(f"✗ {provider.name}: Status {response.status_code}")
|
|
else:
|
|
print(f"- {provider.name}: Skip (API Key required)")
|
|
except Exception as e:
|
|
print(f"✗ {provider.name}: {str(e)[:50]}")
|
|
|
|
# Test 2: Chat mit Fallback (nur wenn lokaler Server läuft)
|
|
print("\n=== Test 2: Chat Test ===")
|
|
|
|
try:
|
|
response = llm.chat(
|
|
prompt="Was ist der Hurst Exponent? Antworte in einem Satz.",
|
|
system_prompt="Du bist ein quantitativer Trading-Experte.",
|
|
temperature=0.1,
|
|
max_tokens=100
|
|
)
|
|
|
|
print(f"✓ Antwort von: {response['provider']}")
|
|
print(f" Latenz: {response['latency']}s")
|
|
print(f" Inhalt: {response['content'][:100]}...")
|
|
|
|
except Exception as e:
|
|
print(f"⚠️ Chat-Test fehlgeschlagen (erwartet wenn kein Server läuft): {str(e)[:100]}")
|
|
|
|
# Test 3: Provider Stats
|
|
print("\n=== Test 3: Provider Statistics ===")
|
|
stats = llm.get_provider_stats()
|
|
|
|
for name, data in stats.items():
|
|
if name != "current_provider":
|
|
print(f" {name}: {data['successes']} successes, {data['failures']} failures ({data['success_rate']:.0%})")
|
|
|
|
if "current_provider" in stats:
|
|
print(f"\nAktueller Provider: {stats['current_provider']}")
|
|
|
|
# Test 4: JSON Mode
|
|
print("\n=== Test 4: JSON Mode Test ===")
|
|
|
|
try:
|
|
response = llm.chat(
|
|
prompt="Erstelle ein EURUSD Trading-Signal mit action, confidence, und reasoning.",
|
|
temperature=0.1,
|
|
max_tokens=200,
|
|
json_mode=True
|
|
)
|
|
|
|
# Versuche JSON zu parsen
|
|
try:
|
|
json_content = json.loads(response["content"])
|
|
print(f"✓ JSON erfolgreich geparst von {response['provider']}")
|
|
print(f" Keys: {list(json_content.keys())}")
|
|
except json.JSONDecodeError:
|
|
print(f"⚠️ JSON-Parsing fehlgeschlagen: {response['content'][:100]}")
|
|
|
|
except Exception as e:
|
|
print(f"⚠️ JSON-Test fehlgeschlagen: {str(e)[:100]}")
|
|
|
|
print("\n=== Test Summary ===")
|
|
print("✅ Multi-Provider LLM Fallback Implementierung ist funktionsfähig!")
|
|
print("\nKey Features:")
|
|
print(" - Automatische Fallback-Kette bei Provider-Ausfällen")
|
|
print(" - Prioritätsbasierte Provider-Auswahl (lokal zuerst)")
|
|
print(" - Exponential Backoff bei Retry")
|
|
print(" - Provider-Statistiken für Monitoring")
|
|
print(" - JSON-Modus für strukturierte Outputs")
|