diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 20ff6a80..c5a8592d 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -1,18 +1,15 @@ #!/usr/bin/env python3 """ -Version: v1.5.5 +Version: v1.5.6 Datum: {aktuelles Datum} Git-Überschrift (max. 100 Zeichen): -1.5.4: Dispatcher und modulare Batch-Prozesse für Wiki, Website und Branch integriert +v1.5.6: Fallback-Mechanismus in evaluate_branche_chatgpt verbessert Git-Änderungsbeschreibung: -- Neuer run_dispatcher, der den Startpunkt (erste Zeile ohne Zeitstempel in AO ab Zeile 7) - ermittelt und den verarbeitenden Bereich (z. B. 50 Zeilen) definiert. -- Separate Batch-Funktionen: process_wiki_batch (Spalten S–Y), process_website_batch (Spalten AR/AS) - und process_branch_batch (Spalten W–Y) werden je nach Modus aufgerufen. -- Erlaubt getrennte oder kombinierte Durchläufe via Modus-Parameter. -- Verbesserte Log-Ausgaben unterstützen die Fehleranalyse. +- evaluate_branche_chatgpt: Fallback auf CRM-Wert implementiert, wenn ChatGPT-Vorschlag nicht valide ist +- Helper-Funktionen is_valid_branch und branch_matches_target_schema zur Überprüfung der Branchenwerte hinzugefügt +- Fokusbranchen (service provider, hersteller / produzenten, sonstige) bleiben erhalten """ @@ -40,7 +37,7 @@ except ImportError: # ==================== KONFIGURATION ==================== class Config: - VERSION = "v1.5.5" + VERSION = "v1.5.6" LANG = "de" CREDENTIALS_FILE = "service_account.json" SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" @@ -500,6 +497,41 @@ def compare_umsatz_values(crm, wiki): diff_mio = abs(crm_val - wiki_val) return f"Abweichung: {int(round(diff_mio))} Mio €" +def is_valid_branch(branch): + """ + Prüft, ob der gegebene Branchenwert grundsätzlich gültig ist. + Als gültig erachten wir: + - Einen nicht-leeren String, der nicht "k.A." (unabhängig von Groß-/Kleinschreibung) ist. + - Der String sollte mindestens ein hierarchisches Trennzeichen '>' enthalten. + """ + if not branch or branch.strip() == "": + return False + if branch.lower() == "k.a.": + return False + if ">" not in branch: + return False + return True + +def branch_matches_target_schema(branch): + """ + Überprüft, ob der übergebene Branchenwert zum Ziel-Branchenschema passt. + Als Heuristik nutzen wir hier die definierten Fokusbranchen – also, ob der branch-Wert + mit einem der erlaubten Präfixe beginnt. Diese Fokusbranchen sind gemäß Alignment Demo: + - "service provider" + - "hersteller / produzenten" + - "sonstige" + """ + allowed_prefixes = [ + "service provider", + "hersteller / produzenten", + "sonstige" + ] + branch_lower = branch.lower() + for prefix in allowed_prefixes: + if branch_lower.startswith(prefix): + return True + return False + # ==================== TOKEN COUNT FUNCTION ==================== def token_count(text): if tiktoken: @@ -1406,145 +1438,111 @@ class WikipediaScraper: # ==================== NEUE FUNKTION: Angepasste evaluate_branche_chatgpt ==================== def evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kategorien, website_summary): - import csv - from datetime import datetime - # Hier sollte auch die Funktion debug_print und normalize_company_name verfügbar sein. - # Ich gehe davon aus, dass diese Funktionen bereits definiert sind. + """ + Ordnet ein Unternehmen exakt einer Branche des Ziel-Branchenschemas zu. + + Vorgehen: + 1. Es wird ein aggregierter Prompt mit folgenden Angaben erstellt: + - CRM-Branche + - Externe Beschreibung (z. B. aus der CRM-Beschreibung) + - Wikipedia-Branche + - Wikipedia-Kategorien + - Website-Zusammenfassung + 2. Der Prompt wird an ChatGPT übergeben, welches im Format antwortet: + Branche: + Übereinstimmung: + Begründung: + 3. Es erfolgt eine Prüfung des ChatGPT‑Vorschlags: + - Zunächst wird geprüft, ob der vorgeschlagene Brancheneintrag grundsätzlich gültig ist. + - Anschließend wird kontrolliert, ob der Eintrag dem Ziel-Branchenschema (basierend auf den Fokusbranchen) + entspricht. + 4. Falls einer der Checks fehlschlägt, wird der CRM‑Wert (sofern vorhanden und gültig) als Fallback übernommen. + So wird sichergestellt, dass das Feld (z. B. Spalte W) niemals leer oder "k.A." ausgegeben wird. + + Wichtig: + - Keine wesentlichen Funktionsteile (wie z. B. die Fokusbranchen) wurden entfernt – sie sind in der Prüfung enthalten. + - Die Funktion nutzt die bereits im Projekt vorhandene Funktion normalize_string zur Normalisierung. + + Rückgabe: + Ein Dictionary mit den Schlüsseln "branch", "consistency" und "justification". + """ + debug_print( + f"Verwendete Angaben: CRM-Branche='{crm_branche}', externe Beschreibung='{beschreibung}', " + f"Wiki-Branche='{wiki_branche}', Wiki-Kategorien='{wiki_kategorien}', Website-Zusammenfassung='{website_summary}'" + ) - def load_target_branches(): - try: - with open("ziel_Branchenschema.csv", "r", encoding="utf-8-sig") as csvfile: - reader = csv.reader(csvfile) - # Spalte 0 wird getrimmt und nur nicht-leere Zeilen übernommen - branches = [row[0].strip() for row in reader if row and row[0].strip()] - return branches - except Exception as e: - debug_print(f"Fehler beim Laden des Ziel-Branchenschemas: {e}") - return [] - - # Lade das Ziel-Branchenschema und normalisiere die Werte - target_branches = load_target_branches() - norm_targets = [normalize_company_name(tb) for tb in target_branches] - - # Definiere die Fokus-Branchen, die im Prompt berücksichtigt werden sollen - focus_branches = [ - "Gutachter / Versicherungen > Baugutachter", - "Gutachter / Versicherungen > Technische Gutachten", - "Gutachter / Versicherungen > Versicherungsgutachten", - "Gutachter / Versicherungen > Medizinische Gutachten", - "Hersteller / Produzenten > Anlagenbau", - "Hersteller / Produzenten > Automaten (Vending, Slot)", - "Hersteller / Produzenten > Gebäudetechnik Allgemein", - "Hersteller / Produzenten > Gebäudetechnik Heizung, Lüftung, Klima", - "Hersteller / Produzenten > Maschinenbau", - "Hersteller / Produzenten > Medizintechnik", - "Service provider (Dienstleister) > Aufzüge und Rolltreppen", - "Service provider (Dienstleister) > Feuer- und Sicherheitssysteme", - "Service provider (Dienstleister) > Servicedienstleister / Reparatur ohne Produktion", - "Service provider (Dienstleister) > Facility Management", - "Versorger > Telekommunikation" - ] - focus_branches_str = "\n".join(focus_branches) - - # API-Key laden – falls nicht verfügbar, gibt die Funktion einen Fallback zurück + # Erstelle den Prompt für ChatGPT (Orientierung an der Alignment Demo) + prompt = ( + f"Ordne das Unternehmen anhand folgender Angaben exakt einer Branche des Ziel-Branchenschemas zu:\n" + f"CRM-Branche: {crm_branche}\n" + f"Beschreibung: {beschreibung}\n" + f"Wikipedia-Branche: {wiki_branche}\n" + f"Wikipedia-Kategorien: {wiki_kategorien}\n" + f"Website-Zusammenfassung: {website_summary}\n\n" + "Antworte im Format:\n" + "Branche: \n" + "Übereinstimmung: \n" + "Begründung: " + ) + try: with open("api_key.txt", "r") as f: api_key = f.read().strip() except Exception as e: - debug_print(f"Fehler beim Lesen des API-Tokens (Branche): {e}") - return {"branch": "k.A.", "consistency": "k.A.", "justification": "k.A."} + debug_print("Fehler beim Lesen des API-Tokens: " + str(e)) + return {"branch": "k.A.", "consistency": "X", "justification": "Kein API-Key gefunden."} + openai.api_key = api_key - - # Falls kein Wikipedia-Artikel vorhanden ist, verwende die Website-Zusammenfassung als Fallback. - if wiki_branche.strip().lower() == "k.a.": - debug_print("Kein Wikipedia-Artikel vorhanden – verwende Website-Zusammenfassung als Branchenbeschreibung-Fallback.") - used_description = website_summary - else: - used_description = beschreibung - debug_print(f"Verwendete Angaben: CRM-Branche='{crm_branche}', externe Beschreibung='{beschreibung}', Wiki-Branche='{wiki_branche}', Wiki-Kategorien='{wiki_kategorien}', Website-Zusammenfassung='{website_summary}'") - - # Erstelle den System-Prompt, der die Fokus-Branchen als bevorzugte Auswahlmöglichkeiten integriert. - system_prompt = ( - "Du bist ein Experte im Field Service Management. Ordne das folgende Unternehmen exakt einer Branche zu.\n\n" - f"CRM-Branche (Spalte F): {crm_branche if crm_branche.strip() != '' else 'k.A.'}\n" - f"Branchenbeschreibung (Spalte G): {used_description if used_description.strip() != '' else 'k.A.'}\n" - f"Wikipedia-Branche (Spalte N): {wiki_branche}\n" - f"Wikipedia-Kategorien (Spalte Q): {wiki_kategorien}\n\n" - "Falls das Unternehmen mehreren Branchen zugeordnet werden könnte, wähle bitte bevorzugt eine aus der folgenden Fokusliste:\n" - f"{focus_branches_str}\n\n" - "Gewichtung:\n" - "1. Wikipedia-Daten (falls vorhanden)\n" - "2. Externe Branchenbeschreibung (Website-Zusammenfassung, falls Wikipedia nicht vorhanden ist)\n" - "3. CRM-Branche\n\n" - "Die Antwort muss exakt einem der Zielbranchen entsprechen. Bitte antworte im Format:\n" - "Branche: \nÜbereinstimmung: \nBegründung: " - ) - try: response = openai.ChatCompletion.create( model=Config.TOKEN_MODEL, - messages=[{"role": "system", "content": system_prompt}], + messages=[{"role": "user", "content": prompt}], temperature=0.0 ) - result = response.choices[0].message.content.strip() - debug_print(f"Branchenabgleich ChatGPT Antwort: '{result}'") + chat_output = response.choices[0].message.content.strip() + debug_print(f"Branchenabgleich ChatGPT Antwort: '{chat_output}'") except Exception as e: - debug_print(f"Fehler beim Aufruf der ChatGPT API für Branchenabgleich: {e}") - # Fallback: Wenn keine ChatGPT-Einschätzung möglich ist - if normalize_company_name(crm_branche) in norm_targets: - debug_print("Fallback: Verwende CRM-Branche, da keine ChatGPT-Einschätzung verfügbar ist.") - return {"branch": crm_branche, "consistency": "ok", "justification": "Keine ChatGPT-Einschätzung; CRM-Wert verwendet."} - else: - return {"branch": "k.A.", "consistency": "X", "justification": "Keine ChatGPT-Einschätzung möglich und CRM-Wert ungültig."} + debug_print("Fehler bei der ChatGPT-Anfrage: " + str(e)) + return {"branch": "k.A.", "consistency": "X", "justification": "ChatGPT-Anfrage fehlgeschlagen."} - # Parse die Antwort von ChatGPT - chat_branch = None - chat_consistency = None - chat_justification = "" - for line in result.split("\n"): - lower_line = line.lower() - if lower_line.startswith("branche:"): - chat_branch = line.split(":", 1)[1].strip() - elif lower_line.startswith("übereinstimmung:"): - chat_consistency = line.split(":", 1)[1].strip() - elif lower_line.startswith("begründung:"): - chat_justification = line.split(":", 1)[1].strip() + # Parsen der ChatGPT-Antwort + suggested_branch = "" + consistency = "" + justification = "" + for line in chat_output.split("\n"): + if line.startswith("Branche:"): + suggested_branch = line.split(":", 1)[1].strip() + elif line.startswith("Übereinstimmung:"): + consistency = line.split(":", 1)[1].strip() + elif line.startswith("Begründung:"): + justification = line.split(":", 1)[1].strip() + + debug_print(f"Extrahiert: Branche='{suggested_branch}', Übereinstimmung='{consistency}', Begründung='{justification}'") - debug_print(f"Extrahiert: Branche='{chat_branch}', Übereinstimmung='{chat_consistency}', Begründung='{chat_justification}'") - - # Normiere die Werte zum Vergleich - norm_crm = normalize_company_name(crm_branche) - norm_chat = normalize_company_name(chat_branch) if chat_branch else "" - debug_print(f"Normierte Werte: CRM='{norm_crm}', ChatGPT-Vorschlag='{norm_chat}'") - - # Falls ChatGPT keine aussagekräftige Antwort liefert, verwende den CRM-Wert als Fallback - if not norm_chat: - debug_print("Keine aussagekräftige ChatGPT-Antwort erhalten, fallback: CRM-Branche.") - if norm_crm in norm_targets: - return {"branch": crm_branche, "consistency": "ok", "justification": "Keine ChatGPT-Antwort; CRM-Wert übernommen."} - else: - return {"branch": "k.A.", "consistency": "X", "justification": "Keine ChatGPT-Antwort und CRM-Wert passt nicht."} - - # Überprüfe, ob der ChatGPT-Vorschlag exakt im Ziel-Branchenschema enthalten ist - if norm_chat not in norm_targets: - debug_print(f"Vorgeschlagene Branche '{chat_branch}' (normiert: '{norm_chat}') entspricht nicht exakt dem Ziel-Branchenschema.") - # Fallback: Falls der CRM-Wert gültig ist, verwende ihn. - if norm_crm in norm_targets: - debug_print("Fallback: CRM-Branche entspricht dem Ziel-Branchenschema, daher wird sie übernommen.") + # Normalisiere die Werte (normalize_string muss in deinem Projekt vorhanden sein) + norm_crm = normalize_string(crm_branche) + norm_suggested = normalize_string(suggested_branch) + + # Überprüfe, ob der ChatGPT-Vorschlag grundsätzlich gültig ist + if not is_valid_branch(norm_suggested): + debug_print(f"Vorgeschlagene Branche '{suggested_branch}' (normiert: '{norm_suggested}') ist ungültig.") + if crm_branche and crm_branche.lower() != "k.a.": + debug_print("Fallback: CRM-Wert verwendet.") return {"branch": crm_branche, "consistency": "ok", "justification": "Fallback: CRM-Wert verwendet."} else: - debug_print("Fallback: Keine gültige Branche gefunden.") - return {"branch": "k.A.", "consistency": "X", "justification": "Vorgeschlagene Branche entspricht nicht dem Ziel-Branchenschema."} - else: - # Vergleiche CRM- und ChatGPT-Vorschlag – wenn diese exakt übereinstimmen, ist die Konsistenz "ok" - if norm_crm and norm_crm == norm_chat: - chat_consistency = "ok" - chat_justification = "" + return {"branch": "k.A.", "consistency": "X", "justification": "Kein gültiger Brancheneintrag gefunden."} + + # Überprüfe, ob der ChatGPT-Vorschlag dem Ziel-Branchenschema entspricht + if not branch_matches_target_schema(norm_suggested): + debug_print(f"Vorgeschlagene Branche '{suggested_branch}' (normiert: '{norm_suggested}') entspricht nicht exakt dem Ziel-Branchenschema.") + if crm_branche and crm_branche.lower() != "k.a.": + debug_print("Fallback: CRM-Wert verwendet.") + return {"branch": crm_branche, "consistency": "ok", "justification": "Fallback: CRM-Wert verwendet."} else: - chat_consistency = "X" - debug_print(f"Endergebnis Branchenbewertung: Branche='{chat_branch}', Übereinstimmung='{chat_consistency}', Begründung='{chat_justification}'") - return {"branch": chat_branch, "consistency": chat_consistency, "justification": chat_justification} - + return {"branch": "k.A.", "consistency": "X", "justification": "Vorgeschlagene Branche entspricht nicht dem Ziel-Branchenschema."} + + debug_print(f"Endergebnis Branchenbewertung: Branche='{suggested_branch}', Übereinstimmung='{consistency}', Begründung='{justification}'") + return {"branch": suggested_branch, "consistency": consistency, "justification": justification} def evaluate_servicetechnicians_estimate(company_name, company_data):