diff --git a/contact_grouping.py b/contact_grouping.py index 2ca3d6ce..d0509dae 100644 --- a/contact_grouping.py +++ b/contact_grouping.py @@ -1,6 +1,6 @@ # contact_grouping.py -__version__ = "v1.1.3" # Versionsnummer hochgezählt +__version__ = "v1.1.6" # Versionsnummer hochgezählt import logging import json @@ -21,9 +21,31 @@ 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.""" + # --- NEU: Robuste Konfiguration, die ein bestehendes Default-Setup überschreibt --- + + # 1. Dateinamen holen (dies kann das Logging implizit initialisieren) log_filename = create_log_filename("contact_grouping") + if not log_filename: + # Fallback, falls die Log-Datei nicht erstellt werden kann + print("KRITISCHER FEHLER: Log-Datei konnte nicht erstellt werden. Logge nur in die Konsole.") + logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[logging.StreamHandler()] + ) + return + log_level = logging.DEBUG + # 2. Bestehende Handler vom Root-Logger entfernen, um eine Neukonfiguration zu erzwingen + # Dies ist der entscheidende Schritt, um das "Silent Logging"-Problem zu beheben. + root_logger = logging.getLogger() + if root_logger.handlers: + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # 3. Unsere gewünschte Konfiguration anwenden logging.basicConfig( level=log_level, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', @@ -32,9 +54,13 @@ def setup_logging(): logging.StreamHandler() ] ) + + # 4. Logger von Drittanbieter-Bibliotheken beruhigen logging.getLogger("gspread").setLevel(logging.WARNING) logging.getLogger("oauth2client").setLevel(logging.WARNING) - logging.info(f"Logging initialisiert. Log-Datei: {log_filename}") + + # WICHTIG: Die erste Log-Nachricht erfolgt erst NACH der vollständigen Konfiguration + logging.info(f"Logging erfolgreich initialisiert. Log-Datei: {log_filename}") class ContactGrouper: @@ -96,13 +122,11 @@ class ContactGrouper: top_departments = [dept for dept, score in scores.items() if score == max_score] if len(top_departments) == 1: - # KORREKTUR: Hier wurde vorher die ganze Liste zurückgegeben. Jetzt wird das erste Element extrahiert. winner = top_departments[0] self.logger.debug(f"'{job_title}' -> '{winner}' (Stufe 2: Keyword Match, Score {max_score})") return winner best_priority = float('inf') - # KORREKTUR: Fallback-Gewinner ist jetzt ebenfalls ein String, kein Liste. winner = top_departments[0] for department in top_departments: priority = self.keyword_rules[department].get("priority", 99) @@ -114,40 +138,30 @@ class ContactGrouper: return winner def _get_ai_classification(self, job_titles_to_classify): - self.logger.info(f"Starte Stufe 3: Sende {len(job_titles_to_classify)} 'Undefined' Jobtitel zur KI-Klassifizierung...") + self.logger.info(f"Starte Stufe 3: Sende {len(job_titles_to_classify)} einzigartige 'Undefined' Jobtitel zur KI-Klassifizierung...") + self.logger.debug(f"Beispiel-Titel für KI: {job_titles_to_classify[:3]}") 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, 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 ---", + "Du bist ein HR-Experte...", json.dumps(job_titles_to_classify, ensure_ascii=False) ] prompt = "\n".join(prompt_parts) - response_str = "" # Initialisieren für den Fehlerfall + response_str = "" try: response_str = call_openai_chat(prompt, temperature=0.0, model="gpt-4o-mini", response_format_json=True) - - # --- 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.") @@ -175,24 +189,31 @@ class ContactGrouper: df = gsh.get_sheet_as_dataframe(TARGET_SHEET_NAME) if df is None or df.empty: - self.logger.warning(f"'{TARGET_SHEET_NAME}' ist leer. Nichts zu tun.") + self.logger.warning(f"'{TARGET_SHEET_NAME}' ist leer oder konnte nicht geladen werden. Nichts zu tun.") return + self.logger.info(f"{len(df)} Zeilen aus '{TARGET_SHEET_NAME}' erfolgreich geladen.") + df.columns = [col.strip() for col in df.columns] if "Job Title" not in df.columns: self.logger.critical(f"Benötigte Spalte 'Job Title' nicht gefunden. Abbruch.") return df['Original Job Title'] = df['Job Title'] - self.logger.info(f"{len(df)} Kontakte aus '{TARGET_SHEET_NAME}' geladen.") if "Department" not in df.columns: df["Department"] = "" + + self.logger.info("Starte regelbasierte Zuordnung (Stufe 1 & 2) für alle Zeilen...") df['Department'] = df['Job Title'].apply(self._find_best_match) + self.logger.info("Regelbasierte Zuordnung abgeschlossen.") undefined_df = df[df['Department'] == DEFAULT_DEPARTMENT] + if not undefined_df.empty: + self.logger.info(f"{len(undefined_df)} Jobtitel konnten nicht durch Regeln zugeordnet werden. Starte Stufe 3 (KI-Klassifizierung).") titles_to_classify = undefined_df['Job Title'].unique().tolist() ai_results_map = self._get_ai_classification(titles_to_classify) + df['Department'] = df.apply( lambda row: ai_results_map.get(row['Job Title'], row['Department']) if row['Department'] == DEFAULT_DEPARTMENT else row['Department'], axis=1 @@ -201,6 +222,8 @@ class ContactGrouper: new_learnings = [{'Job Title': title, 'Department': dept} for title, dept in ai_results_map.items()] if new_learnings: self._append_learnings_to_source(gsh, pd.DataFrame(new_learnings)) + else: + self.logger.info("Alle Jobtitel konnten erfolgreich durch Regeln (Stufe 1 & 2) zugeordnet werden. Stufe 3 wird übersprungen.") self.logger.info("--- Zuordnungs-Statistik ---") stats = df['Department'].value_counts()