This commit is contained in:
2025-05-05 14:50:21 +00:00
parent 59fac9a964
commit 1e61c16936

View File

@@ -228,10 +228,10 @@ logger = logging.getLogger(__name__)
import random # Für Jitter im Backoff
import time # Für sleep
# logging ist bereits importiert
import requests # Für requests.exceptions
import gspread # Für gspread.exceptions
import openai # Für openai.error
import wikipedia # Für wikipedia.exceptions
import requests # Für requests.exceptions (RequestException, HTTPError)
import gspread # Für gspread.exceptions (APIError, SpreadsheetNotFound)
import openai # Für openai.error (OpenAIError, AuthenticationError, InvalidRequestError etc.)
import wikipedia # Für wikipedia.exceptions (WikipediaException, PageError, DisambiguationError etc.)
# traceback ist bereits importiert
# re ist bereits importiert
# csv ist bereits importiert
@@ -252,7 +252,7 @@ decorator_logger = logging.getLogger(__name__ + ".Retry")
# --- Retry Decorator ---
# KORRIGIERTE Version
# KORRIGIERTE Version (Behandelt SpreadsheetNotFound und 404 HTTPError explizit)
def retry_on_failure(func):
"""
Decorator, der eine Funktion bei bestimmten Fehlern mehrmals wiederholt.
@@ -276,7 +276,7 @@ def retry_on_failure(func):
except Exception as e:
# Fehler loggen und weitergeben, wenn keine Retries konfiguriert sind
decorator_logger.error(f"FEHLER bei '{effective_func_name}' (keine Retries konfiguriert). {type(e).__name__} - {str(e)[:150]}...")
# Log traceback für unerwartete Fehler
# Log traceback für unerwartete Fehler (nicht die spezifischen API/Netzwerkfehler)
if not isinstance(e, (requests.exceptions.RequestException, gspread.exceptions.APIError, openai.error.OpenAIError, wikipedia.exceptions.WikipediaException)):
decorator_logger.exception("Details zum Fehler:")
raise e # Re-raise the exception
@@ -293,6 +293,28 @@ def retry_on_failure(func):
return func(*args, **kwargs) # Call the original function
# Spezifische Exceptions, die ein Retry rechtfertigen
# Fangen Sie nicht-wiederholbare Fehler separat
except (gspread.exceptions.SpreadsheetNotFound, openai.error.AuthenticationError, ValueError) as e:
# Diese Fehler deuten auf ein permanentes Problem hin (falsche URL, falscher Key, falsche Eingabe)
decorator_logger.critical(f"❌ ENDGÜLTIGER FEHLER bei '{effective_func_name}': Permanentes Problem erkannt. {type(e).__name__} - {str(e)[:150]}...")
decorator_logger.exception("Details:") # Log traceback für permanente Fehler
raise e # Leiten Sie diese Exception sofort weiter
# Fangen Sie Requests HTTP Errors (wie 404)
except requests.exceptions.HTTPError as e:
if e.response is not None:
status_code = e.response.status_code
if status_code in [404, 400]: # Fügen Sie hier weitere Status-Codes hinzu, die NICHT wiederholt werden sollen
decorator_logger.critical(f"❌ ENDGÜLTIGER FEHLER bei '{effective_func_name}': HTTP Fehler {status_code} erhalten ({e.response.reason}). Nicht wiederholbar. {str(e)[:150]}...")
decorator_logger.exception("Details:") # Log traceback
raise e # Leiten Sie diese nicht-wiederholbare Exception sofort weiter
# Ansonsten behandle HTTP Errors wie andere RequestExceptions (weiter unten)
# Wenn kein Response-Objekt oder kein spezifischer Statuscode gehandhabt wurde,
# lassen Sie diesen Fehler durchfallen zur allgemeinen RequestException Behandlung.
# Fangen Sie andere wiederholbare Exceptions (Netzwerk, Rate Limit, Timeout etc.)
except (requests.exceptions.RequestException, gspread.exceptions.APIError, openai.error.OpenAIError, wikipedia.exceptions.WikipediaException) as e:
error_msg = str(e)
error_type = type(e).__name__
@@ -304,20 +326,19 @@ def retry_on_failure(func):
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...")
elif isinstance(e, requests.exceptions.Timeout):
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, requests.exceptions.RequestException):
elif isinstance(e, requests.exceptions.RequestException): # Allgemeine 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...")
elif isinstance(e, openai.error.OpenAIError):
elif isinstance(e, openai.error.OpenAIError): # Allgemeine OpenAI Fehler
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): # Allgemeine Wikipedia Fehler
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: # Andere spezifisch behandelte Exceptions
decorator_logger.warning(f" BEHANDELTER FEHLER ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...")
else: # Andere wiederholbare Exceptions
decorator_logger.warning(f" WIEDERHOLBARER FEHLER ({error_type}) bei '{effective_func_name}' (Versuch {attempt+1}/{max_retries_config}). {error_msg[:150]}... Warte {wait_time:.2f}s...")
time.sleep(wait_time) # Warte vor dem nächsten Versuch
else: # Letzter Versuch fehlgeschlagen
decorator_logger.error(f"❌ ENDGÜLTIGER FEHLER bei '{effective_func_name}' nach {max_retries_config} Versuchen.")
# Log traceback für erwartete Fehler beim endgültigen Scheitern
# Log traceback nur bei unerwarteten Fehlern (nicht den, der zum Retry führte)
# decorator_logger.exception("Details zum endgültigen Fehler:") # Kann zu viel Lärm machen
raise e # Leite die ursprüngliche Exception weiter
@@ -332,21 +353,10 @@ def retry_on_failure(func):
# und eine Exception immer zu einer raise e Anweisung führt.
# Wenn die Schleife endet, ohne zurückzukehren oder eine Exception zu werfen,
# deutet dies auf einen Logikfehler im Decorator hin.
# Als Schutzmechanismus werfen wir hier eine RuntimeError, falls dieser Punkt doch erreicht wird.
# Oder geben None zurück, falls das erwartete Verhalten bei endgültigem Fehler None ist.
# Angesichts der Tatsache, dass die aufgerufenen Funktionen oft None bei Fehler zurückgeben,
# geben wir hier None zurück, anstatt eine RuntimeError zu werfen.
# Dies erfordert, dass die aufgerufene Funktion im Falle eines Fehlers IMMER eine Exception wirft,
# die vom Decorator gefangen und (beim letzten Versuch) weitergeleitet wird.
# Wenn die dekorierte Funktion selbst interne Fehler fängt und None zurückgibt,
# wird der Decorator dies als erfolgreichen Durchlauf interpretieren und None zurückgeben.
# Das ist das erwartete Verhalten.
# Entfernen Sie die RuntimeError und lassen Sie die Funktion implizit None zurückgeben,
# wenn der try/except/raise Block nicht erreicht wurde.
pass # Dies sollte nicht erreicht werden, wenn eine Exception geworfen wird.
raise RuntimeError(f"Retry decorator logic error: Loop completed unexpectedly for {effective_func_name}. This should not happen.")
# Die wrapper Funktion muss am Ende zurückgegeben werden
return wrapper
return wrapper # Gibt die Wrapper-Funktion zurück
# --- Token Count Funktion ---
@@ -471,7 +481,7 @@ def normalize_company_name(name):
"""Entfernt gängige Rechtsformzusätze etc. für Vergleiche."""
if not name: return ""
name = clean_text(name)
forms = [ r'gmbh', r'ges\.?\s*m\.?\s*b\.?\s*h\.?', r'gesellschaft mit beschränkter haftung', r'ug', r'u\.g\.', r'unternehmergesellschaft', r'haftungsbeschränkt', r'ag', r'a\.g\.', r'aktiengesellschaft', r'ohg', r'o\.h\.g\.', r'offene handelsgesellschaft', r'kg', r'k\.g\.', r'kommanditgesellschaft', r'gmbh\s*&\s*co\.?\s*kg', r'ges\.?\s*m\.?\s*b\.?\s*h\.?\s*&\s*co\.?\s*k\.g\.?', r'ag\s*&\s*co\.?\s*kg', r'a\.g\.?\s*&\s*co\.?\s*k\.g\.?', r'e\.k\.', r'e\.kfm\.', r'e\.kfr\.', r'eingetragene[rn]? kauffrau', r'eingetragene[rn]? kaufmann', r'ltd\.?', r'limited', r'ltd\s*&\s*co\.?\s*kg', r's\.?a\.?r\.?l\.?', r'sàrl', r'sagl', r's\.?a\.?', r'société anonyme', r'sociedad anónima', r's\.?p\.?a\.?', r'società per azioni', r'b\.?v\.?', r'besloten vennootschap', r'n\.?v\.?', r'naamloze vennootschap', r'plc\.?', r'public limited company', 'inc', 'incorporated', r'corp\.?', 'corporation', 'llc', 'limited liability company', r'kgaa', r'kommanditgesellschaft auf aktien', 'se', 'societas europaea', r'e\.?g\.?', r'eingetragene genossenschaft', 'genossenschaft', 'genmbh', r'e\.?v\.?', r'eingetragener verein', 'verein', 'stiftung', 'ggmbh', r'gemeinnützige gmbh', 'gug', 'partg', 'partnerschaftsgesellschaft', 'partgmbb', 'og', r'o\.g\.', 'offene gesellschaft', r'e\.u\.', 'eingetragenes unternehmen', r'ges\.?n\.?b\.?r\.?', r'gesellschaft nach bürgerlichem recht', 'kollektivgesellschaft', 'einzelfirma', 'gruppe', 'holding', 'international', 'systeme', 'technik', 'logistik', 'solutions', 'services', 'management', 'consulting', 'produktion', 'vertrieb', 'entwicklung', 'maschinenbau', 'anlagenbau'
forms = [ r'gmbh', r'ges\.?\s*m\.?\s*b\.?\s*h\.?', r'gesellschaft mit beschränkter haftung', r'ug', r'u\.g\.', r'unternehmergesellschaft', r'haftungsbeschränkt', r'ag', r'a\.g\.', r'aktiengesellschaft', r'ohg', r'o\.h\.g\.', r'offene handelsgesellschaft', r'kg', r'k\.g\.', r'kommanditgesellschaft', r'gmbh\s*&\s*co\.?\s*kg', r'ges\.?\s*m\.?\s*b\.?\s*h\.?\s*&\s*co\.?\s*k\.g\.?', r'ag\s*&\s*co\.?\s*kg', r'a\.g\.?\s*&\s*co\.?\s*k\.g\.?', r'e\.k\.', r'e\.kfm\.', r'e\.kfr\.', r'eingetragene[rn]? kauffrau', r'eingetragene[rn]? kaufmann', r'ltd\.?', r'limited', r'ltd\s*&\s*co\.?\s*kg', r's\.?a\.?r\.?l\.?', r'sàrl', r'sagl', r's\.?a\.?', r'société anonyme', r'sociedad anónima', r's\.?p\.?a\.?', r'società per azioni', r'b\.?v\.?', r'besloten vennootschap', r'n\.?v\.?', r'naamloze vennootschap', r'plc\.?', r'public limited company', 'inc', 'incorporated', r'corp\.?', 'corporation', 'llc', 'limited liability company', r'kgaa', r'kommanditgesellschaft auf aktien', 'se', 'societas europaea', r'e\.?g\.?', r'eingetragene genossenschaft', 'genossenschaft', 'genmbh', r'e\.?v\.?', r'eingetragener verein', 'verein', 'stiftung', 'ggmbh', r'gemeinnützige gmbh', r'gemeinnützige[rn]? gmbh', 'gug', 'partg', 'partnerschaftsgesellschaft', 'partgmbb', 'og', r'o\.g\.', 'offene gesellschaft', r'e\.u\.', 'eingetragenes unternehmen', r'ges\.?n\.?b\.?r\.?', r'gesellschaft nach bürgerlichem recht', 'kollektivgesellschaft', 'einzelfirma', 'gruppe', 'holding', 'international', 'systeme', 'technik', 'logistik', 'solutions', 'services', 'management', 'consulting', 'produktion', 'vertrieb', 'entwicklung', 'maschinenbau', 'anlagenbau'
]
# Pattern für ganze Wörter (case-insensitive)
# Fügen Sie \b hinzu, um sicherzustellen, dass ganze Wörter gematcht werden (z.B. nicht "ag" in "manage")
@@ -514,7 +524,7 @@ def extract_numeric_value(raw_value, is_umsatz=False):
# logger.debug(f"extract_numeric_value: Verarbeite Wert: '{raw_value_str}' -> '{processed_value}' (is_umsatz={is_umsatz})") # Zu viel Lärm
# Entferne gängige Präfixe/Suffixe und Spannen-Trennzeichen
processed_value = re.sub(r'(?i)^\s*(ca\.?|circa|rund|etwa|über|unter|mehr als|weniger als|bis zu)\s+', '', processed_value)
processed_value = re.sub(r'(?i)^\s*(ca\.?|circa|rund|etwa|über|under|mehr als|weniger als|bis zu)\s+', '', processed_value)
processed_value = re.sub(r'[€$£¥]', '', processed_value).strip()
processed_value = re.split(r'\s*(-||bis)\s*', processed_value, 1)[0].strip() # Nimm nur den ersten Teil bei Spannen
@@ -823,15 +833,16 @@ def call_openai_chat(prompt, temperature=0.3, model=None):
if not prompt:
logger.error("Fehler: Leerer Prompt für OpenAI.")
# Werfen Sie eine Value Error Exception
# Werfen Sie einen Value Error Exception
raise ValueError("Leerer Prompt für OpenAI.")
current_model = model if model else getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')
try:
# Token zählen vor dem Senden (optional, gut für Debugging/Monitoring)
# prompt_tokens = token_count(prompt, model=current_model)
# logger.debug(f"Sende Prompt an OpenAI ({current_model}, geschätzt {prompt_tokens} Tokens)...") # Zu viel Lärm
# try: prompt_tokens = token_count(prompt, model=current_model); logger.debug(f"Sende Prompt an OpenAI ({current_model}, geschätzt {prompt_tokens} Tokens)...");
# except Exception as e_tc: logger.debug(f"Fehler beim Token-Zählen: {e_tc}"); # Logge Fehler beim Token-Zählen
# Der OpenAI Call selbst kann Exceptions werfen (APIError, RateLimitError, InvalidRequestError etc.)
# Diese werden vom @retry_on_failure Decorator behandelt.
@@ -850,9 +861,8 @@ def call_openai_chat(prompt, temperature=0.3, model=None):
result = response.choices[0].message.content.strip()
# Token zählen für die Antwort (optional)
# completion_tokens = token_count(result, model=current_model)
# total_tokens = response.usage.total_tokens # Usage info kann direkt aus response kommen
# logger.debug(f"OpenAI Antwort erhalten ({completion_tokens} Completion Tokens, {total_tokens} Gesamt).") # Zu viel Lärm
# try: completion_tokens = token_count(result, model=current_model); total_tokens = response.usage.total_tokens; logger.debug(f"OpenAI Antwort erhalten ({completion_tokens} Completion Tokens, {total_tokens} Gesamt).");
# except Exception as e_tc: logger.debug(f"Fehler beim Token-Zählen der Antwort: {e_tc}"); # Logge Fehler beim Token-Zählen
return result # Gibt den bereinigten Antwortstring zurück
@@ -861,11 +871,7 @@ def call_openai_chat(prompt, temperature=0.3, model=None):
except Exception as e:
# Loggen Sie den unerwarteten Fehler
logger.error(f"Allgemeiner Fehler während OpenAI-Aufruf: {type(e).__name__} - {e}")
# Loggen Sie den Traceback für unerwartete Fehler
# logger.exception("Traceback des allgemeinen Fehlers:") # Der Retry-Decorator loggt das
# Werfen Sie die Exception erneut, damit der retry_on_failure Decorator sie fangen kann.
# Dies ist wichtig, damit der Decorator den Fehler erkennt und Retries durchführt.
raise e
@@ -886,7 +892,7 @@ def summarize_website_content(raw_text):
prompt = (
"Du bist ein KI-Assistent, der Webinhalte analysiert.\n"
"Fasse den folgenden Text einer Unternehmenswebsite prägnant zusammen. "
"Konzentriere dich auf:\n"
"Konzentriere dich dabei auf:\n"
"- Haupttätigkeitsfeld des Unternehmens\n"
"- Wichtigste Produkte und/oder Dienstleistungen\n"
"- Zielgruppe (falls erkennbar)\n\n"
@@ -897,7 +903,6 @@ def summarize_website_content(raw_text):
# Call_openai_chat nutzt den retry_on_failure Decorator.
# Wenn call_openai_chat nach Retries eine Exception wirft, wird diese hier nicht gefangen,
# sondern weitergereicht (z.B. an _process_single_row), was gut ist.
# Wenn call_openai_chat erfolgreich ist, gibt es den String oder None zurück (obwohl es jetzt Exception bei Fehlern wirft).
try:
summary = call_openai_chat(prompt, temperature=0.2)
return summary if summary and summary.strip() else "k.A. (Keine Zusammenfassung erhalten)"
@@ -922,6 +927,7 @@ def summarize_batch_openai(tasks_data):
dict: Ein Dictionary, das Zeilennummern auf ihre Zusammenfassungen mappt.
z.B. {2122: "Zusammenfassung A", 2123: "Zusammenfassung B"}
Bei Fehlern oder fehlenden Zusammenfassungen wird ein Fehlerstring verwendet.
Wirft Exception bei API-Fehlern nach Retries.
"""
if not tasks_data: return {}
@@ -975,28 +981,30 @@ def summarize_batch_openai(tasks_data):
final_prompt = "\n".join(prompt_parts)
# Optional: Token zählen zur Info, aber nicht zur Blockade
# try: prompt_tokens = token_count(final_prompt); logger.debug(f"Geschätzte Prompt-Tokens für Batch: {prompt_tokens} (Limit ca. 4096 für gpt-3.5-turbo)");
# try: prompt_tokens = token_count(final_prompt, model=getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')); logger.debug(f"Geschätzt Prompt-Tokens für Batch: {prompt_tokens}.");
# except Exception as e_tc: logger.debug(f"Fehler beim Token-Zählen: {e_tc}");
# --- OpenAI API Call (Die API wirft Fehler bei Token-Limit oder anderen Problemen) ---
# call_openai_chat nutzt den retry_on_failure Decorator und wirft bei endgültigem Fehler eine Exception
chat_response = None
# --- OpenAI API Call ---
# call_openai_chat nutzt den retry_on_failure Decorator und wirft bei endgültigem Fehler eine Exception.
# Der retry_on_failure Decorator DIESER summarize_batch_openai Funktion fängt die Exception
# und führt die Retries für die GESAMTE Batch-Funktion durch.
try:
chat_response = call_openai_chat(final_prompt, temperature=0.2)
# Wenn call_openai_chat erfolgreich ist, gibt es den String zurück.
# Exceptions werden nach Retries geworfen.
# Exceptions werden nach Retries geworfen und vom äußeren retry_on_failure dieser Funktion gefangen.
if not chat_response:
# Dieser Fall sollte nach der Änderung in call_openai_chat nicht mehr auftreten (würde Exception werfen)
logger.error("call_openai_chat gab unerwarteterweise None zurück für Batch-Zusammenfassung.")
# Behandeln Sie dies als Fehler für alle Zeilen im Batch
return {row_num: "FEHLER (OpenAI None Antwort)" for row_num in row_numbers_in_batch}
# Werfen Sie eine spezifische Exception, damit der äußere Decorator sie fängt
raise openai.error.APIError("Keine Antwort von OpenAI erhalten für Batch-Zusammenfassung.")
except Exception as e:
# Wenn call_openai_chat nach Retries eine Exception wirft
logger.error(f"Endgültiger FEHLER beim OpenAI-Batch-Aufruf für Zusammenfassung: {e}")
# Wenn call_openai_chat oder der äußere retry_on_failure eine Exception wirft
# Die Exception wird hier gefangen, bevor sie an den Aufrufer (DataProcessor Methode) weitergeleitet wird.
logger.error(f"Endgültiger FEHLER beim OpenAI-Batch-Aufruf für Zusammenfassung (innerhalb Batch Decorator): {e}")
# Geben Sie ein Dictionary zurück, das signalisiert, dass für alle Zeilen im Batch ein Fehler aufgetreten ist
return {row_num: f"FEHLER API: {str(e)[:100]}" for row_num in row_numbers_in_batch}
@@ -1033,7 +1041,7 @@ def summarize_batch_openai(tasks_data):
original_row_nums = {t['row_num'] for t in tasks_data}
for row_num in original_row_nums:
if row_num not in summaries:
summaries[row_num] = "k.A. (Ungültiger Rohtext im Batch)"
summaries[row_num] = "k.A. (Kein gültiger Rohtext im Batch)"
return summaries # Rückgabe des Dictionarys mit Ergebnissen oder Fehlern
@@ -1060,7 +1068,7 @@ def evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kateg
dict: Enthält "branch" (die finale, gültige Kurzform oder Fehler),
"consistency" ('ok', 'X', 'fallback_crm_valid', 'fallback_invalid', 'error_...'),
"justification" (Begründung von ChatGPT oder Fallback-Info).
Wirft Exception bei API-Fehlern.
Wirft Exception bei API-Fehlern (von call_openai_chat nach Retries).
"""
global ALLOWED_TARGET_BRANCHES, TARGET_SCHEMA_STRING
@@ -1245,7 +1253,7 @@ def serp_website_lookup(company_name):
serp_key = Config.API_KEYS.get('serpapi')
if not serp_key:
logger.error("Fehler: SerpAPI Key nicht verfügbar für Website Lookup.")
# Werfen Sie eine Exception, damit retry_on_failure dies behandeln kann
# Werfen Sie eine Exception, damit retry_on_failure dies behandelt (oder nicht, je nach Config)
raise ConnectionRefusedError("SerpAPI Key nicht konfiguriert.")
if not company_name or str(company_name).strip() == "":