diff --git a/contact_grouping.py b/contact_grouping.py index e251b0a3..c1b15729 100644 --- a/contact_grouping.py +++ b/contact_grouping.py @@ -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() \ No newline at end of file