From b26e836f8e512b867241ffd46c25a8ce7e37f8d5 Mon Sep 17 00:00:00 2001 From: Floke Date: Tue, 15 Apr 2025 13:33:53 +0000 Subject: [PATCH] bugfix --- brancheneinstufung.py | 291 +++++++++++++++++++++++++++++++----------- 1 file changed, 213 insertions(+), 78 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 6bed7aed..1f6586e5 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -443,45 +443,107 @@ def fuzzy_similarity(str1, str2): def load_branch_mapping(file_path=BRANCH_MAPPING_FILE): """Lädt Mapping extern -> Ziel-Branche aus CSV.""" mapping = {} + debug_print(f"Versuche, Mapping aus '{file_path}' zu laden...") # NEU + line_count = 0 # NEU try: with open(file_path, mode='r', encoding="utf-8") as f: reader = csv.reader(f) # Optional: Header überspringen, falls vorhanden - # next(reader, None) + # try: # NEU - Sicher Header überspringen + # header = next(reader) + # debug_print(f"CSV Header übersprungen: {header}") + # except StopIteration: + # debug_print("CSV ist leer oder hat keinen Header.") + # return mapping + # except Exception as e_header: + # debug_print(f"Fehler beim Lesen des Headers: {e_header}") + # return mapping + for row in reader: + line_count += 1 # NEU + if line_count <= 10 or line_count % 100 == 0: # Logge die ersten 10 und dann jede 100. Zeile + debug_print(f"Lese Zeile {line_count}: {row}") # NEU + if len(row) >= 2: - # Spalte A: Externer Begriff (normalisiert) - # Spalte B: Ziel-Branchenschema - key = normalize_string(row[0].strip()).lower() # Normalisieren für besseres Matching - value = row[1].strip() # Zielwert nicht normalisieren - if key and value: - # Falls der Key schon existiert, überschreibe oder logge einen Konflikt - if key in mapping: - debug_print(f"Warnung: Doppelter Mapping-Key '{key}' in {file_path}. Wert '{mapping[key]}' wird mit '{value}' überschrieben.") - mapping[key] = value + # Spalte A: Externer Begriff (normalisiert) - anpassen, falls nur eine Spalte relevant ist + # Spalte B: Ziel-Branchenschema - dies sollte die Kurzform sein + # WICHTIG: Annehmen, dass die Kurzform jetzt in Spalte A steht, da kein externes Mapping mehr? + # ODER steht die Kurzform in Spalte B und Spalte A ist jetzt leer/irrelevant? + # --> ANNAHME: Die Kurzform steht jetzt in Spalte A (Index 0) und ist der Zielwert. + + # key = normalize_string(row[0].strip()).lower() # Alter Ansatz + # value = row[1].strip() # Alter Ansatz + + # --> NEUER ANSATZ (Annahme: Kurzform in Spalte A): + target_branch = row[0].strip() + if target_branch: # Nur wenn nicht leer + # Wir brauchen kein Mapping mehr, nur die Liste der Ziele + # Also bauen wir das Set direkt auf + pass # Wird jetzt in load_target_schema erledigt + # --------- ENDE NEUER ANSATZ --------- + + + # if key and value: # Alter Ansatz + # if key in mapping: + # debug_print(f"Warnung: Doppelter Mapping-Key '{key}' in {file_path}. Wert '{mapping[key]}' wird mit '{value}' überschrieben.") + # mapping[key] = value except FileNotFoundError: debug_print(f"Fehler: Branchen-Mapping-Datei '{file_path}' nicht gefunden.") + return {} # Leeres Mapping zurückgeben except Exception as e: - debug_print(f"Fehler beim Laden des Branchen-Mappings aus '{file_path}': {e}") - return mapping + debug_print(f"Fehler beim Laden des Branchen-Mappings aus '{file_path}' (Zeile {line_count}): {e}") + return {} # Leeres Mapping zurückgeben + # debug_print(f"Mapping erfolgreich geladen. {len(mapping)} Einträge gefunden nach {line_count} gelesenen Zeilen.") # Alt + # return mapping # Alt + # Die Funktion lädt jetzt nichts mehr, load_target_schema erledigt das + return {} def load_target_schema(csv_filepath=BRANCH_MAPPING_FILE): - """Lädt Mapping, baut Schema-String und Liste erlaubter Ziele.""" + """Lädt Liste erlaubter Ziele (Kurzformen) aus Spalte A der CSV.""" global BRANCH_MAPPING, TARGET_SCHEMA_STRING, ALLOWED_TARGET_BRANCHES - BRANCH_MAPPING = load_branch_mapping(csv_filepath) + # BRANCH_MAPPING wird nicht mehr benötigt, wenn wir nur die Ziele laden + BRANCH_MAPPING = {} - allowed_branches_set = set(BRANCH_MAPPING.values()) - ALLOWED_TARGET_BRANCHES = sorted(list(allowed_branches_set), key=str.lower) + allowed_branches_set = set() + debug_print(f"Versuche, Ziel-Schema (Kurzformen) aus '{csv_filepath}' Spalte A zu laden...") # NEU + line_count = 0 + try: + with open(csv_filepath, encoding="utf-8") as f: + reader = csv.reader(f) + # Optional: Header überspringen + # next(reader, None) + for row in reader: + line_count += 1 + if line_count <= 10 or line_count % 100 == 0: + debug_print(f"Schema-Laden: Lese Zeile {line_count}: {row}") + + if len(row) >= 1: # Nur Spalte A (Index 0) wird benötigt + target = row[0].strip() + if target: # Nur nicht-leere Einträge hinzufügen + allowed_branches_set.add(target) + if line_count <= 10: # Logge die ersten 10 hinzugefügten + debug_print(f" -> '{target}' zum Set hinzugefügt.") + + except FileNotFoundError: + debug_print(f"Fehler: Schema-Datei '{csv_filepath}' nicht gefunden.") + ALLOWED_TARGET_BRANCHES = [] + except Exception as e: + debug_print(f"Fehler beim Laden des Ziel-Schemas aus '{csv_filepath}' (Zeile {line_count}): {e}") + ALLOWED_TARGET_BRANCHES = [] + ALLOWED_TARGET_BRANCHES = sorted(list(allowed_branches_set), key=str.lower) + debug_print(f"Ziel-Schema geladen. {len(ALLOWED_TARGET_BRANCHES)} eindeutige Zielbranchen gefunden.") # NEU: Zählung der Branches + + # Logge die ersten paar geladenen Branches zur Kontrolle if ALLOWED_TARGET_BRANCHES: - schema_lines = ["Ziel-Branchenschema: Folgende Branchenbereiche sind gültig:"] + debug_print(f"Erste 10 geladene Zielbranchen: {ALLOWED_TARGET_BRANCHES[:10]}") + schema_lines = ["Ziel-Branchenschema: Folgende Branchenbereiche sind gültig (Kurzformen):"] # Klarstellung schema_lines.extend(f"- {branch}" for branch in ALLOWED_TARGET_BRANCHES) - schema_lines.append("Bitte ordne das Unternehmen ausschließlich in einen dieser Bereiche ein.") + schema_lines.append("Bitte ordne das Unternehmen ausschließlich in einen dieser Bereiche ein. Gib NUR den Kurznamen der Branche zurück (keine Präfixe wie 'Hersteller / Produzenten >').") # Strengere Anweisung TARGET_SCHEMA_STRING = "\n".join(schema_lines) else: - TARGET_SCHEMA_STRING = "Ziel-Branchenschema nicht verfügbar (Mapping-Datei leer oder Fehler)." + TARGET_SCHEMA_STRING = "Ziel-Branchenschema nicht verfügbar (Datei leer oder Fehler)." ALLOWED_TARGET_BRANCHES = [] - debug_print(f"Branchen-Mapping und Schema geladen. {len(BRANCH_MAPPING)} Mappings, {len(ALLOWED_TARGET_BRANCHES)} Zielbranchen.") def map_external_branch(external_branch): @@ -1101,87 +1163,160 @@ def summarize_website_content(raw_text): def evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kategorien, website_summary): - """Ordnet Unternehmen einer Branche aus dem Ziel-Schema zu (via OpenAI).""" - # Stelle sicher, dass das Schema geladen ist - if not ALLOWED_TARGET_BRANCHES: - debug_print("Fehler: Ziel-Branchenschema nicht geladen. Branchenevaluierung übersprungen.") - return {"branch": crm_branche, "consistency": "X", "justification": "Fehler: Ziel-Schema nicht geladen"} + """ + Ordnet das Unternehmen basierend auf den angegebenen Informationen exakt einer Branche + aus dem Ziel-Branchenschema (nur Kurzformen) zu. Validiert den ChatGPT-Vorschlag + strikt gegen die erlaubten Kurzformen und führt einen Fallback auf die (extrahierte) + CRM-Kurzform durch, falls der Vorschlag ungültig ist. - # Baue den Prompt dynamisch auf, füge nur vorhandene Informationen hinzu - prompt_parts = [TARGET_SCHEMA_STRING] # Beginne mit den Regeln - prompt_parts.append("\nOrdne das Unternehmen anhand folgender Angaben exakt einer Branche des Ziel-Branchenschemas zu:") + Args: + crm_branche (str): Branche laut CRM (kann noch Präfix enthalten). + beschreibung (str): Unternehmensbeschreibung (CRM). + wiki_branche (str): Branche aus Wikipedia (falls vorhanden). + wiki_kategorien (str): Wikipedia-Kategorien. + website_summary (str): Zusammenfassung des Website-Inhalts. + + Returns: + dict: Enthält "branch" (die finale, gültige Kurzform oder Fehler), + "consistency" ('ok', 'X', 'fallback_crm_valid', 'fallback_invalid') und + "justification" (Begründung von ChatGPT oder Fallback-Info). + """ + # Globale Variablen für Schema und erlaubte Branches verwenden + global ALLOWED_TARGET_BRANCHES, TARGET_SCHEMA_STRING + + # Grundlegende Prüfung: Ist das Schema überhaupt geladen? + if not ALLOWED_TARGET_BRANCHES: + debug_print("FEHLER in evaluate_branche_chatgpt: Ziel-Branchenschema (ALLOWED_TARGET_BRANCHES) ist leer. Abbruch.") + # Gib den CRM-Wert zurück, aber markiere als Fehler + return {"branch": crm_branche, "consistency": "error_schema_missing", "justification": "Fehler: Ziel-Schema nicht geladen"} + + # Erstelle ein Set/Dict der erlaubten Branches in Kleinbuchstaben für effizientes Nachschlagen + # Speichert die Originalschreibweise als Wert. + allowed_branches_lookup = {b.lower(): b for b in ALLOWED_TARGET_BRANCHES} + + # --- Prompt für ChatGPT erstellen --- + # Beginne mit den Regeln und der Liste der gültigen Kurzformen + prompt_parts = [TARGET_SCHEMA_STRING] # TARGET_SCHEMA_STRING sollte bereits die klare Anweisung enthalten + prompt_parts.append("\nOrdne das Unternehmen anhand folgender Angaben exakt einer Branche des Ziel-Branchenschemas (Kurzformen) zu:") + + # Füge nur vorhandene Informationen hinzu und kürze sie ggf. if crm_branche and crm_branche != "k.A.": prompt_parts.append(f"- CRM-Branche (Referenz): {crm_branche}") if beschreibung and beschreibung != "k.A.": prompt_parts.append(f"- Beschreibung: {beschreibung[:500]}") # Kürzen if wiki_branche and wiki_branche != "k.A.": prompt_parts.append(f"- Wikipedia-Branche: {wiki_branche}") if wiki_kategorien and wiki_kategorien != "k.A.": prompt_parts.append(f"- Wikipedia-Kategorien: {wiki_kategorien[:500]}") # Kürzen if website_summary and website_summary != "k.A.": prompt_parts.append(f"- Website-Zusammenfassung: {website_summary[:500]}") # Kürzen - - # Fallback, wenn gar keine Infos da sind + + # Fallback, wenn gar keine spezifischen Infos da sind if len(prompt_parts) <= 2: - debug_print("Warnung: Zu wenige Informationen für Branchenevaluierung.") - # Optional: Prompt anpassen oder abbrechen - # prompt_parts.append("- KEINE SPEZIFISCHEN INFORMATIONEN VERFÜGBAR.") - return {"branch": crm_branche, "consistency": "X", "justification": "Zu wenige Informationen"} - + debug_print("Warnung in evaluate_branche_chatgpt: Zu wenige Informationen für Branchenevaluierung.") + return {"branch": crm_branche, "consistency": "error_no_info", "justification": "Fehler: Zu wenige Informationen für eine Einschätzung"} + # Füge die strengen Anweisungen für das Antwortformat hinzu + prompt_parts.append("\nWICHTIG: Antworte NUR mit dem exakten Kurznamen einer Branche aus der obigen Liste. Verwende KEINE Präfixe wie 'Hersteller / Produzenten >' oder 'Service provider (Dienstleister) >'.") prompt_parts.append("\nAntworte ausschließlich im folgenden Format (keine Einleitung, kein Schlusssatz):") - prompt_parts.append("Branche: ") - prompt_parts.append("Übereinstimmung: ") - prompt_parts.append("Begründung: ") + prompt_parts.append("Branche: ") + prompt_parts.append("Übereinstimmung: ") + prompt_parts.append("Begründung: ") prompt = "\n".join(prompt_parts) + # --- ChatGPT aufrufen --- chat_response = call_openai_chat(prompt, temperature=0.0) # Niedrige Temperatur für konsistente Zuordnung if not chat_response: - return {"branch": crm_branche, "consistency": "X", "justification": "API-Fehler oder leere Antwort"} + debug_print("Fehler in evaluate_branche_chatgpt: Keine Antwort von OpenAI erhalten.") + return {"branch": crm_branche, "consistency": "error_api_no_response", "justification": "Fehler: Keine Antwort von API"} - # Parse die Antwort + # --- Antwort parsen --- lines = chat_response.strip().split("\n") - result = {"branch": "", "consistency": "", "justification": ""} + result = {"branch": None, "consistency": None, "justification": ""} # Initialisiere mit None + suggested_branch = "" for line in lines: - if line.lower().startswith("branche:"): - result["branch"] = line.split(":", 1)[1].strip() - elif line.lower().startswith("übereinstimmung:"): - result["consistency"] = line.split(":", 1)[1].strip().lower() - elif line.lower().startswith("begründung:"): + line_lower = line.lower() + if line_lower.startswith("branche:"): + suggested_branch = line.split(":", 1)[1].strip() + # Entferne mögliche Anführungszeichen + suggested_branch = suggested_branch.strip('"\'') + elif line_lower.startswith("übereinstimmung:"): + # Wir überschreiben die Konsistenz später basierend auf unserer Logik + pass + elif line_lower.startswith("begründung:"): result["justification"] = line.split(":", 1)[1].strip() - suggested_branch = result["branch"] - - # --- Validierung und Mapping des Vorschlags --- - # 1. Prüfe, ob der Vorschlag *exakt* einer erlaubten Zielbranche entspricht - if suggested_branch in ALLOWED_TARGET_BRANCHES: - final_branch = suggested_branch - debug_print(f"ChatGPT-Branchenvorschlag '{suggested_branch}' ist gültig.") - else: - # 2. Wenn nicht exakt, versuche Mapping über map_external_branch (mit Normalisierung etc.) - mapped_branch = map_external_branch(suggested_branch) - if mapped_branch in ALLOWED_TARGET_BRANCHES: - final_branch = mapped_branch - debug_print(f"ChatGPT-Branchenvorschlag '{suggested_branch}' gemappt zu '{final_branch}'.") - result["justification"] += f" (Hinweis: Vorschlag '{suggested_branch}' wurde zu '{final_branch}' gemappt)" - else: - # 3. Wenn Mapping fehlschlägt, Fallback auf CRM-Branche (falls vorhanden und gültig) - # oder behalte den (ungültigen) Vorschlag und markiere als fehlerhaft - debug_print(f"ChatGPT-Branchenvorschlag '{suggested_branch}' ist ungültig und konnte nicht gemappt werden.") - if crm_branche and crm_branche != "k.A.": # and crm_branche in ALLOWED_TARGET_BRANCHES: # Optional: CRM auch prüfen - final_branch = crm_branche - result["consistency"] = "X" - result["justification"] = f"Fallback: Ungültiger ChatGPT-Vorschlag ('{suggested_branch}'). CRM-Branche '{crm_branche}' verwendet." - else: - final_branch = suggested_branch # Behalte ungültigen Vorschlag - result["consistency"] = "X" - result["justification"] = f"Fehler: Ungültiger ChatGPT-Vorschlag ('{suggested_branch}') und kein gültiger CRM-Fallback." + if not suggested_branch: + debug_print(f"Fehler in evaluate_branche_chatgpt: Konnte 'Branche:' nicht aus Antwort parsen: {chat_response}") + # Optional: Versuche Begründung als Branche zu nehmen? Eher nicht. + return {"branch": crm_branche, "consistency": "error_parsing", "justification": f"Fehler: Parsing der API Antwort fehlgeschlagen. Antwort: {chat_response}"} - result["branch"] = final_branch + # --- Validierung des ChatGPT-Vorschlags --- + final_branch = None + suggested_branch_lower = suggested_branch.lower() - # Konsistenzprüfung explizit neu bewerten basierend auf finalem Branch vs CRM Branch - if final_branch == crm_branche: - result["consistency"] = "ok" + if suggested_branch_lower in allowed_branches_lookup: + final_branch = allowed_branches_lookup[suggested_branch_lower] # Nimm korrekte Schreibweise + debug_print(f"ChatGPT-Branchenvorschlag '{suggested_branch}' ist gültig ('{final_branch}').") + # Konsistenz wird später gesetzt + result["consistency"] = "pending_comparison" # Temporärer Status else: - # Hier könnte man noch Fuzzy Similarity einbauen, falls gewünscht - result["consistency"] = "X" + # --- Fallback-Logik --- + debug_print(f"ChatGPT-Branchenvorschlag '{suggested_branch}' ist NICHT im Ziel-Schema ({len(ALLOWED_TARGET_BRANCHES)} Einträge) enthalten. Starte Fallback...") + + # Versuche Kurzform aus CRM-Branche zu extrahieren + crm_short_branch = "k.A." + if crm_branche and ">" in crm_branche: + crm_short_branch = crm_branche.split(">", 1)[1].strip() + elif crm_branche and crm_branche != "k.A.": # Wenn CRM schon Kurzform sein könnte + crm_short_branch = crm_branche.strip() + + # Prüfe, ob die extrahierte CRM-Kurzform gültig ist + if crm_short_branch != "k.A." and crm_short_branch.lower() in allowed_branches_lookup: + final_branch = allowed_branches_lookup[crm_short_branch.lower()] # Nimm korrekte Schreibweise + result["consistency"] = "fallback_crm_valid" # Setze Fallback-Status + # Kombiniere ChatGPT Begründung (falls vorhanden) mit Fallback-Info + fallback_reason = f"Fallback: Ungültiger ChatGPT-Vorschlag ('{suggested_branch}'). Gültige CRM-Kurzform '{final_branch}' verwendet." + result["justification"] = f"{fallback_reason} (ChatGPT Begründung war: {result.get('justification', 'Keine')})" + debug_print(f"Fallback auf gültige CRM-Kurzform erfolgreich: '{final_branch}'") + else: + # Wenn auch CRM-Kurzform ungültig oder nicht extrahierbar + final_branch = suggested_branch # Behalte ungültigen Vorschlag + result["consistency"] = "fallback_invalid" # Setze Fehler-Fallback-Status + error_reason = f"Fehler: Ungültiger ChatGPT-Vorschlag ('{suggested_branch}') und keine gültige CRM-Kurzform ('{crm_short_branch}') als Fallback verfügbar." + result["justification"] = f"{error_reason} (ChatGPT Begründung war: {result.get('justification', 'Keine')})" + debug_print(f"Fallback fehlgeschlagen. Ungültiger Vorschlag: '{final_branch}', Ungültige CRM-Kurzform: '{crm_short_branch}'") + # Alternativ: Gib einen speziellen Fehlerwert zurück + # final_branch = "FEHLER - UNGÜLTIGE ZUWEISUNG" + + # Setze den finalen Branch im Ergebnis-Dictionary + result["branch"] = final_branch if final_branch else "FEHLER" + + # --- Konsistenzprüfung (Finale Bewertung) --- + # Extrahiere CRM-Kurzform für den Vergleich (erneut oder Variable von oben) + crm_short_to_compare = "k.A." + if crm_branche and ">" in crm_branche: + crm_short_to_compare = crm_branche.split(">", 1)[1].strip() + elif crm_branche and crm_branche != "k.A.": + crm_short_to_compare = crm_branche.strip() + + # Vergleiche finalen Branch (falls nicht FEHLER) mit CRM-Kurzform (case-insensitive) + if result["branch"] != "FEHLER" and result["branch"].lower() == crm_short_to_compare.lower(): + # Wenn sie übereinstimmen UND *kein* Fallback stattgefunden hat, ist es 'ok'. + if result["consistency"] == "pending_comparison": + result["consistency"] = "ok" + # Wenn Fallback auf gültige CRM stattfand (Status 'fallback_crm_valid'), bleibt dieser Status. + elif result["consistency"] == "pending_comparison": + # Wenn sie nicht übereinstimmen und kein Fallback stattfand, ist es 'X'. + result["consistency"] = "X" + # Wenn der Status bereits 'fallback_crm_valid' oder 'fallback_invalid' ist, bleibt er unverändert. + elif result["consistency"] is None: # Sollte nicht passieren, aber zur Sicherheit + result["consistency"] = "error_unknown_state" + + + # Entferne den temporären Status, falls er noch da ist + if result["consistency"] == "pending_comparison": + result["consistency"] = "error_comparison_failed" + + # Debug-Ausgabe des finalen Ergebnisses vor Rückgabe + debug_print(f"Finale Branch-Evaluation: {result}") return result