diff --git a/brancheneinstufung.py b/brancheneinstufung.py index c0696513..01477359 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -14,7 +14,7 @@ import csv # ==================== KONFIGURATION ==================== class Config: - VERSION = "v1.3.4" # v1.3.4: FSM-Eignungsprüfung, Servicetechniker-Schätzung, Wiki-Vorschlag und 3s Pause integriert. + VERSION = "v1.3.5" # v1.3.5: FSM-Prüfung mit flexiblem Parser, Servicetechniker-Explanation, Log-Datei, Warten bis Sheet-Update. LANG = "de" CREDENTIALS_FILE = "service_account.json" SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" @@ -26,21 +26,19 @@ class Config: WIKIPEDIA_SEARCH_RESULTS = 5 HTML_PARSER = "html.parser" -# ==================== HELPER FUNCTIONS ==================== -def retry_on_failure(func): - def wrapper(*args, **kwargs): - for attempt in range(Config.MAX_RETRIES): - try: - return func(*args, **kwargs) - except Exception as e: - print(f"⚠️ Fehler bei {func.__name__} (Versuch {attempt+1}): {str(e)[:100]}") - time.sleep(Config.RETRY_DELAY) - return None - return wrapper +# Log-Datei vorbereiten +if not os.path.exists("Log"): + os.makedirs("Log") +LOG_FILE = os.path.join("Log", f"{datetime.now().strftime('%d-%m-%Y_%H-%M')}_{Config.VERSION.replace('.', '')}.txt") def debug_print(message): if Config.DEBUG: print(f"[DEBUG] {message}") + try: + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(f"[DEBUG] {message}\n") + except Exception as e: + print(f"[DEBUG] Log-Schreibfehler: {e}") def clean_text(text): if not text: @@ -144,23 +142,18 @@ def evaluate_umsatz_chatgpt(company_name, wiki_umsatz): temperature=0.0 ) result = response.choices[0].message.content.strip() - debug_print(f"ChatGPT Antwort: '{result}'") + debug_print(f"ChatGPT Umsatzschätzung: '{result}'") try: value = float(result.replace(',', '.')) return str(int(round(value))) except Exception as conv_e: - debug_print(f"Fehler bei der Verarbeitung der ChatGPT-Antwort '{result}': {conv_e}") + debug_print(f"Fehler bei der Verarbeitung der Umsatzschätzung '{result}': {conv_e}") return result except Exception as e: - debug_print(f"Fehler beim Aufruf der ChatGPT API: {e}") + debug_print(f"Fehler beim Aufruf der ChatGPT API für Umsatzschätzung: {e}") return "k.A." def validate_article_with_chatgpt(crm_data, wiki_data): - """ - Aggregiert die CRM-Daten (Spalten B-J) und Wikipedia-Daten (Spalten L-Q) - als CSV-Text und übermittelt diesen an die ChatGPT-API, um zu validieren, - ob beide Datensätze zum selben Unternehmen gehören. - """ crm_headers = "Firmenname;Website;Ort;Beschreibung;Aktuelle Branche;Beschreibung Branche extern;Anzahl Techniker;Umsatz (CRM);Anzahl Mitarbeiter (CRM)" wiki_headers = "Wikipedia URL;Wikipedia Absatz;Wikipedia Branche;Wikipedia Umsatz;Wikipedia Mitarbeiter;Wikipedia Kategorien" prompt_text = ( @@ -199,13 +192,13 @@ def evaluate_fsm_suitability(company_name, company_data): 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: {e}") + debug_print(f"Fehler beim Lesen des API-Tokens (FSM): {e}") return {"suitability": "k.A.", "justification": "k.A."} openai.api_key = api_key prompt = ( f"Bitte bewerte, ob das Unternehmen '{company_name}' für den Einsatz einer Field Service Management Lösung geeignet ist. " "Berücksichtige, dass ein Unternehmen mit einem technischen Außendienst, idealerweise mit über 50 Technikern und " - "Disponenten, die mit der Planung mobiler Ressourcen beschäftigt sind, als geeignet gilt. Nutze dabei vor allem verifizierte " + "Disponenten, die mit der Planung mobiler Ressourcen beschäftigt sind, als geeignet gilt. Nutze dabei verifizierte " "Wikipedia-Daten und deine eigene Einschätzung. Antworte ausschließlich mit 'Ja' oder 'Nein' und gib eine kurze Begründung." ) try: @@ -216,14 +209,24 @@ def evaluate_fsm_suitability(company_name, company_data): ) result = response.choices[0].message.content.strip() debug_print(f"FSM-Eignungsantwort ChatGPT: '{result}'") - # Erwartetes Format: "Eignung: \nBegründung: <...>" + # Flexibler Parser: Falls keine Zeilen mit ":" vorhanden sind, nimm den ersten Satz. suitability = "k.A." justification = "" - for line in result.split("\n"): - if line.lower().startswith("eignung:"): - suitability = line.split(":", 1)[1].strip() - elif line.lower().startswith("begründung:"): - justification = line.split(":", 1)[1].strip() + lines = result.split("\n") + if len(lines) == 1: + parts = result.split(" ", 1) + suitability = parts[0].strip() + justification = parts[1].strip() if len(parts) > 1 else "" + else: + for line in lines: + if line.lower().startswith("eignung:"): + suitability = line.split(":", 1)[1].strip() + elif line.lower().startswith("begründung:"): + justification = line.split(":", 1)[1].strip() + if suitability not in ["Ja", "Nein"]: + parts = result.split(" ", 1) + suitability = parts[0].strip() + justification = " ".join(result.split()[1:]).strip() return {"suitability": suitability, "justification": justification} except Exception as e: debug_print(f"Fehler beim Aufruf der ChatGPT API für FSM-Eignungsprüfung: {e}") @@ -235,13 +238,12 @@ def evaluate_servicetechnicians_estimate(company_name, company_data): 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: {e}") + debug_print(f"Fehler beim Lesen des API-Tokens (Servicetechniker): {e}") return "k.A." openai.api_key = api_key prompt = ( - f"Bitte schätze auf Basis öffentlich zugänglicher Informationen (insbesondere verifizierte Wikipedia-Daten) " + f"Bitte schätze auf Basis öffentlich zugänglicher Informationen (vor allem verifizierte Wikipedia-Daten) " f"die Anzahl der Servicetechniker des Unternehmens '{company_name}' ein. " - "Berücksichtige dabei Angaben zu Branche, Umsatz und Mitarbeiterzahl. " "Gib die Antwort ausschließlich in einer der folgenden Kategorien aus: " "'<50 Techniker', '>100 Techniker', '>200 Techniker', '>500 Techniker'." ) @@ -253,12 +255,36 @@ def evaluate_servicetechnicians_estimate(company_name, company_data): ) result = response.choices[0].message.content.strip() debug_print(f"Schätzung Servicetechniker ChatGPT: '{result}'") - # Wir erwarten eine Antwort, die exakt einer der vier Kategorien entspricht. return result except Exception as e: debug_print(f"Fehler beim Aufruf der ChatGPT API für Servicetechniker-Schätzung: {e}") return "k.A." +def evaluate_servicetechnicians_explanation(company_name, st_estimate, company_data): + 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 (ST-Erklärung): {e}") + return "k.A." + openai.api_key = api_key + prompt = ( + f"Bitte erkläre, warum du für das Unternehmen '{company_name}' die Anzahl der Servicetechniker als '{st_estimate}' geschätzt hast. " + "Berücksichtige dabei öffentlich zugängliche Informationen wie Branche, Umsatz, Mitarbeiterzahl und andere relevante Daten." + ) + try: + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[{"role": "system", "content": prompt}], + temperature=0.0 + ) + result = response.choices[0].message.content.strip() + debug_print(f"Servicetechniker-Erklärung ChatGPT: '{result}'") + return result + except Exception as e: + debug_print(f"Fehler beim Aufruf der ChatGPT API für Servicetechniker-Erklärung: {e}") + return "k.A." + def map_internal_technicians(value): try: num = int(value) @@ -273,19 +299,30 @@ def map_internal_technicians(value): else: return ">500 Techniker" +# ==================== WARTEN BIS ZELLE AKTUALISIERT IST ==================== +def wait_for_sheet_update(sheet, cell, expected_value, timeout=5): + start_time = time.time() + while time.time() - start_time < timeout: + try: + current_value = sheet.acell(cell).value + if current_value == expected_value: + return True + except Exception as e: + debug_print(f"Fehler beim Lesen von Zelle {cell}: {e}") + time.sleep(0.5) + return False + # ==================== BRANCHENABGLEICH PER CHATGPT ==================== def load_target_branches(): try: with open("ziel_Branchenschema.csv", "r", encoding="utf-8") as csvfile: reader = csv.reader(csvfile) - # Annahme: Jede Zeile enthält in der ersten Spalte einen gültigen Branchen-Namen branches = [row[0] for row in reader if row] return branches except Exception as e: debug_print(f"Fehler beim Laden des Ziel-Branchenschemas: {e}") return [] -# Fokusbranchen-Liste (wie definiert) focus_branches = [ "Gutachter / Versicherungen > Baugutachter", "Gutachter / Versicherungen > Technische Gutachten", @@ -312,17 +349,16 @@ def evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kateg 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: {e}") + debug_print(f"Fehler beim Lesen des API-Tokens (Branche): {e}") return {"branch": "k.A.", "consistency": "k.A.", "justification": "k.A."} openai.api_key = api_key - system_prompt = ( "Du bist ein Experte im Field Service Management. Deine Aufgabe ist es, ein Unternehmen basierend auf folgenden Angaben einer Branche zuzuordnen.\n\n" f"CRM-Branche (Spalte F): {crm_branche}\n" f"Branchenbeschreibung (Spalte G): {beschreibung}\n" f"Wikipedia-Branche (Spalte N): {wiki_branche}\n" f"Wikipedia-Kategorien (Spalte Q): {wiki_kategorien}\n\n" - "Das Ziel-Branchenschema umfasst ALLE gültigen Branchen, also sowohl Fokusbranchen als auch weitere, z. B. 'Housing > Sozialbau Unternehmen'.\n" + "Das Ziel-Branchenschema umfasst ALLE gültigen Branchen, also sowohl Fokusbranchen als auch weitere, z. B. 'Housing > Sozialbau Unternehmen'.\n" "Das vollständige Ziel-Branchenschema lautet:\n" f"{target_branches_str}\n\n" "Falls das Unternehmen mehreren Branchen zugeordnet werden könnte, wähle bitte bevorzugt eine Branche aus der folgenden Fokusliste, sofern zutreffend:\n" @@ -583,7 +619,6 @@ class WikipediaScraper: @retry_on_failure def search_company_article(self, company_name, website): # Zuerst prüfen: Gibt es in Spalte K bereits einen Wikipedia-Vorschlag? - # (Dies wird im _process_single_row gehandhabt) search_terms = self._generate_search_terms(company_name, website) for term in search_terms: try: @@ -625,19 +660,17 @@ class DataProcessor: if i >= self.sheet_handler.get_start_index(): self._process_single_row(i, row) def _process_single_row(self, row_num, row_data): - # CRM-Daten: Spalten B bis J (Indices 1 bis 9) - # Wikipedia-Daten: Spalten L bis Q (Indices 11 bis 16) company_name = row_data[1] if len(row_data) > 1 else "" website = row_data[2] if len(row_data) > 2 else "" wiki_update_range = f"K{row_num}:Q{row_num}" chatgpt_range = f"AF{row_num}" abgleich_range = f"AG{row_num}" - valid_range = f"R{row_num}" # Konsistenzprüfung + valid_range = f"R{row_num}" dt_range = f"AH{row_num}" ver_range = f"AI{row_num}" print(f"\n[{datetime.now().strftime('%H:%M:%S')}] Verarbeite Zeile {row_num}: {company_name}") - # Prüfe: Ist in Spalte K (Index 10) bereits ein Wikipedia-Vorschlag hinterlegt? + # Prüfen: Wikipedia-Vorschlag in Spalte K? if len(row_data) > 10 and row_data[10].strip() not in ["", "k.A."]: wiki_url = row_data[10].strip() try: @@ -646,28 +679,20 @@ class DataProcessor: debug_print(f"Fehler beim Laden des vorgeschlagenen Wikipedia-Artikels: {e}") article = self.wiki_scraper.search_company_article(company_name, website) company_data = self.wiki_scraper.extract_company_data(article.url) if article else { - 'url': 'k.A.', - 'first_paragraph': 'k.A.', - 'branche': 'k.A.', - 'umsatz': 'k.A.', - 'mitarbeiter': 'k.A.', - 'categories': 'k.A.', + 'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', + 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.', 'full_infobox': 'k.A.' } else: article = self.wiki_scraper.search_company_article(company_name, website) company_data = self.wiki_scraper.extract_company_data(article.url) if article else { - 'url': 'k.A.', - 'first_paragraph': 'k.A.', - 'branche': 'k.A.', - 'umsatz': 'k.A.', - 'mitarbeiter': 'k.A.', - 'categories': 'k.A.', + 'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', + 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.', 'full_infobox': 'k.A.' } wiki_values = [ - row_data[10] if len(row_data) > 10 and row_data[10].strip() not in ["", "k.A."] else "k.A.", # Vorschlag Wiki URL + row_data[10] if len(row_data) > 10 and row_data[10].strip() not in ["", "k.A."] else "k.A.", company_data.get('url', 'k.A.'), company_data.get('first_paragraph', 'k.A.'), company_data.get('branche', 'k.A.'), @@ -676,28 +701,25 @@ class DataProcessor: company_data.get('categories', 'k.A.') ] self.sheet_handler.sheet.update(values=[wiki_values], range_name=wiki_update_range) - time.sleep(3) # 3 Sekunden warten, bevor Validierung durchgeführt wird. + # Warten, bis das Update im Sheet übernommen wurde (prüfe Zelle K{row_num}) + wait_for_sheet_update(self.sheet_handler.sheet, f"K{row_num}", wiki_values[0]) + time.sleep(3) - # Umsatz-Schätzung via ChatGPT (wie bisher) - wiki_umsatz = company_data.get('umsatz', 'k.A.') - if wiki_umsatz != "k.A.": - chatgpt_umsatz = evaluate_umsatz_chatgpt(company_name, wiki_umsatz) - else: - chatgpt_umsatz = "k.A." - self.sheet_handler.sheet.update(values=[[chatgpt_umsatz]], range_name=chatgpt_range) + # Umsatz-Schätzung (Spalte AF soll "XX" erhalten) + self.sheet_handler.sheet.update(values=[["XX"]], range_name=chatgpt_range) - # Umsatz-Abgleich (wie bisher) + # Umsatz-Abgleich (Spalte AG) crm_umsatz = row_data[8] if len(row_data) > 8 else "k.A." abgleich_result = compare_umsatz_values(crm_umsatz, company_data.get('umsatz', 'k.A.')) self.sheet_handler.sheet.update(values=[[abgleich_result]], range_name=abgleich_range) - # Neuer Validierungsschritt (Firmenabgleich) – unverändert + # Validierung crm_data = ";".join(row_data[1:10]) wiki_data = ";".join(row_data[11:17]) valid_result = validate_article_with_chatgpt(crm_data, wiki_data) self.sheet_handler.sheet.update(values=[[valid_result]], range_name=valid_range) - # Neuer Branchenabgleich per ChatGPT: + # Branchenabgleich crm_branche = row_data[5] if len(row_data) > 5 else "k.A." beschreibung_branche = row_data[6] if len(row_data) > 6 else "k.A." wiki_branche = company_data.get('branche', 'k.A.') @@ -710,31 +732,35 @@ class DataProcessor: self.sheet_handler.sheet.update(values=[[branche_result["consistency"]]], range_name=branche_w_range) self.sheet_handler.sheet.update(values=[[branche_result["justification"]]], range_name=branche_x_range) - # Neue FSM-Eignungsprüfung: + # FSM-Eignungsprüfung (Spalte Y/Z) fsm_result = evaluate_fsm_suitability(company_name, company_data) self.sheet_handler.sheet.update(values=[[fsm_result["suitability"]]], range_name=f"Y{row_num}") self.sheet_handler.sheet.update(values=[[fsm_result["justification"]]], range_name=f"Z{row_num}") - # Neue Servicetechniker-Schätzung (ohne Berücksichtigung interner Angaben in Spalte H) + # Servicetechniker-Schätzung (Spalte AD) und Vergleich (Spalte AE) st_estimate = evaluate_servicetechnicians_estimate(company_name, company_data) self.sheet_handler.sheet.update(values=[[st_estimate]], range_name=f"AD{row_num}") - # Vergleich mit interner Angabe (Spalte H) internal_value = row_data[7] if len(row_data) > 7 else "k.A." internal_category = map_internal_technicians(internal_value) if internal_value != "k.A." else "k.A." if internal_category != "k.A." and st_estimate != internal_category: - discrepancy = f"Interne Angabe: {internal_category} vs. ChatGPT: {st_estimate}" + # Hole detaillierte Erklärung von ChatGPT, warum die Schätzung so ist. + explanation = evaluate_servicetechnicians_explanation(company_name, st_estimate, company_data) + discrepancy = explanation else: discrepancy = "ok" self.sheet_handler.sheet.update(values=[[discrepancy]], range_name=f"AE{row_num}") - # Timestamp und Version schreiben + # Spalten AF und AG sollen "XX" enthalten + self.sheet_handler.sheet.update(values=[["XX"]], range_name="AF" + str(row_num)) + self.sheet_handler.sheet.update(values=[["XX"]], range_name="AG" + str(row_num)) + + # Timestamp und Version current_dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.sheet_handler.sheet.update(values=[[current_dt]], range_name=dt_range) self.sheet_handler.sheet.update(values=[[Config.VERSION]], range_name=ver_range) - print(f"✅ Aktualisiert: URL: {company_data.get('url', 'k.A.')}, " - f"Branche: {company_data.get('branche', 'k.A.')}, " - f"ChatGPT Umsatz: {chatgpt_umsatz}, Umsatz-Abgleich: {abgleich_result}, " + debug_print(f"✅ Aktualisiert: URL: {company_data.get('url', 'k.A.')}, " + f"Branche: {company_data.get('branche', 'k.A.')}, Umsatz-Abgleich: {abgleich_result}, " f"Validierung: {valid_result}, Branchenvorschlag: {branche_result['branch']}, " f"FSM: {fsm_result['suitability']}, Servicetechniker-Schätzung: {st_estimate}") time.sleep(Config.RETRY_DELAY)