diff --git a/contact_grouping.py b/contact_grouping.py index d0509dae..9a8db8c9 100644 --- a/contact_grouping.py +++ b/contact_grouping.py @@ -1,6 +1,6 @@ # contact_grouping.py -__version__ = "v1.1.6" # Versionsnummer hochgezählt +__version__ = "v1.1.7" # Versionsnummer hochgezählt import logging import json @@ -21,45 +21,19 @@ 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()] - ) + logging.basicConfig(level=logging.DEBUG, format='%(asctime)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', - handlers=[ - logging.FileHandler(log_filename, encoding='utf-8'), - logging.StreamHandler() - ] - ) - - # 4. Logger von Drittanbieter-Bibliotheken beruhigen + logging.basicConfig(level=log_level, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[logging.FileHandler(log_filename, encoding='utf-8'), logging.StreamHandler()]) logging.getLogger("gspread").setLevel(logging.WARNING) logging.getLogger("oauth2client").setLevel(logging.WARNING) - - # WICHTIG: Die erste Log-Nachricht erfolgt erst NACH der vollständigen Konfiguration logging.info(f"Logging erfolgreich initialisiert. Log-Datei: {log_filename}") @@ -73,17 +47,15 @@ class ContactGrouper: self.logger.info("Lade Wissensbasis...") self.exact_match_map = self._load_json(EXACT_MATCH_FILE) self.keyword_rules = self._load_json(KEYWORD_RULES_FILE) - if self.exact_match_map is None or self.keyword_rules is None: self.logger.critical("Eine oder mehrere Wissensbasis-Dateien konnten nicht geladen werden. Abbruch.") return False - self.logger.info("Wissensbasis erfolgreich geladen.") return True def _load_json(self, file_path): if not os.path.exists(file_path): - self.logger.error(f"Wissensbasis-Datei '{file_path}' nicht gefunden. Bitte 'knowledge_base_builder.py' ausführen.") + self.logger.error(f"Wissensbasis-Datei '{file_path}' nicht gefunden.") return None try: with open(file_path, 'r', encoding='utf-8') as f: @@ -92,7 +64,7 @@ class ContactGrouper: self.logger.debug(f"'{file_path}' erfolgreich geparst.") return data except (json.JSONDecodeError, IOError) as e: - self.logger.error(f"Fehler beim Laden oder Parsen der Datei '{file_path}': {e}") + self.logger.error(f"Fehler beim Laden der Datei '{file_path}': {e}") return None def _normalize_job_title(self, job_title): @@ -144,8 +116,18 @@ class ContactGrouper: valid_departments = sorted([dept for dept in self.keyword_rules.keys() if dept != DEFAULT_DEPARTMENT]) + # --- NEUER, ROBUSTERER PROMPT --- prompt_parts = [ - "Du bist ein HR-Experte...", + "You are a specialized data processing tool. Your SOLE function is to receive a list of job titles and classify each one into a predefined department category.", + "--- VALID DEPARTMENT CATEGORIES ---", + ", ".join(valid_departments), + "\n--- RULES ---", + "1. You MUST classify EVERY job title into ONE of the valid categories.", + "2. Your response MUST be a single, valid JSON array of objects.", + "3. Each object MUST contain the keys 'job_title' and 'department'.", + "4. Your entire response MUST start with '[' and end with ']'.", + "5. You MUST NOT add any introductory text, explanations, summaries, or markdown formatting like ```json.", + "\n--- JOB TITLES TO CLASSIFY ---", json.dumps(job_titles_to_classify, ensure_ascii=False) ] prompt = "\n".join(prompt_parts) @@ -168,11 +150,11 @@ class ContactGrouper: 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------------------------------------") + self.logger.error(f"Fehler beim Parsen des extrahierten JSON: {e}") + self.logger.debug(f"--- EXTRAHIERTER JSON-STRING, DER FEHLER VERURSACHTE ---\n{json_str}\n------------------------------------") return {} except Exception as e: - self.logger.error(f"Ein unerwarteter Fehler ist bei der KI-Klassifizierung aufgetreten: {e}") + self.logger.error(f"Unerwarteter Fehler bei KI-Klassifizierung: {e}") return {} def _append_learnings_to_source(self, gsh, new_mappings_df): @@ -189,31 +171,28 @@ 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 oder konnte nicht geladen werden. Nichts zu tun.") + self.logger.warning(f"'{TARGET_SHEET_NAME}' ist leer. 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'] - if "Department" not in df.columns: df["Department"] = "" - self.logger.info("Starte regelbasierte Zuordnung (Stufe 1 & 2) für alle Zeilen...") + self.logger.info("Starte regelbasierte Zuordnung (Stufe 1 & 2)...") 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).") + self.logger.info(f"{len(undefined_df)} Jobtitel konnten nicht durch Regeln zugeordnet werden. Starte Stufe 3 (KI).") 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 @@ -223,7 +202,7 @@ class ContactGrouper: 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("Alle Jobtitel durch Regeln (Stufe 1 & 2) zugeordnet. Stufe 3 wird übersprungen.") self.logger.info("--- Zuordnungs-Statistik ---") stats = df['Department'].value_counts() @@ -237,7 +216,6 @@ class ContactGrouper: if success: self.logger.info(f"Ergebnisse erfolgreich in '{TARGET_SHEET_NAME}' geschrieben.") else: self.logger.error("Fehler beim Zurückschreiben der Daten.") - if __name__ == "__main__": setup_logging() logging.info(f"Starte contact_grouping.py v{__version__}") @@ -247,7 +225,7 @@ if __name__ == "__main__": grouper = ContactGrouper() if not grouper.load_knowledge_base(): - logging.critical("Skript-Abbruch aufgrund von Fehlern beim Laden der Wissensbasis.") + logging.critical("Skript-Abbruch: Fehler beim Laden der Wissensbasis.") sys.exit(1) grouper.process_contacts() \ No newline at end of file