v1.1.7 - Bugfix KI-Prompt & Zuverlässigkeit
- Bugfix: Behebt das Problem, bei dem die OpenAI API eine textuelle Antwort anstelle eines JSON-Arrays zurückgab. - Der Prompt für die KI-Klassifizierung (Stufe 3) wurde grundlegend überarbeitet, um Missverständnisse durch die KI zu verhindern. - Die KI wird nun als "Datenverarbeitungstool" instruiert und erhält einen unmissverständlichen Befehl mit strikten Ausgabe-Formatierungsregeln. - Dies erhöht die Zuverlässigkeit der KI-gestützten Klassifizierung erheblich und stellt sicher, dass die Antwort maschinell verarbeitbar ist.
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user