neue Version mit Wartezeit bei externen LLM Fehler
This commit is contained in:
parent
c9cf1b7e4c
commit
ecfdc67485
|
|
@ -2,8 +2,8 @@
|
||||||
FILE: app/config.py
|
FILE: app/config.py
|
||||||
DESCRIPTION: Zentrale Pydantic-Konfiguration.
|
DESCRIPTION: Zentrale Pydantic-Konfiguration.
|
||||||
WP-20: Hybrid-Cloud Modus Support (OpenRouter/Gemini/Ollama).
|
WP-20: Hybrid-Cloud Modus Support (OpenRouter/Gemini/Ollama).
|
||||||
FIX: Update auf Gemini 2.5 Serie & Optimierung für Gemma 2 Durchsatz.
|
FIX: Einführung von Parametern zur intelligenten Rate-Limit Steuerung (429 Handling).
|
||||||
VERSION: 0.6.6
|
VERSION: 0.6.7
|
||||||
STATUS: Active
|
STATUS: Active
|
||||||
DEPENDENCIES: os, functools, pathlib, python-dotenv
|
DEPENDENCIES: os, functools, pathlib, python-dotenv
|
||||||
"""
|
"""
|
||||||
|
|
@ -27,32 +27,36 @@ class Settings:
|
||||||
VECTOR_SIZE: int = int(os.getenv("VECTOR_DIM", "768"))
|
VECTOR_SIZE: int = int(os.getenv("VECTOR_DIM", "768"))
|
||||||
DISTANCE: str = os.getenv("MINDNET_DISTANCE", "Cosine")
|
DISTANCE: str = os.getenv("MINDNET_DISTANCE", "Cosine")
|
||||||
|
|
||||||
# --- Lokale Embeddings ---
|
# --- Lokale Embeddings (Ollama & Sentence-Transformers) ---
|
||||||
EMBEDDING_MODEL: str = os.getenv("MINDNET_EMBEDDING_MODEL", "nomic-embed-text")
|
EMBEDDING_MODEL: str = os.getenv("MINDNET_EMBEDDING_MODEL", "nomic-embed-text")
|
||||||
MODEL_NAME: str = os.getenv("MINDNET_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
|
MODEL_NAME: str = os.getenv("MINDNET_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
|
||||||
|
|
||||||
# --- WP-20 Hybrid LLM Provider ---
|
# --- WP-20 Hybrid LLM Provider ---
|
||||||
# "openrouter" ist primär für den Ingest-Turbo mit Gemma 2 empfohlen.
|
# Erlaubt: "ollama" | "gemini" | "openrouter"
|
||||||
MINDNET_LLM_PROVIDER: str = os.getenv("MINDNET_LLM_PROVIDER", "openrouter").lower()
|
MINDNET_LLM_PROVIDER: str = os.getenv("MINDNET_LLM_PROVIDER", "openrouter").lower()
|
||||||
|
|
||||||
# Google AI Studio (Fallback auf 2.5-Serie)
|
# Google AI Studio (2025er Lite-Modell für höhere Kapazität)
|
||||||
GOOGLE_API_KEY: str | None = os.getenv("GOOGLE_API_KEY")
|
GOOGLE_API_KEY: str | None = os.getenv("GOOGLE_API_KEY")
|
||||||
# "gemini-2.5-flash-lite" ist die skalierbare 2025-Alternative für hohe Last.
|
|
||||||
GEMINI_MODEL: str = os.getenv("MINDNET_GEMINI_MODEL", "gemini-2.5-flash-lite")
|
GEMINI_MODEL: str = os.getenv("MINDNET_GEMINI_MODEL", "gemini-2.5-flash-lite")
|
||||||
|
|
||||||
# OpenRouter Integration (openai/gpt-oss-20b:free oder gemma-2)
|
# OpenRouter Integration (Verfügbares Free-Modell 2025)
|
||||||
OPENROUTER_API_KEY: str | None = os.getenv("OPENROUTER_API_KEY")
|
OPENROUTER_API_KEY: str | None = os.getenv("OPENROUTER_API_KEY")
|
||||||
# "google/gemma-2-9b-it:free" bietet hohe Kapazität bei Kostenfreiheit.
|
OPENROUTER_MODEL: str = os.getenv("OPENROUTER_MODEL", "mistralai/mistral-7b-instruct:free")
|
||||||
OPENROUTER_MODEL: str = os.getenv("OPENROUTER_MODEL", "google/gemma-2-9b-it:free")
|
|
||||||
|
|
||||||
LLM_FALLBACK_ENABLED: bool = os.getenv("MINDNET_LLM_FALLBACK", "true").lower() == "true"
|
LLM_FALLBACK_ENABLED: bool = os.getenv("MINDNET_LLM_FALLBACK", "true").lower() == "true"
|
||||||
|
|
||||||
|
# --- NEU: Intelligente Rate-Limit Steuerung ---
|
||||||
|
# Dauer der Wartezeit in Sekunden, wenn ein HTTP 429 (Rate Limit) auftritt
|
||||||
|
LLM_RATE_LIMIT_WAIT: float = float(os.getenv("MINDNET_LLM_RATE_LIMIT_WAIT", "60.0"))
|
||||||
|
# Anzahl der Cloud-Retries bei 429, bevor Ollama-Fallback greift
|
||||||
|
LLM_RATE_LIMIT_RETRIES: int = int(os.getenv("MINDNET_LLM_RATE_LIMIT_RETRIES", "3"))
|
||||||
|
|
||||||
# --- WP-05 Lokales LLM (Ollama) ---
|
# --- WP-05 Lokales LLM (Ollama) ---
|
||||||
OLLAMA_URL: str = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434")
|
OLLAMA_URL: str = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434")
|
||||||
LLM_MODEL: str = os.getenv("MINDNET_LLM_MODEL", "phi3:mini")
|
LLM_MODEL: str = os.getenv("MINDNET_LLM_MODEL", "phi3:mini")
|
||||||
PROMPTS_PATH: str = os.getenv("MINDNET_PROMPTS_PATH", "config/prompts.yaml")
|
PROMPTS_PATH: str = os.getenv("MINDNET_PROMPTS_PATH", "config/prompts.yaml")
|
||||||
|
|
||||||
# --- Performance & Last-Steuerung ---
|
# --- WP-06 / WP-14 Performance & Last-Steuerung ---
|
||||||
LLM_TIMEOUT: float = float(os.getenv("MINDNET_LLM_TIMEOUT", "300.0"))
|
LLM_TIMEOUT: float = float(os.getenv("MINDNET_LLM_TIMEOUT", "300.0"))
|
||||||
DECISION_CONFIG_PATH: str = os.getenv("MINDNET_DECISION_CONFIG", "config/decision_engine.yaml")
|
DECISION_CONFIG_PATH: str = os.getenv("MINDNET_DECISION_CONFIG", "config/decision_engine.yaml")
|
||||||
BACKGROUND_LIMIT: int = int(os.getenv("MINDNET_LLM_BACKGROUND_LIMIT", "2"))
|
BACKGROUND_LIMIT: int = int(os.getenv("MINDNET_LLM_BACKGROUND_LIMIT", "2"))
|
||||||
|
|
@ -62,8 +66,6 @@ class Settings:
|
||||||
MINDNET_VAULT_ROOT: str = os.getenv("MINDNET_VAULT_ROOT", "./vault_master")
|
MINDNET_VAULT_ROOT: str = os.getenv("MINDNET_VAULT_ROOT", "./vault_master")
|
||||||
MINDNET_TYPES_FILE: str = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
|
MINDNET_TYPES_FILE: str = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
|
||||||
MINDNET_VOCAB_PATH: str = os.getenv("MINDNET_VOCAB_PATH", "/mindnet/vault/mindnet/_system/dictionary/edge_vocabulary.md")
|
MINDNET_VOCAB_PATH: str = os.getenv("MINDNET_VOCAB_PATH", "/mindnet/vault/mindnet/_system/dictionary/edge_vocabulary.md")
|
||||||
|
|
||||||
# WP-22: 'full' für Multi-Hash Change Detection
|
|
||||||
CHANGE_DETECTION_MODE: str = os.getenv("MINDNET_CHANGE_DETECTION_MODE", "full")
|
CHANGE_DETECTION_MODE: str = os.getenv("MINDNET_CHANGE_DETECTION_MODE", "full")
|
||||||
|
|
||||||
# --- WP-04 Retriever Gewichte ---
|
# --- WP-04 Retriever Gewichte ---
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ DESCRIPTION: Hybrid-Client für Ollama, Google GenAI (Gemini) und OpenRouter.
|
||||||
Verwaltet provider-spezifische Prompts und Background-Last.
|
Verwaltet provider-spezifische Prompts und Background-Last.
|
||||||
WP-20: Optimiertes Fallback-Management zum Schutz von Cloud-Quoten.
|
WP-20: Optimiertes Fallback-Management zum Schutz von Cloud-Quoten.
|
||||||
WP-20 Fix: Bulletproof Prompt-Auflösung für format() Aufrufe.
|
WP-20 Fix: Bulletproof Prompt-Auflösung für format() Aufrufe.
|
||||||
WP-22/JSON: Optionales JSON-Schema + strict (für OpenRouter structured outputs),
|
WP-22/JSON: Optionales JSON-Schema + strict (für OpenRouter structured outputs).
|
||||||
OHNE Breaking Changes (neue Parameter nur am Ende).
|
FIX: Intelligente Rate-Limit Erkennung (429 Handling), v1-API Sync & Timeouts.
|
||||||
VERSION: 3.3.3
|
VERSION: 3.3.6
|
||||||
STATUS: Active
|
STATUS: Active
|
||||||
|
DEPENDENCIES: httpx, yaml, logging, asyncio, json, google-genai, openai, app.config
|
||||||
"""
|
"""
|
||||||
import httpx
|
import httpx
|
||||||
import yaml
|
import yaml
|
||||||
|
|
@ -47,7 +48,11 @@ class LLMService:
|
||||||
# 2. Google GenAI Client (Modern SDK)
|
# 2. Google GenAI Client (Modern SDK)
|
||||||
self.google_client = None
|
self.google_client = None
|
||||||
if self.settings.GOOGLE_API_KEY:
|
if self.settings.GOOGLE_API_KEY:
|
||||||
self.google_client = genai.Client(api_key=self.settings.GOOGLE_API_KEY)
|
# FIX: Wir erzwingen api_version 'v1' für höhere Stabilität bei 2.5er Modellen.
|
||||||
|
self.google_client = genai.Client(
|
||||||
|
api_key=self.settings.GOOGLE_API_KEY,
|
||||||
|
http_options={'api_version': 'v1'}
|
||||||
|
)
|
||||||
logger.info("✨ LLMService: Google GenAI (Gemini) active.")
|
logger.info("✨ LLMService: Google GenAI (Gemini) active.")
|
||||||
|
|
||||||
# 3. OpenRouter Client
|
# 3. OpenRouter Client
|
||||||
|
|
@ -55,7 +60,9 @@ class LLMService:
|
||||||
if self.settings.OPENROUTER_API_KEY:
|
if self.settings.OPENROUTER_API_KEY:
|
||||||
self.openrouter_client = AsyncOpenAI(
|
self.openrouter_client = AsyncOpenAI(
|
||||||
base_url="https://openrouter.ai/api/v1",
|
base_url="https://openrouter.ai/api/v1",
|
||||||
api_key=self.settings.OPENROUTER_API_KEY
|
api_key=self.settings.OPENROUTER_API_KEY,
|
||||||
|
# Strikter Timeout für OpenRouter Free-Tier zur Vermeidung von Hangs.
|
||||||
|
timeout=45.0
|
||||||
)
|
)
|
||||||
logger.info("🛰️ LLMService: OpenRouter Integration active.")
|
logger.info("🛰️ LLMService: OpenRouter Integration active.")
|
||||||
|
|
||||||
|
|
@ -84,7 +91,7 @@ class LLMService:
|
||||||
data = self.prompts.get(key, "")
|
data = self.prompts.get(key, "")
|
||||||
|
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
# Wir versuchen erst den Provider, dann Gemini (weil ähnlich leistungsfähig), dann Ollama
|
# Wir versuchen erst den Provider, dann Gemini, dann Ollama
|
||||||
val = data.get(active_provider, data.get("gemini", data.get("ollama", "")))
|
val = data.get(active_provider, data.get("gemini", data.get("ollama", "")))
|
||||||
|
|
||||||
# Falls val durch YAML-Fehler immer noch ein Dict ist, extrahiere ersten String
|
# Falls val durch YAML-Fehler immer noch ein Dict ist, extrahiere ersten String
|
||||||
|
|
@ -105,7 +112,6 @@ class LLMService:
|
||||||
priority: Literal["realtime", "background"] = "realtime",
|
priority: Literal["realtime", "background"] = "realtime",
|
||||||
provider: Optional[str] = None,
|
provider: Optional[str] = None,
|
||||||
model_override: Optional[str] = None,
|
model_override: Optional[str] = None,
|
||||||
# --- NEW (am Ende => rückwärtskompatibel!) ---
|
|
||||||
json_schema: Optional[Dict[str, Any]] = None,
|
json_schema: Optional[Dict[str, Any]] = None,
|
||||||
json_schema_name: str = "mindnet_json",
|
json_schema_name: str = "mindnet_json",
|
||||||
strict_json_schema: bool = True
|
strict_json_schema: bool = True
|
||||||
|
|
@ -116,40 +122,22 @@ class LLMService:
|
||||||
force_json:
|
force_json:
|
||||||
- Ollama: nutzt payload["format"]="json"
|
- Ollama: nutzt payload["format"]="json"
|
||||||
- Gemini: nutzt response_mime_type="application/json"
|
- Gemini: nutzt response_mime_type="application/json"
|
||||||
- OpenRouter: nutzt response_format=json_object (Fallback) oder json_schema (structured outputs)
|
- OpenRouter: nutzt response_format=json_object (Fallback) oder json_schema
|
||||||
|
|
||||||
json_schema + strict_json_schema (nur OpenRouter relevant):
|
|
||||||
- Wenn json_schema gesetzt ist UND force_json=True -> response_format.type="json_schema"
|
|
||||||
- strict_json_schema wird an OpenRouter/Provider weitergereicht (best effort je nach Provider)
|
|
||||||
"""
|
"""
|
||||||
target_provider = provider or self.settings.MINDNET_LLM_PROVIDER
|
target_provider = provider or self.settings.MINDNET_LLM_PROVIDER
|
||||||
|
|
||||||
if priority == "background":
|
if priority == "background":
|
||||||
async with LLMService._background_semaphore:
|
async with LLMService._background_semaphore:
|
||||||
return await self._dispatch(
|
return await self._dispatch(
|
||||||
target_provider,
|
target_provider, prompt, system, force_json,
|
||||||
prompt,
|
max_retries, base_delay, model_override,
|
||||||
system,
|
json_schema, json_schema_name, strict_json_schema
|
||||||
force_json,
|
|
||||||
max_retries,
|
|
||||||
base_delay,
|
|
||||||
model_override,
|
|
||||||
json_schema,
|
|
||||||
json_schema_name,
|
|
||||||
strict_json_schema
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return await self._dispatch(
|
return await self._dispatch(
|
||||||
target_provider,
|
target_provider, prompt, system, force_json,
|
||||||
prompt,
|
max_retries, base_delay, model_override,
|
||||||
system,
|
json_schema, json_schema_name, strict_json_schema
|
||||||
force_json,
|
|
||||||
max_retries,
|
|
||||||
base_delay,
|
|
||||||
model_override,
|
|
||||||
json_schema,
|
|
||||||
json_schema_name,
|
|
||||||
strict_json_schema
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _dispatch(
|
async def _dispatch(
|
||||||
|
|
@ -165,47 +153,73 @@ class LLMService:
|
||||||
json_schema_name: str,
|
json_schema_name: str,
|
||||||
strict_json_schema: bool
|
strict_json_schema: bool
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Routet die Anfrage an den physikalischen API-Provider."""
|
"""
|
||||||
try:
|
Routet die Anfrage mit intelligenter Rate-Limit Erkennung (WP-20 + WP-76).
|
||||||
if provider == "openrouter" and self.openrouter_client:
|
Schleife läuft über MINDNET_LLM_RATE_LIMIT_RETRIES.
|
||||||
return await self._execute_openrouter(
|
"""
|
||||||
prompt=prompt,
|
rate_limit_attempts = 0
|
||||||
system=system,
|
max_rate_retries = getattr(self.settings, "LLM_RATE_LIMIT_RETRIES", 3)
|
||||||
force_json=force_json,
|
wait_time = getattr(self.settings, "LLM_RATE_LIMIT_WAIT", 60.0)
|
||||||
model_override=model_override,
|
|
||||||
json_schema=json_schema,
|
|
||||||
json_schema_name=json_schema_name,
|
|
||||||
strict_json_schema=strict_json_schema
|
|
||||||
)
|
|
||||||
|
|
||||||
if provider == "gemini" and self.google_client:
|
while rate_limit_attempts <= max_rate_retries:
|
||||||
return await self._execute_google(prompt, system, force_json, model_override)
|
try:
|
||||||
|
if provider == "openrouter" and self.openrouter_client:
|
||||||
|
return await self._execute_openrouter(
|
||||||
|
prompt=prompt,
|
||||||
|
system=system,
|
||||||
|
force_json=force_json,
|
||||||
|
model_override=model_override,
|
||||||
|
json_schema=json_schema,
|
||||||
|
json_schema_name=json_schema_name,
|
||||||
|
strict_json_schema=strict_json_schema
|
||||||
|
)
|
||||||
|
|
||||||
# Default/Fallback zu Ollama
|
if provider == "gemini" and self.google_client:
|
||||||
return await self._execute_ollama(prompt, system, force_json, max_retries, base_delay)
|
return await self._execute_google(prompt, system, force_json, model_override)
|
||||||
|
|
||||||
except Exception as e:
|
# Default/Fallback zu Ollama
|
||||||
# QUOTEN-SCHUTZ: Wenn Cloud (OpenRouter/Gemini) fehlschlägt,
|
|
||||||
# gehen wir IMMER zu Ollama, niemals von OpenRouter zu Gemini.
|
|
||||||
if self.settings.LLM_FALLBACK_ENABLED and provider != "ollama":
|
|
||||||
logger.warning(
|
|
||||||
f"🔄 Provider {provider} failed: {e}. Falling back to LOCAL OLLAMA to protect cloud quotas."
|
|
||||||
)
|
|
||||||
return await self._execute_ollama(prompt, system, force_json, max_retries, base_delay)
|
return await self._execute_ollama(prompt, system, force_json, max_retries, base_delay)
|
||||||
raise e
|
|
||||||
|
except Exception as e:
|
||||||
|
err_str = str(e)
|
||||||
|
# Intelligente 429 Erkennung für alle Cloud-Provider
|
||||||
|
is_rate_limit = any(x in err_str for x in ["429", "RESOURCE_EXHAUSTED", "rate_limited", "Too Many Requests"])
|
||||||
|
|
||||||
|
if is_rate_limit and rate_limit_attempts < max_rate_retries:
|
||||||
|
rate_limit_attempts += 1
|
||||||
|
logger.warning(
|
||||||
|
f"⏳ [LLMService] Rate Limit (429) detected from {provider}. "
|
||||||
|
f"Attempt {rate_limit_attempts}/{max_rate_retries}. "
|
||||||
|
f"Waiting {wait_time}s before cloud retry..."
|
||||||
|
)
|
||||||
|
await asyncio.sleep(wait_time)
|
||||||
|
continue # Nächster Versuch in der Cloud-Schleife
|
||||||
|
|
||||||
|
# Wenn kein Rate-Limit oder Retries erschöpft -> Fallback zu Ollama (falls aktiviert)
|
||||||
|
if self.settings.LLM_FALLBACK_ENABLED and provider != "ollama":
|
||||||
|
logger.warning(
|
||||||
|
f"🔄 Provider {provider} failed ({err_str}). Falling back to LOCAL OLLAMA."
|
||||||
|
)
|
||||||
|
return await self._execute_ollama(prompt, system, force_json, max_retries, base_delay)
|
||||||
|
raise e
|
||||||
|
|
||||||
async def _execute_google(self, prompt, system, force_json, model_override):
|
async def _execute_google(self, prompt, system, force_json, model_override):
|
||||||
"""Native Google SDK Integration (Gemini)."""
|
"""Native Google SDK Integration (Gemini) mit v1 Fix."""
|
||||||
# Nutzt GEMINI_MODEL aus config.py falls kein override übergeben wurde
|
|
||||||
model = model_override or self.settings.GEMINI_MODEL
|
model = model_override or self.settings.GEMINI_MODEL
|
||||||
|
# Fix: Bereinige Modellnamen (Entfernung von 'models/' Präfix)
|
||||||
|
clean_model = model.replace("models/", "")
|
||||||
|
|
||||||
config = types.GenerateContentConfig(
|
config = types.GenerateContentConfig(
|
||||||
system_instruction=system,
|
system_instruction=system,
|
||||||
response_mime_type="application/json" if force_json else "text/plain"
|
response_mime_type="application/json" if force_json else "text/plain"
|
||||||
)
|
)
|
||||||
# SDK Call in Thread auslagern, da die Google API blocking sein kann
|
# Thread-Offloading mit striktem Timeout gegen "Hangs"
|
||||||
response = await asyncio.to_thread(
|
response = await asyncio.wait_for(
|
||||||
self.google_client.models.generate_content,
|
asyncio.to_thread(
|
||||||
model=model, contents=prompt, config=config
|
self.google_client.models.generate_content,
|
||||||
|
model=clean_model, contents=prompt, config=config
|
||||||
|
),
|
||||||
|
timeout=45.0
|
||||||
)
|
)
|
||||||
return response.text.strip()
|
return response.text.strip()
|
||||||
|
|
||||||
|
|
@ -215,21 +229,11 @@ class LLMService:
|
||||||
system: Optional[str],
|
system: Optional[str],
|
||||||
force_json: bool,
|
force_json: bool,
|
||||||
model_override: Optional[str],
|
model_override: Optional[str],
|
||||||
# --- NEW (optional) ---
|
|
||||||
json_schema: Optional[Dict[str, Any]] = None,
|
json_schema: Optional[Dict[str, Any]] = None,
|
||||||
json_schema_name: str = "mindnet_json",
|
json_schema_name: str = "mindnet_json",
|
||||||
strict_json_schema: bool = True
|
strict_json_schema: bool = True
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""OpenRouter API Integration (OpenAI-kompatibel) mit Schema-Support."""
|
||||||
OpenRouter API Integration (OpenAI-kompatibel).
|
|
||||||
|
|
||||||
force_json=True:
|
|
||||||
- Ohne json_schema -> response_format={"type":"json_object"}
|
|
||||||
- Mit json_schema -> response_format={"type":"json_schema", "json_schema": {..., "strict": True}}
|
|
||||||
|
|
||||||
Wichtig: response_format NICHT als None senden (robuster gegenüber SDK/Provider).
|
|
||||||
"""
|
|
||||||
# Nutzt OPENROUTER_MODEL aus config.py
|
|
||||||
model = model_override or self.settings.OPENROUTER_MODEL
|
model = model_override or self.settings.OPENROUTER_MODEL
|
||||||
messages = []
|
messages = []
|
||||||
if system:
|
if system:
|
||||||
|
|
@ -237,7 +241,6 @@ class LLMService:
|
||||||
messages.append({"role": "user", "content": prompt})
|
messages.append({"role": "user", "content": prompt})
|
||||||
|
|
||||||
kwargs: Dict[str, Any] = {}
|
kwargs: Dict[str, Any] = {}
|
||||||
|
|
||||||
if force_json:
|
if force_json:
|
||||||
if json_schema:
|
if json_schema:
|
||||||
kwargs["response_format"] = {
|
kwargs["response_format"] = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user