This commit is contained in:
2025-07-02 20:35:16 +00:00
parent 2d96dda5e2
commit 3bbf67dd4e

View File

@@ -35,10 +35,33 @@ import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import pandas as pd import pandas as pd
import openai import openai
# NEU: Korrigierte Imports für openai v1.x
from openai import APIError, RateLimitError, APIConnectionError, BadRequestError, AuthenticationError, Timeout
from config import (Config, BRANCH_MAPPING_FILE, URL_CHECK_MARKER, USER_AGENTS, LOG_DIR) from config import (Config, BRANCH_MAPPING_FILE, URL_CHECK_MARKER, USER_AGENTS, LOG_DIR)
# ==============================================================================
# UNIVERSAL OPENAI v0.x / v1.x IMPORTS
# ==============================================================================
# This block makes the code compatible with both old (v0.x) and new (v1.x)
# versions of the OpenAI library.
try:
# Attempt to import from the new (v1.x) structure
from openai import APIError, RateLimitError, APIConnectionError, BadRequestError, AuthenticationError, Timeout
IS_OPENAI_V1 = True
logging.info("OpenAI library v1.x or higher detected.")
except ImportError:
# Fallback to the old (v0.x) structure
from openai.error import (
APIError,
RateLimitError,
APIConnectionError,
InvalidRequestError as BadRequestError, # Alias für Kompatibilität
AuthenticationError,
Timeout,
OpenAIError
)
IS_OPENAI_V1 = False
logging.info("Legacy OpenAI library v0.x detected.")
# Optionale Bibliotheken # Optionale Bibliotheken
try: try:
import tiktoken import tiktoken
@@ -94,6 +117,7 @@ def retry_on_failure(func):
""" """
Decorator, der eine Funktion bei bestimmten Fehlern mehrmals wiederholt. Decorator, der eine Funktion bei bestimmten Fehlern mehrmals wiederholt.
Implementiert exponentiellen Backoff mit Jitter. Implementiert exponentiellen Backoff mit Jitter.
Ist kompatibel mit openai v0.x und v1.x.
""" """
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
func_name = func.__name__ func_name = func.__name__
@@ -108,8 +132,8 @@ def retry_on_failure(func):
return func(*args, **kwargs) return func(*args, **kwargs)
except Exception as e: except Exception as e:
decorator_logger.error(f"FEHLER bei '{effective_func_name}' (keine Retries konfiguriert). {type(e).__name__} - {str(e)[:150]}...") decorator_logger.error(f"FEHLER bei '{effective_func_name}' (keine Retries konfiguriert). {type(e).__name__} - {str(e)[:150]}...")
if not isinstance(e, (requests.exceptions.RequestException, gspread.exceptions.APIError, OpenAIError, wikipedia.exceptions.WikipediaException)): # Wir fangen hier jetzt alle Fehler, da die spezifischen unten sind.
decorator_logger.exception("Details zum Fehler:") decorator_logger.exception("Details zum Fehler:")
raise e raise e
for attempt in range(max_retries_config): for attempt in range(max_retries_config):
@@ -118,11 +142,13 @@ def retry_on_failure(func):
decorator_logger.warning(f"Wiederhole Versuch {attempt + 1}/{max_retries_config} fuer '{effective_func_name}'...") decorator_logger.warning(f"Wiederhole Versuch {attempt + 1}/{max_retries_config} fuer '{effective_func_name}'...")
return func(*args, **kwargs) return func(*args, **kwargs)
# Fehler, die NICHT wiederholt werden sollen
except (gspread.exceptions.SpreadsheetNotFound, AuthenticationError, ValueError, BadRequestError) as e: except (gspread.exceptions.SpreadsheetNotFound, AuthenticationError, ValueError, BadRequestError) as e:
decorator_logger.critical(f"❌ ENDGUELTIGER FEHLER bei '{effective_func_name}': Permanentes Problem erkannt. {type(e).__name__} - {str(e)[:150]}...") decorator_logger.critical(f"❌ ENDGUELTIGER FEHLER bei '{effective_func_name}': Permanentes Problem erkannt. {type(e).__name__} - {str(e)[:150]}...")
decorator_logger.exception("Details:") decorator_logger.exception("Details:")
raise e raise e
# HTTP-Fehler, die NICHT wiederholt werden sollen
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
if hasattr(e, 'response') and e.response is not None: if hasattr(e, 'response') and e.response is not None:
status_code = e.response.status_code status_code = e.response.status_code
@@ -131,26 +157,30 @@ def retry_on_failure(func):
decorator_logger.critical(f"❌ ENDGUELTIGER FEHLER bei '{effective_func_name}': HTTP Fehler {status_code} erhalten ({e.response.reason}). Nicht wiederholbar. {str(e)[:100]}...") decorator_logger.critical(f"❌ ENDGUELTIGER FEHLER bei '{effective_func_name}': HTTP Fehler {status_code} erhalten ({e.response.reason}). Nicht wiederholbar. {str(e)[:100]}...")
decorator_logger.exception("Details:") decorator_logger.exception("Details:")
raise e raise e
# Wenn der HTTP-Fehler wiederholbar ist (z.B. 500), wird er unten gefangen
pass
# Fehler, die wiederholt werden sollen (inkl. OpenAI-Fehler)
except (requests.exceptions.RequestException, gspread.exceptions.APIError, APIError, wikipedia.exceptions.WikipediaException) as e: except (requests.exceptions.RequestException, gspread.exceptions.APIError, APIError, wikipedia.exceptions.WikipediaException) as e:
error_msg = str(e) error_msg = str(e)
error_type = type(e).__name__ error_type = type(e).__name__
if attempt < max_retries_config - 1: if attempt < max_retries_config - 1:
wait_time = base_delay * (2 ** attempt) + random.uniform(0, 1) wait_time = base_delay * (2 ** attempt) + random.uniform(0, 1)
# Spezifisches Logging für OpenAI-Fehler
if isinstance(e, RateLimitError): if isinstance(e, RateLimitError):
decorator_logger.warning(f"🚦 RATE LIMIT ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...") decorator_logger.warning(f"🚦 OPENAI RATE LIMIT ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...")
elif isinstance(e, Timeout) and isinstance(e, OpenAIError): elif isinstance(e, Timeout):
decorator_logger.warning(f" OPENAI TIMEOUT ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...") decorator_logger.warning(f"⏰ TIMEOUT ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...")
elif isinstance(e, APIError): # Fängt alle anderen wiederholbaren OpenAI-Fehler
decorator_logger.warning(f"🤖 OPENAI API FEHLER ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...")
# Spezifisches Logging für andere Bibliotheken
elif isinstance(e, gspread.exceptions.APIError) and hasattr(e, 'response') and e.response is not None and e.response.status_code == 429: elif isinstance(e, gspread.exceptions.APIError) and hasattr(e, 'response') and e.response is not None and e.response.status_code == 429:
decorator_logger.warning(f"🚦 GSPREAD RATE LIMIT ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...") decorator_logger.warning(f"🚦 GSPREAD RATE LIMIT ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...")
elif isinstance(e, requests.exceptions.Timeout):
decorator_logger.warning(f"⏰ REQUESTS TIMEOUT ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...")
elif isinstance(e, requests.exceptions.RequestException): elif isinstance(e, requests.exceptions.RequestException):
decorator_logger.warning(f"🌐 NETZWERKFEHLER ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...") decorator_logger.warning(f"🌐 NETZWERKFEHLER ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...")
elif isinstance(e, OpenAIError):
decorator_logger.warning(f"🤖 OPENAI FEHLER ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...")
elif isinstance(e, wikipedia.exceptions.WikipediaException): elif isinstance(e, wikipedia.exceptions.WikipediaException):
decorator_logger.warning(f"📚 WIKIPEDIA FEHLER ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...") decorator_logger.warning(f"📚 WIKIPEDIA FEHLER ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...")
else: else:
@@ -739,21 +769,12 @@ def initialize_target_schema():
def call_openai_chat(prompt, temperature=0.3, model=None): def call_openai_chat(prompt, temperature=0.3, model=None):
""" """
Zentrale Funktion fuer OpenAI Chat API Aufrufe. Zentrale Funktion fuer OpenAI Chat API Aufrufe.
Wird von anderen globalen Helfern oder DataProcessor Methoden aufgerufen. Kompatibel mit openai v0.x und v1.x.
Args:
prompt (str): Der Prompt-Text an die API.
temperature (float, optional): Die Temperatur fuer die Textgenerierung. Defaults to 0.3.
model (str, optional): Das zu verwendende OpenAI Modell. Defaults to Config.TOKEN_MODEL.
Returns:
str: Der bereinigte Antwortstring von der API.
Wirft Exception bei API-Fehlern nach Retries.
""" """
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if not Config.API_KEYS.get('openai'): if not Config.API_KEYS.get('openai'):
logger.error("Fehler: OpenAI API Key nicht konfiguriert.") logger.error("Fehler: OpenAI API Key nicht konfiguriert.")
raise openai.error.AuthenticationError("OpenAI API Key nicht konfiguriert.") raise AuthenticationError("OpenAI API Key nicht konfiguriert.") # Funktioniert in beiden Versionen
if not prompt or not isinstance(prompt, str) or not prompt.strip(): if not prompt or not isinstance(prompt, str) or not prompt.strip():
logger.error("Fehler: Leerer Prompt fuer OpenAI.") logger.error("Fehler: Leerer Prompt fuer OpenAI.")
@@ -762,35 +783,33 @@ def call_openai_chat(prompt, temperature=0.3, model=None):
current_model = model if model else getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo') current_model = model if model else getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')
try: try:
# NEU: OpenAI v1.x Client-Instanziierung if IS_OPENAI_V1:
# Es ist Best Practice, einen Client zu erstellen, anstatt die globalen Methoden zu verwenden. # Code für die neue v1.x Bibliothek
# Der API-Schlüssel wird automatisch aus der Umgebungsvariable oder der Konfiguration gelesen. client = openai.OpenAI(api_key=Config.API_KEYS.get('openai'))
client = openai.OpenAI(api_key=Config.API_KEYS.get('openai')) response = client.chat.completions.create(
model=current_model,
# logger.debug(f"Sende Prompt an OpenAI ({current_model})...") messages=[{"role": "user", "content": prompt}],
temperature=temperature
response = client.chat.completions.create( )
model=current_model, result = response.choices[0].message.content.strip() if response.choices and response.choices[0].message else ""
messages=[{"role": "user", "content": prompt}], else:
temperature=temperature # Code für die alte v0.x Bibliothek
) response = openai.ChatCompletion.create(
api_key=Config.API_KEYS.get('openai'), # explizit übergeben
if not response or not response.choices: model=current_model,
logger.error(f"OpenAI Call erfolgreich, aber keine Choices in der Antwort erhalten. Response: {str(response)[:200]}...") messages=[{"role": "user", "content": prompt}],
raise APIError("Keine Choices in OpenAI Antwort erhalten.", request=None, body=None) temperature=temperature
)
result = response.choices[0].message.content.strip() if response.choices[0].message and response.choices[0].message.content else "" result = response.choices[0].message.content.strip() if response.choices and response.choices[0].message else ""
if not result: if not result:
logger.warning(f"OpenAI Call erfolgreich, erhielt aber leeren Inhalt in der Antwort. Prompt Anfang: {prompt[:100]}...") logger.warning(f"OpenAI Call erfolgreich, erhielt aber leeren Inhalt in der Antwort.")
return "" return ""
return result return result
except Exception as e: except Exception as e:
# Wird vom @retry_on_failure Decorator gefangen und behandelt. raise e # Wird vom @retry_on_failure Decorator gefangen
# Wir heben die Exception erneut auf, damit der Decorator sie sehen kann.
raise e
def summarize_website_content(raw_text): def summarize_website_content(raw_text):