v1.1.3 - Bugfix Robuste KI-Antwortverarbeitung

- Bugfix: Behebt einen `JSONDecodeError`, der auftrat, wenn die OpenAI API eine Antwort mit einleitendem Text oder ohne JSON-Array zurückgab.
- Die JSON-Extraktion in der KI-Klassifizierung wurde von einer einfachen String-Suche auf eine robuste Regex-Suche umgestellt.
- Verbessertes Fehler-Logging: Bei einem Fehler bei der KI-Klassifizierung wird nun die vollständige, rohe API-Antwort ins Log geschrieben, um die Fehlersuche zu erleichtern.
This commit is contained in:
2025-09-18 07:54:18 +00:00
parent 8664d77a4f
commit c70d6b3b60

View File

@@ -1,12 +1,12 @@
# contact_grouping.py
__version__ = "v1.1.2" # Versionsnummer hochgezählt
__version__ = "v1.1.3" # Versionsnummer hochgezählt
import logging
import json
import re
import os
import sys # NEU: Import für sauberen Abbruch
import sys
import pandas as pd
from google_sheet_handler import GoogleSheetHandler
@@ -21,7 +21,6 @@ KEYWORD_RULES_FILE = "keyword_rules.json"
DEFAULT_DEPARTMENT = "Undefined"
def setup_logging():
"""Konfiguriert das Logging, um sowohl in der Konsole als auch in einer Datei zu loggen."""
log_filename = create_log_filename("contact_grouping")
log_level = logging.DEBUG
@@ -39,21 +38,12 @@ def setup_logging():
class ContactGrouper:
"""
Kapselt die Logik zur automatischen Gruppierung von Kontakten
basierend auf ihrem Jobtitel. Inklusive Lernfunktion via KI.
"""
def __init__(self):
self.logger = logging.getLogger(__name__ + ".ContactGrouper")
# --- ÄNDERUNG: Initialisierung ist jetzt "faul" (lazy) ---
self.exact_match_map = None
self.keyword_rules = None
def load_knowledge_base(self):
"""
NEUE METHODE: Lädt die Wissensbasis-Dateien explizit.
Gibt True bei Erfolg, False bei Fehlern zurück.
"""
self.logger.info("Lade Wissensbasis...")
self.exact_match_map = self._load_json(EXACT_MATCH_FILE)
self.keyword_rules = self._load_json(KEYWORD_RULES_FILE)
@@ -66,7 +56,6 @@ class ContactGrouper:
return True
def _load_json(self, file_path):
"""Lädt eine JSON-Datei und gibt den Inhalt als Dictionary zurück."""
if not os.path.exists(file_path):
self.logger.error(f"Wissensbasis-Datei '{file_path}' nicht gefunden. Bitte 'knowledge_base_builder.py' ausführen.")
return None
@@ -79,8 +68,6 @@ class ContactGrouper:
except (json.JSONDecodeError, IOError) as e:
self.logger.error(f"Fehler beim Laden oder Parsen der Datei '{file_path}': {e}")
return None
# ... (alle anderen Methoden wie _normalize_job_title, _find_best_match etc. bleiben unverändert) ...
def _normalize_job_title(self, job_title):
if not isinstance(job_title, str): return ""
@@ -109,12 +96,12 @@ class ContactGrouper:
top_departments = [dept for dept, score in scores.items() if score == max_score]
if len(top_departments) == 1:
winner = top_departments[0]
winner = top_departments
self.logger.debug(f"'{job_title}' -> '{winner}' (Stufe 2: Keyword Match, Score {max_score})")
return winner
best_priority = float('inf')
winner = top_departments[0]
winner = top_departments
for department in top_departments:
priority = self.keyword_rules[department].get("priority", 99)
if priority < best_priority:
@@ -129,24 +116,47 @@ class ContactGrouper:
if not job_titles_to_classify: return {}
valid_departments = sorted([dept for dept in self.keyword_rules.keys() if dept != DEFAULT_DEPARTMENT])
prompt_parts = [
"Du bist ein HR-Experte...",
"Du bist ein HR-Experte, der Jobtitel präzise vordefinierten Abteilungen zuordnet.",
"Analysiere die folgende Liste von Jobtiteln.",
"Ordne JEDEN Jobtitel EINER der folgenden gültigen Abteilungen zu:",
", ".join(valid_departments),
"\nGib deine Antwort als valides JSON-Array von Objekten zurück, wobei jedes Objekt die Schlüssel 'job_title' und 'department' hat.",
"Stelle sicher, dass die Antwort ausschließlich das JSON-Array enthält, ohne einleitenden Text oder Markdown-Formatierung.",
"Beispiel: [{\"job_title\": \"Head of Fleet Management\", \"department\": \"Fuhrparkmanagement\"}]",
"\n--- Zu klassifizierende Jobtitel ---",
json.dumps(job_titles_to_classify, ensure_ascii=False)
]
prompt = "\n".join(prompt_parts)
response_str = "" # Initialisieren für den Fehlerfall
try:
response_str = call_openai_chat(prompt, temperature=0.0, model="gpt-4o-mini", response_format_json=True)
json_start = response_str.find('[')
json_end = response_str.rfind(']')
if json_start == -1 or json_end == -1: raise json.JSONDecodeError("Kein JSON-Array.", response_str, 0)
json_str = response_str[json_start : json_end + 1]
# --- NEU: Robuste Regex-basierte JSON-Extraktion ---
match = re.search(r'\[.*\]', response_str, re.DOTALL)
if not match:
# NEU: Verbessertes Logging, um die Roh-Antwort zu sehen
self.logger.error("Konnte kein JSON-Array in der KI-Antwort finden.")
self.logger.debug(f"--- VOLLSTÄNDIGE ROH-ANTWORT DER API ---\n{response_str}\n------------------------------------")
return {}
json_str = match.group(0)
results_list = json.loads(json_str)
classified_map = {item['job_title']: item['department'] for item in results_list if item.get('department') in valid_departments}
self.logger.info(f"{len(classified_map)} Jobtitel erfolgreich von der KI klassifiziert.")
return classified_map
except json.JSONDecodeError as e:
self.logger.error(f"Fehler beim Parsen des extrahierten JSON aus der KI-Antwort: {e}")
self.logger.debug(f"--- EXTRAHIERTER JSON-STRING, DER ZUM FEHLER FÜHRTE ---\n{json_str}\n------------------------------------")
return {}
except Exception as e:
self.logger.error(f"Fehler bei der KI-Klassifizierung: {e}")
self.logger.error(f"Ein unerwarteter Fehler ist bei der KI-Klassifizierung aufgetreten: {e}")
return {}
def _append_learnings_to_source(self, gsh, new_mappings_df):
@@ -158,9 +168,6 @@ class ContactGrouper:
else: self.logger.error("Fehler beim Anhängen der Lern-Daten.")
def process_contacts(self):
"""
Führt den eigentlichen Verarbeitungsprozess aus, nachdem die Wissensbasis geladen wurde.
"""
self.logger.info("Starte Kontakt-Verarbeitung...")
gsh = GoogleSheetHandler()
df = gsh.get_sheet_as_dataframe(TARGET_SHEET_NAME)
@@ -214,9 +221,8 @@ if __name__ == "__main__":
grouper = ContactGrouper()
# NEU: Expliziter Lade-Schritt mit Fehlerprüfung
if not grouper.load_knowledge_base():
logging.critical("Skript-Abbruch aufgrund von Fehlern beim Laden der Wissensbasis.")
sys.exit(1) # Beendet das Skript mit einem Fehlercode
sys.exit(1)
grouper.process_contacts()