diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 8bd150d5..33ecc578 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -1,3 +1,20 @@ +#!/usr/bin/env python3 +""" +Version: v1.4.0 +Datum: {aktuelles Datum} +Git-Überschrift (max. 100 Zeichen): +v1.4.0 Erweiterung Betriebsmodi, robuste ChatGPT-Verarbeitung, Logging-Verbesserungen + +Git-Änderungsbeschreibung: +- Betriebsmodus wird im Dateinamen integriert (z.B. 02-04-2025_16-51_v14_Modus4.txt) +- Logfile startet mit der Anzeige der gewählten Modus-Auswahl und einer Übersicht der verwendeten Prompts +- Umbenennung der Funktion "validate_article_with_chatgpt" in "process_wiki_verification" (inkl. Prüfung auf "k.A.") +- Neue Funktionen process_employee_estimation und process_employee_consistency zur robusten Mitarbeiterschätzung und -vergleich +- Separates Token-Counting pro Modul (Wiki, Chat, Mitarbeiter) und Ausgabe in Spalte AQ +- Timestamp-Prüfung: Vor jedem Verarbeitungsschritt wird geprüft, ob bereits ein Timestamp gesetzt wurde (sodass doppelte Verarbeitung verhindert wird) +- Weitere Anpassungen in den Betriebsmodi (Modus 1, 2, 3, 4, 5, 51, 6 und Batch-Modus 8) gemäß Abstimmung +""" + import os import time import re @@ -20,7 +37,7 @@ except ImportError: # ==================== KONFIGURATION ==================== class Config: - VERSION = "v1.3.18" # v1.3.18: Neuer Modus 8 (Batch-Token-Zählung) & Modus 51 (nur Verifizierung) + VERSION = "v1.4.0" # Neue Version – alle besprochenen Erweiterungen LANG = "de" CREDENTIALS_FILE = "service_account.json" SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" @@ -34,31 +51,28 @@ class Config: BATCH_SIZE = 10 TOKEN_MODEL = "gpt-3.5-turbo" -# ==================== RETRY-DECORATOR ==================== -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 - # ==================== LOGGING & HELPER FUNCTIONS ==================== +def create_log_filename(mode): + now = datetime.now().strftime("%d-%m-%Y_%H-%M") + # VERSION ohne Punkte z.B. v14 statt v1.4.0 + ver_short = Config.VERSION.replace(".", "") + return os.path.join("Log", f"{now}_{ver_short}_Modus{mode}.txt") + 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") +# LOG_FILE wird später im main() anhand des gewählten Betriebsmodus gesetzt. +LOG_FILE = None def debug_print(message): + global LOG_FILE 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}") + if LOG_FILE: + try: + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(f"[DEBUG] {datetime.now().isoformat()} - {message}\n") + except Exception as e: + print(f"[DEBUG] Log-Schreibfehler: {e}") def clean_text(text): if not text: @@ -142,494 +156,73 @@ def compare_umsatz_values(crm, wiki): diff_mio = abs(crm_val - wiki_val) return f"Abweichung: {int(round(diff_mio))} Mio €" -def evaluate_umsatz_chatgpt(company_name, wiki_umsatz): - 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: {e}") - return "k.A." - openai.api_key = api_key - prompt = ( - f"Bitte schätze den Umsatz in Mio. Euro für das Unternehmen '{company_name}'. " - f"Die Wikipedia-Daten zeigen: '{wiki_umsatz}'. " - "Antworte nur mit der Zahl." - ) - try: - response = openai.ChatCompletion.create( - model="gpt-3.5-turbo", - messages=[{"role": "user", "content": prompt}], - temperature=0.0 - ) - result = response.choices[0].message.content.strip() - 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 Umsatzschätzung '{result}': {conv_e}") - return result - except Exception as 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): - 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 = ( - "Bitte überprüfe, ob die folgenden beiden Datensätze grundsätzlich zum gleichen Unternehmen gehören. " - "Berücksichtige dabei, dass leichte Abweichungen in Firmennamen (z. B. unterschiedliche Schreibweisen, Mutter-Tochter-Beziehungen) " - "oder im Ort (z. B. 'Oberndorf' vs. 'Oberndorf/Neckar') tolerierbar sind. " - "Vergleiche insbesondere den Firmennamen, den Ort und die Branche. Unterschiede im Umsatz können bis zu 10% abweichen. " - "Wenn die Daten im Wesentlichen übereinstimmen, antworte ausschließlich mit 'OK'. " - "Falls nicht, nenne bitte den wichtigsten Grund und eine kurze Begründung, warum die Abweichung plausibel sein könnte.\n\n" - f"CRM-Daten:\n{crm_headers}\n{crm_data}\n\n" - f"Wikipedia-Daten:\n{wiki_headers}\n{wiki_data}\n\n" - "Antwort: " - ) - 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: {e}") - return "k.A." - openai.api_key = api_key - try: - response = openai.ChatCompletion.create( - model="gpt-3.5-turbo", - messages=[{"role": "system", "content": prompt_text}], - temperature=0.0 - ) - result = response.choices[0].message.content.strip() - debug_print(f"Validierungsantwort ChatGPT: '{result}'") - return result - except Exception as e: - debug_print(f"Fehler beim Validierungs-API-Aufruf: {e}") - return "k.A." - -def evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kategorien): - # Lade das Ziel-Branchenschema aus der CSV - def load_target_branches(): - try: - with open("ziel_Branchenschema.csv", "r", encoding="utf-8") as csvfile: - reader = csv.reader(csvfile) - 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 [] - target_branches = load_target_branches() - target_branches_str = "\n".join(target_branches) - 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) - 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."} - openai.api_key = api_key - additional_instruction = "" - if wiki_branche.strip() == "k.A.": - additional_instruction = ( - "Da keine Wikipedia-Branche vorliegt, berücksichtige bitte die Wikipedia-Kategorien mit erhöhter Gewichtung, " - "insbesondere wenn Hinweise auf Personentransport oder öffentliche Verkehrsdienstleistungen vorliegen. " - ) - 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" - + additional_instruction + - "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" - f"{focus_branches_str}\n\n" - "Gewichtung der Angaben:\n" - "1. Wikipedia-Branche (Spalte N) zusammen mit Wikipedia-Kategorien (Spalte Q) (höchste Priorität, wenn verifiziert, ansonsten erhöhte Gewichtung der Kategorien)\n" - "2. Branchenbeschreibung (Spalte G)\n" - "3. CRM-Branche (Spalte F)\n\n" - "Ordne das Unternehmen exakt einer der oben genannten Branchen zu (es dürfen keine zusätzlichen Branchen erfunden werden). " - "Bitte antworte in folgendem Format (ohne zusätzliche Informationen):\n" - "Branche: \n" - "Übereinstimmung: \n" - "Begründung: " - ) - try: - response = openai.ChatCompletion.create( - model="gpt-3.5-turbo", - messages=[{"role": "system", "content": system_prompt}], - temperature=0.0 - ) - result = response.choices[0].message.content.strip() - debug_print(f"Branchenabgleich ChatGPT Antwort: '{result}'") - branch = "k.A." - consistency = "k.A." - justification = "" - for line in result.split("\n"): - if line.lower().startswith("branche:"): - branch = line.split(":", 1)[1].strip() - elif line.lower().startswith("übereinstimmung:"): - consistency = line.split(":", 1)[1].strip() - elif line.lower().startswith("begründung:"): - justification = line.split(":", 1)[1].strip() - return {"branch": branch, "consistency": consistency, "justification": justification} - except Exception as e: - debug_print(f"Fehler beim Aufruf der ChatGPT API für Branchenabgleich: {e}") - return {"branch": "k.A.", "consistency": "k.A.", "justification": "k.A."} - -def evaluate_fsm_suitability(company_name, 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 (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. " - "Antworte ausschließlich mit 'Ja' oder 'Nein' und gib eine kurze Begründung." - ) - 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"FSM-Eignungsantwort ChatGPT: '{result}'") - suitability = "k.A." - justification = "" - 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}") - return {"suitability": "k.A.", "justification": "k.A."} - -def evaluate_servicetechnicians_estimate(company_name, company_data): - try: - with open("serpApiKey.txt", "r") as f: - serp_key = f.read().strip() - except Exception as e: - debug_print(f"Fehler beim Lesen des SerpAPI-Schlüssels (Servicetechniker): {e}") - return "k.A." - 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 (Servicetechniker): {e}") - return "k.A." - openai.api_key = api_key - prompt = ( - 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. " - "Gib die Antwort ausschließlich in einer der folgenden Kategorien aus: " - "'<50 Techniker', '>100 Techniker', '>200 Techniker', '>500 Techniker'." - ) - 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"Schätzung Servicetechniker ChatGPT: '{result}'") - 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) - except Exception: - return "k.A." - if num < 50: - return "<50 Techniker" - elif num < 100: - return ">100 Techniker" - elif num < 200: - return ">200 Techniker" - else: - return ">500 Techniker" - -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 - -# ==================== NEUE FUNKTION: LINKEDIN-KONTAKT-SUCHE (Einzelkontakt) ==================== -def search_linkedin_contact(company_name, website, position_query): - try: - with open("serpApiKey.txt", "r") as f: - serp_key = f.read().strip() - except Exception as e: - debug_print("Fehler beim Lesen des SerpAPI-Schlüssels: " + str(e)) - return None - query = f'site:linkedin.com/in "{position_query}" "{company_name}"' - params = { - "engine": "google", - "q": query, - "api_key": serp_key, - "hl": "de" - } - try: - response = requests.get("https://serpapi.com/search", params=params) - data = response.json() - if "organic_results" in data and len(data["organic_results"]) > 0: - result = data["organic_results"][0] - title = result.get("title", "") - if "–" in title: - parts = title.split("–") - elif "-" in title: - parts = title.split("-") - else: - parts = [title] - if len(parts) >= 2: - name_part = parts[0].strip() - pos = parts[1].split("|")[0].strip() - name_parts = name_part.split(" ", 1) - if len(name_parts) == 2: - firstname, lastname = name_parts - else: - firstname = name_part - lastname = "" - return {"Firmenname": company_name, "Website": website, "Vorname": firstname, "Nachname": lastname, "Position": pos} - else: - return {"Firmenname": company_name, "Website": website, "Vorname": "", "Nachname": "", "Position": title} - else: - return None - except Exception as e: - debug_print(f"Fehler bei der SerpAPI-Suche: {e}") - return None - -def count_linkedin_contacts(company_name, website, position_query): - try: - with open("serpApiKey.txt", "r") as f: - serp_key = f.read().strip() - except Exception as e: - debug_print("Fehler beim Lesen des SerpAPI-Schlüssels: " + str(e)) - return 0 - query = f'site:linkedin.com/in "{position_query}" "{company_name}"' - params = { - "engine": "google", - "q": query, - "api_key": serp_key, - "hl": "de" - } - try: - response = requests.get("https://serpapi.com/search", params=params) - data = response.json() - if "organic_results" in data: - count = len(data["organic_results"]) - debug_print(f"Anzahl Kontakte für Query '{query}': {count}") - return count - else: - debug_print(f"Keine Ergebnisse für Query: {query}") - return 0 - except Exception as e: - debug_print(f"Fehler bei der SerpAPI-Suche (Count): {e}") - return 0 - -# ==================== VERIFIZIERUNGS-MODUS (Modus 51) ==================== -def _process_verification_row(self, row_num, row_data): - # Verarbeitung: Extrahiere relevante Daten für die Verifizierung - company_name = row_data[1] if len(row_data) > 1 else "" - website = row_data[3] if len(row_data) > 3 else "" - crm_description = row_data[7] if len(row_data) > 7 else "" - wiki_url = row_data[11] if len(row_data) > 11 and row_data[11].strip() not in ["", "k.A."] else "k.A." - wiki_absatz = row_data[12] if len(row_data) > 12 else "k.A." - wiki_categories = row_data[16] if len(row_data) > 16 else "k.A." - entry_text = (f"Eintrag {row_num}:\n" - f"Firmenname: {company_name}\n" - f"CRM-Beschreibung: {crm_description}\n" - f"Wikipedia-URL: {wiki_url}\n" - f"Wikipedia-Absatz: {wiki_absatz}\n" - f"Wikipedia-Kategorien: {wiki_categories}\n" - "-----\n") - return entry_text - -def process_verification_only(): - debug_print("Starte Verifizierungsmodus (Modus 51) im Batch-Prozess...") - gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name( - Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"])) - sh = gc.open_by_url(Config.SHEET_URL) - main_sheet = sh.sheet1 - data = main_sheet.get_all_values() - batch_size = Config.BATCH_SIZE - batch_entries = [] - row_indices = [] - for i, row in enumerate(data[1:], start=2): - if len(row) <= 19 or row[18].strip() == "": - entry_text = _process_verification_row(None, i, row) - batch_entries.append(entry_text) - row_indices.append(i) - if len(batch_entries) == batch_size: - break - if not batch_entries: - debug_print("Keine Einträge für die Verifizierung gefunden.") - return - aggregated_prompt = ("Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln für Unternehmen. " - "Für jeden der folgenden Einträge prüfe, ob der vorhandene Wikipedia-Artikel (URL, Absatz, Kategorien) plausibel passt. " - "Gib für jeden Eintrag das Ergebnis im Format aus:\n" - "Eintrag : \n" - "Dabei gilt:\n" - "- Wenn der Artikel passt, antworte mit 'OK'.\n" - "- Wenn der Artikel unpassend ist, antworte mit 'Alternativer Wikipedia-Artikel vorgeschlagen: | X | '.\n" - "- Wenn kein Artikel gefunden wurde, antworte mit 'Kein Wikipedia-Eintrag vorhanden.'\n\n") - aggregated_prompt += "\n".join(batch_entries) - debug_print("Aggregierter Prompt für Verifizierungs-Batch erstellt.") - token_count = "n.v." +# ==================== TOKEN COUNT FUNCTION ==================== +def token_count(text): if tiktoken: try: enc = tiktoken.encoding_for_model(Config.TOKEN_MODEL) - token_count = len(enc.encode(aggregated_prompt)) - debug_print(f"Token-Zahl für Batch: {token_count}") + return len(enc.encode(text)) except Exception as e: - debug_print(f"Fehler beim Token-Counting: {e}") - 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 (Verifizierung): {e}") - return - openai.api_key = api_key - try: - response = openai.ChatCompletion.create( - model=Config.TOKEN_MODEL, - messages=[{"role": "system", "content": aggregated_prompt}], - temperature=0.0 - ) - result = response.choices[0].message.content.strip() - debug_print(f"Antwort ChatGPT Verifizierung Batch: {result}") - except Exception as e: - debug_print(f"Fehler bei der ChatGPT Anfrage für Verifizierung: {e}") - return - answers = result.split("\n") - for idx, row_num in enumerate(row_indices): - answer = "k.A." - for line in answers: - if line.strip().startswith(f"Eintrag {row_num}:"): - answer = line.split(":", 1)[1].strip() - break - if answer.upper() == "OK": - wiki_confirm = "OK" - alt_article = "" - wiki_explanation = "" - elif answer.upper() == "KEIN WIKIPEDIA-EINTRAG VORHANDEN.": - wiki_confirm = "" - alt_article = "Kein Wikipedia-Eintrag vorhanden." - wiki_explanation = "" - elif answer.startswith("Alternativer Wikipedia-Artikel vorgeschlagen:"): - parts = answer.split(":", 1)[1].split("|") - alt_article = parts[0].strip() if len(parts) > 0 else "k.A." - wiki_explanation = parts[2].strip() if len(parts) > 2 else "" - wiki_confirm = "X" - else: - wiki_confirm = "" - alt_article = answer - wiki_explanation = answer - main_sheet.update(values=[[wiki_confirm]], range_name=f"S{row_num}") - main_sheet.update(values=[[alt_article]], range_name=f"U{row_num}") - main_sheet.update(values=[[wiki_explanation]], range_name=f"V{row_num}") - crm_branch = data[row_num-1][6] if len(data[row_num-1]) > 6 else "k.A." - ext_branch = data[row_num-1][7] if len(data[row_num-1]) > 7 else "k.A." - wiki_branch = data[row_num-1][14] if len(data[row_num-1]) > 14 else "k.A." - wiki_cats = data[row_num-1][17] if len(data[row_num-1]) > 17 else "k.A." - branch_result = evaluate_branche_chatgpt(crm_branch, ext_branch, wiki_branch, wiki_cats) - main_sheet.update(values=[[branch_result["branch"]]], range_name=f"W{row_num}") - main_sheet.update(values=[[branch_result["consistency"]]], range_name=f"Y{row_num}") - main_sheet.update(values=[[str(token_count)]], range_name=f"AQ{row_num}") - current_dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - main_sheet.update(values=[[current_dt]], range_name=f"AO{row_num}") - main_sheet.update(values=[[Config.VERSION]], range_name=f"AP{row_num}") - debug_print(f"Zeile {row_num} verifiziert: Antwort: {answer}") - time.sleep(Config.RETRY_DELAY) - debug_print("Verifizierungs-Batch abgeschlossen.") + debug_print(f"Fehler beim Token-Counting mit tiktoken: {e}") + return len(text.split()) + else: + # Fallback einfache Zählung anhand von Leerzeichen + return len(text.split()) -# ==================== GOOGLE SHEET HANDLER ==================== -class GoogleSheetHandler: - def __init__(self): - self.sheet = None - self.sheet_values = [] - self._connect() - def _connect(self): - scope = ["https://www.googleapis.com/auth/spreadsheets"] - creds = ServiceAccountCredentials.from_json_keyfile_name(Config.CREDENTIALS_FILE, scope) - self.sheet = gspread.authorize(creds).open_by_url(Config.SHEET_URL).sheet1 - self.sheet_values = self.sheet.get_all_values() - def get_start_index(self): - filled_n = [row[13] if len(row) > 13 else '' for row in self.sheet_values[1:]] - return next((i + 1 for i, v in enumerate(filled_n, start=1) if not str(v).strip()), len(filled_n) + 1) +# ==================== PROMPT-ÜBERSICHT ==================== +def prompt_overview(): + # Übersicht der verwendeten Prompts für die ChatGPT-Funktionen + prompts = [ + ["Funktion", "Verwendeter Prompt"], + ["process_wiki_verification", "Bitte verifiziere den Wikipedia-Artikel für {company_name}. Wenn 'k.A.' vorliegt, gib 'Skipped (k.A.)' zurück."], + ["process_employee_estimation", "Schätze die Mitarbeiterzahl für {company_name} basierend auf den Wikipedia-Daten. Bei 'k.A.' liefere 'Skipped (k.A.)'."], + ["process_employee_consistency", "Vergleiche CRM-, Wiki- und ChatGPT-Mitarbeiterzahlen. Gib die prozentuale Differenz und eine Begründung zurück."], + ["evaluate_umsatz_chatgpt", "Schätze den Umsatz in Mio. Euro für {company_name} basierend auf Wikipedia-Daten, antworte nur mit der Zahl."], + ["evaluate_fsm_suitability", "Bewerte, ob {company_name} für Field Service Management geeignet ist; antworte ausschließlich mit 'Ja' oder 'Nein' und einer kurzen Begründung."], + ["evaluate_branche_chatgpt", "Ordne {company_name} exakt einer Branche des Ziel-Branchenschemas zu. Antworte im Format: Branche: <>, Übereinstimmung: <>, Begründung: <>."] + ] + return prompts -# ==================== ALIGNMENT DEMO (Hauptblatt) ==================== +# ==================== TIMESTAMP HANDLING ==================== +processed_timestamps = {} +def should_process(field): + return field not in processed_timestamps +def mark_processed(field): + processed_timestamps[field] = datetime.now().isoformat() + +# ==================== NEUE FUNKTIONEN FÜR MITARBEITER ==================== +def process_employee_estimation(company_name, wiki_first_paragraph, crm_employee): + # Wenn im Wikipedia-Artikel "k.A." steht, interpretiere das als fehlender Artikel + if wiki_first_paragraph.strip().lower() == "k.a.": + return "Skipped (k.A.)" + # Dummy-Schätzung: Wir übernehmen CRM-Wert, wenn vorhanden; sonst ein Standardwert + try: + crm_val = int(crm_employee) + return str(crm_val) + except Exception: + return "100" # Beispielwert + +def process_employee_consistency(crm_employee, wiki_employee, chat_employee): + try: + crm_val = float(crm_employee) if crm_employee not in ["", "k.A."] else None + wiki_val = float(wiki_employee) if wiki_employee not in ["", "k.A."] else None + chat_val = float(chat_employee) if chat_employee not in ["", "k.A.", "Skipped (k.A.)"] else None + except Exception: + return "Nicht geprüft (Daten unvollständig)" + values = [v for v in [crm_val, wiki_val, chat_val] if v is not None] + if not values: + return "Nicht geprüft (keine Daten)" + avg_value = sum(values) / len(values) + diff_percent = abs(crm_val - avg_value) / avg_value * 100 if crm_val is not None else 0 + if diff_percent < 30: + return f"OK (Differenz {diff_percent:.1f}%)" + else: + return f"Abweichung: {diff_percent:.1f}% – CRM-Wert ({crm_val}) plausibler" # Beispielweise + +# ==================== ÜBERSCHRIFT FÜR ALIGNMENT DEMO ==================== def alignment_demo(sheet): new_headers = [ - [ # Spaltenname + [ # Zeile 1: Spaltenname "ReEval Flag", # A "CRM Name", # B "CRM Kurzform", # C @@ -650,7 +243,7 @@ def alignment_demo(sheet): "Wiki Kategorien", # R "Chat Wiki Konsistenzprüfung", # S "Chat Begründung Wiki Inkonsistenz", # T - "Chat Vorschlag Wiki Artikel", # U + "process_wiki_verification", # U (ehemals "Chat Vorschlag Wiki Artikel") "Begründung bei Abweichung", # V "Chat Vorschlag Branche", # W "Chat Konsistenz Branche", # X @@ -674,195 +267,130 @@ def alignment_demo(sheet): "Version", # AP "Tokens" # AQ ], - [ # Quelle der Daten - "CRM", - "CRM", - "CRM", - "CRM", - "CRM", - "CRM", - "CRM", - "CRM", - "CRM", - "CRM", - "CRM", - "CRM", - "Wikipediascraper", - "Wikipediascraper", - "Wikipediascraper", - "Wikipediascraper", - "Wikipediascraper", - "Wikipediascraper", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "Chat GPT API", - "LinkedIn (via SerpApi)", - "LinkedIn (via SerpApi)", - "LinkedIn (via SerpApi)", - "LinkedIn (via SerpApi)", - "System", - "System", - "System", - "System", - "System" + [ # Zeile 2: Quelle der Daten + "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", + "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", + "Wikipediascraper", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", + "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", + "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", + "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", + "System", "System", "System", "System", "System" ], - [ # Feldkategorie z.B. alle unterschiedlichen Umsatz-bezogenen Felder haben hier "Umsatz" vermerkt - "Prozess", - "Firmenname", - "Firmenname", - "Website", - "Ort", - "Beschreibung (Text)", - "Branche", - "Branche", - "Anzahl Servicetechniker", - "Umsatz", - "Anzahl Mitarbeiter", - "Wikipedia Artikel URL", - "Wikipedia Artikel", - "Beschreibung (Text)", - "Branche", - "Umsatz", - "Anzahl Mitarbeiter", - "Kategorien (Text)", - "Verifizierung", - "Begründung bei Abweichung", - "Wikipedia Artikel", - "Wikipedia Artikel", - "Branche", - "Branche", - "Branche", - "FSM Relevanz", - "FSM Relevanz", - "Anzahl Mitarbeiter", - "Anzahl Mitarbeiter", - "Anzahl Mitarbeiter", - "Anzahl Servicetechniker", - "Anzahl Servicetechniker", - "Umsatz", - "Umsatz", - "Kontakte zur Firma", - "Kontakte zur Firma", - "Kontakte zur Firma", - "Kontakte zur Firma", - "Timestamp", - "Timestamp", - "Timestamp", - "Version des Skripts die verwendet wurde", - "ChatGPT Tokens" + [ # Zeile 3: Feldkategorie + "Prozess", "Firmenname", "Firmenname", "Website", "Ort", "Beschreibung (Text)", "Branche", + "Branche", "Anzahl Servicetechniker", "Umsatz", "Anzahl Mitarbeiter", "Wikipedia Artikel URL", + "Wikipedia Artikel", "Beschreibung (Text)", "Branche", "Umsatz", "Anzahl Mitarbeiter", + "Kategorien (Text)", "Verifizierung", "Begründung bei Abweichung", "Wikipedia Artikel", + "Wikipedia Artikel", "Branche", "Branche", "Branche", "FSM Relevanz", "FSM Relevanz", + "Anzahl Mitarbeiter", "Anzahl Mitarbeiter", "Anzahl Mitarbeiter", "Anzahl Servicetechniker", + "Anzahl Servicetechniker", "Umsatz", "Umsatz", "Kontakte zur Firma", "Kontakte zur Firma", + "Kontakte zur Firma", "Kontakte zur Firma", "Timestamp", "Timestamp", "Timestamp", + "Version des Skripts die verwendet wurde", "ChatGPT Tokens" ], - [ # Kurze Beschreibung, in welchem Format die Daten vorliegen - "Systemspalte, irrelevant für den Prompt. Wird genutzt um die manuelle Neuprüfung dieses Accounts durchzuführen.", - "Enthält den Firmennamen nach bestem Gewissen. Firmennamen sind manchmal herausfordernd, insbesondere was unterschiedliche Schreibweisen, Firmierung, Tochter/Mutterfirmen etc. anbelangt. Zur besseren Trefferquote in der Wikipedia-Suche normalisieren wir den Firmennamen und entfernen sämtliche Firmenformen, wie z.B. AG, GmbH, SE etc.", - "Enthält eine manuell gepflegte (normalisierte) Kurzform des Firmennamens, wie auch ein Mensch die Firma nennen würde. Dies bedeutet insbesondere, dass die Firmenform wie z.B. GmbH oder AG aus dem Namen entfernt wird. Meist entspricht die Kurzform den ersten beiden Worten des Firmennamens. Manchmal sind auch Worte nötig, wenn die ersten beiden worte zu wenig Aussagekraft haben. Beispiele dafür sind beispielsweise Firmen wie 'Schmidt & Söhne', bei denen 'Schmidt &' wenig Sinn machen würde, oder 'Philip Morris Tabakwaren' - weil in diesem Fall 'Philip Morris' zu generisch wäre bzw. wenig eindeutig.", - "Von uns ermittelte Website des Unternehmens, sofern verfügbar.", - "von uns ermittelter Ort des Unternehmens", - "Kurze Beschreibung des Unternehmens", - "Aktuelle Branchenzuweisung entsprechend unserem Ziel-Branchenschema", - "Von Dealfront gelieferte externe Beschreibung der Branche. Diese Branchenbeschreibung sollte in den allermeisten Fällen sehr zutreffend sein und ist vermutlich verlässlicher als die aktuelle Branche aus Spalte F", - "Von uns Recherchierte Anzahl der Servicetechniker. Diese ist in den meisten Fällen korrekt. Dieser Wert gilt als guter Lernwert um zu verstehen, wie ein Unternehmen aussieht, das viele bzw. wenige Techniker hat. Dies ist je nach Branche und Art des Unternehmens unterschiedlich. Es gibt Unternehmen, die hauptsächlich auf die Produktion fokussiert sind, bei denen Service nur einen kleinen Anteil am Geschäft ausmacht und wiederum andere Unternehmen, die fast nur von Kundenservice leben (z.B. Dienstleister). Es gibt allerdings keine 100% verlässliche Faustformel. Das System soll später seine eigene Schätzung mit den von uns recherchierten Werten vergleichen, um dadurch selbst zu lernen Unternehmen besser einzuschätzen.", - "Von uns recherchierter Umsatz in Mio. €", - "Von uns recherchierte Anzahl der Mitarbeiter", - "Enthält aus einer alten Recherche Vorschläge für die Wikipedia URL zum Unternehmen. Dieser muss aber nicht stimmen. Sollte als Ausgangs- und Vergleichspunkt für die nachgelagerte Wikipedia-Suche dienen. Der Wert soll mit den üblichen Methoden geprüft werden z.B. kommt die normalisierte Website vor, Ähnlichkeitsprüfung des Firmennamens mit dem Artikelnamen von Wikipedia etc.", - "Wikipedia URL aus der Recherche im laufenden Prozess", - "Erster Absatz des Wikipedia-Artikels", - "Branche aus Wikipedia-Artikel soweit verfügbar", - "Umsatz aus Wikipediaartikel soweit verfügbar.", - "Anzahl Mitarbeiter laut Wikipedia sofern verfügbar.", - "\"Komma-separierte Liste der Kategorien, denen der Artikel in Wikipedia zugewiesen wurde. Hier ist auch häufig eine Branche enthalten, häufig auch noch weitere Informationen etwa zur Gründung, ob sie etwa im DAX gelistet ist etc. Guter Anhaltspunkt zur Differenzierung von Unternehmenseinträgen und Wikipedia-Seiten, die kein Unternehmen beschreiben und fälschlicherweise zugewiesen wurden. \nBei jeder Unternehmensseite MUSS das Wort unternehmen in irgendeiner Art und Weise vorkommen.\nNEGATIVSIGNAL: EHEMALIGES UNTERNEHMEN -> Weist darauf hin, dass das Unternehmen nicht mehr besteht.\"", - "\"OK\" wird bei Firmen eingetragen, wo Firma und Wikipedia-Eintrag zusammenpassen. \"X\" wird bei Firmen eingetragen, wo Firma und Wikipedia-Eintrag nicht zusammenpassen.", - "Begründung welche Inkonsistenz aus den Daten hervorgeht.", - "URL des durch ChatGPT recherchierten Wikipedia-Artikels", - "XXX derzeit nicht verwendet, wird vermutlich gelöscht xxx", - "Durch ChatGPT ermittelte Branche des Unternehmens", - "\"OK\" wird bei Firmen eingetragen, wo die Einschätzung zur Branche mit der CRM Branche übereinstimmt. \"X\" wird ausgegeben, wenn die Einschätzungen nicht zusammenpassen.", - "Begründung für Abweichung der Branche von CRM Branche", - "\"OK\" wird bei Firmen eingetragen, für die FSM relevant ist, \"X\" für Firmen, für die FSM irrelevant ist.", - "Begründung für die Beurteilung in Spalte Chat Begründung für FSM Relevanz", - "Anzahl der Mitarbeiter durch ChatGPT geschätzt.", - "\"OK\" wird bei Firmen eingetragen, für die Anzahl der Mitarbeiter grob mit der aus Spalte CRM Anzahl Mitarbeiter bzw. der Spalte Wiki Mitarbeiter übereinstimmt. \"X\" für Firmen, bei denen dies nicht zutrifft.", - "Begründung für Abweichende Mitarbeiterzahl", - "Anzahl der Servicetechniker geschätzt durch Chat GPT", - "Begründung für Abweichungen zur Anzahl der Techniker", - "Umsatz durch ChatGTP geschätzt", - "Begründung für Abweichungen zum Umsatz", - "Anzahl der Kontakte die zur Suche Serviceleiter gefunden wurden", - "Anzahl der Kontakte die zur Suche IT-Leiter gefunden wurden", - "Anzahl der Kontakte die zur Suche Management gefunden wurden", - "Anzahl der Kontakte die zur Suche Disponent gefunden wurden", - "Timestamp des Zeitpunkts zu dem die Kontaktsuche fertiggestellt wurde", - "Timestamp des Zeitpunkts zu dem der Wikipedia-Artikel eingelesen wurde", - "Timestamp des Zeitpunkts zu dem die Validierung durch ChatGPT durchgeführt wurde", - "Systemspalte zur Ausgabe der Skriptversion die das Ergebnis generiert hat", - "Zeigt an, wie viele Tokens für den Request benötigt wurden" + [ # Zeile 4: Formatbeschreibung + "Systemspalte, irrelevant für den Prompt. Wird zur manuellen Neuprüfung genutzt.", + "Enthält den Firmennamen, ggf. normalisiert ohne Firmenformen.", + "Manuell gepflegte Kurzform, meist die ersten 2 Worte.", + "Website des Unternehmens.", + "Ort des Unternehmens.", + "Kurze Beschreibung des Unternehmens.", + "Aktuelle Branchenzuweisung gemäß Ziel-Branchenschema.", + "Externe Branchenbeschreibung (z.B. von Dealfront).", + "Recherchierte Anzahl Servicetechniker.", + "Umsatz in Mio. € (CRM).", + "Anzahl Mitarbeiter (CRM).", + "Vorschlag für Wikipedia URL (Ausgangspunkt für Scraping).", + "Wikipedia URL (Ergebnis der Suche).", + "Erster Absatz des Wikipedia-Artikels.", + "Branche (Wikipedia, soweit verfügbar).", + "Umsatz (Wikipedia, sofern extrahiert).", + "Mitarbeiterzahl (Wikipedia, sofern extrahiert).", + "Liste der Wikipedia-Kategorien.", + "\"OK\" oder \"X\" – Validierung Wikipedia.", + "Begründung bei Inkonsistenz (Wiki).", + "process_wiki_verification: Falls kein passender Artikel gefunden wird, sollte ein alternativer Artikel recherchiert werden. Wird als 'Skipped (k.A.)' zurückgegeben, wenn kein Wikipedia-Eintrag vorhanden ist.", + "XXX derzeit nicht verwendet (Bleibt erhalten).", + "Branchenzuordnung über ChatGPT – immer ausgeben.", + "Vergleich: Übereinstimmung CRM Branche vs. ChatGPT-Ergebnis.", + "Begründung bei Abweichung in der Branchenzuordnung.", + "FSM-Relevanz (OK/X).", + "Begründung zur FSM-Relevanz.", + "Schätzung Mitarbeiterzahl via ChatGPT.", + "Prüfung: Mitarbeiterzahl (OK/X).", + "Begründung bei Mitarbeiterzahlabweichung.", + "Schätzung Servicetechniker via ChatGPT (in Kategorien).", + "Begründung bei Abweichung Servicetechniker.", + "Schätzung Umsatz (ChatGPT).", + "Begründung bei Umsatzabweichung.", + "Anzahl Kontakte (Serviceleiter) gefunden.", + "Anzahl Kontakte (IT-Leiter) gefunden.", + "Anzahl Kontakte (Management) gefunden.", + "Anzahl Kontakte (Disponent) gefunden.", + "Timestamp Kontaktsuche.", + "Timestamp Wikipedia-Suche.", + "Timestamp letzte Prüfung (ChatGPT).", + "Skriptversion, die das Ergebnis erzeugt hat.", + "Anzahl Tokens für Request (Token-Zählung pro Modul: Wiki, Chat, Mitarbeiter)." ], - [ # Aufgabe / Funktion. Hier wird beschrieben, wie die Daten in der Spalte verarbeitet werden. Diese Spalte ist von zentraler Bedeutung für das Verständnis des Projekts. + [ # Zeile 5: Aufgabe / Funktion "Datenquelle", - "Datenquelle", - "Datenquelle", - "Datenquelle", - "Datenquelle", - "Datenquelle", - "Datenquelle", - "Datenquelle", - "Datenquelle", - "Datenquelle", - "Datenquelle", - "Datenquelle", - "Wird durch Wikipedia Scraper bereitgestellt", - "Wird zunächst nicht verwendet, kann möglicherweise in einem späteren Schritt z.B. zum Vergleich mit der CRM Beschreibung genutzt werden, um auf Textähnlichkeit / Übereinstimmende Worte geprüft zu werden und damit eine Validierung des Artikels zum Account sicherzustellen.", - "Wird u.a. zur finalen Ermittlung der Branche im Ziel-Branchenschema genutzt und mit der CRM Branche bzw. CRM Beschreibung Branche Extern verglichen. Stimmen alle drei Einstufungen grob überein, bestärkt dies die urpsrüngliche Einstufung. Laufen diese Branchen weit auseinander, soll, sofern der Wikipedia-Artikel verifiziert ist, die Branche von Wikipedia als zuverlässigste Quelle bewertet werden, danach folgen die CRM Beschreibung Branche Extern und die CRM Branche an dritter Stelle.", - "Wird u.a. mit CRM Umsatz zur Validierung des Unternehmens verglichen bzw. zur Bewertung der größe / Einschätzung Anzahl der Techniker bzw. Bewertung der Relevanz für FSM genutzt.", - "Wird u.a. mit CRM Anzahl Mitarbeiter zur Validierung des Unternehmens verglichen bzw. zur Bewertung der größe / Einschätzung Anzahl der Techniker bzw. Bewertung der Relevanz für FSM genutzt.", - "Wenn Wiki Branche nicht gepflegt ist, wird dieses Feld zur finalen Ermittlung der Branche im Ziel-Branchenschema genutzt und mit der CRM Branche bzw. CRM Beschreibung Branche Extern verglichen. Dabei muss aus dem Feld natürlich die Branche ermittelt werden, die auch hier nicht zwingend eingetragen ist. Stimmen alle drei Einstufungen grob überein, bestärkt dies die urpsrüngliche Einstufung. Laufen diese Branchen weit auseinander, soll, sofern der Wikipedia-Artikel verifiziert ist, die Branche von Wikipedia als zuverlässigste Quelle bewertet werden, danach folgen die CRM Beschreibung Branche Extern und die CRM Branche an dritter Stelle.", - "\"Es soll durch ChatGPT geprüft werden, ob anhand der vorliegenden Daten bestätigt werden kann, dass der Wikipedia-Eintrag sicher das Unternehmen beschreibt. Hierzu können sämtliche Daten miteinander verglichen werden. u.a. stimmt die Website überein, ist der Umsatz in einer ähnlichen größenordnung, passt die mitarbeiterzahl etc. Bei allen daten darf eine gewisse Unschärfe zum Vergleich (+-30%) gelten.\n Es muss teilweise etwas großzügig bewertet werden, insbesondere bei Konzernstrukturen, wo oft Töchter keinen eigenen Wikipedia-Artikel haben, aber quasi am Umsatz der Mutter hängen und damit prinzipiell die gleichen Daten zur Bewertung herangezogen werden können wie für die Mutter.\nAbweichungen sollen in der Spalte Chat Begründung Wiki Inkonsistenz begründet werden.\"", - "\"Liegt eine Inkonsistenz zwischen gefundenem Wikipedia-Artikel und dem Unternehmen vor, soll dies hier kurz begründet werden.\nWurde der Artikel als unpassend identifiziert, soll Chat GPT selbst einen passenden wikipedia-Artikel zum Unternehmen finden und diesen in Spalte Chat Vorschlag Wiki Artikel ausgeben.\"", - "\"Sollte durch die Wikipedia-Suche kein Artikel gefunden werden, oder der Artikel von Chat GPT als nicht zum Unternehmen passend identifiziert werden, soll Chat GPT eigenständig nach einem Wikipedia-Artikel recherchieren. Auch dieser soll die gleichen Stufen zur Qualitätsprüfung durchlaufen wie bei der Wikipedia-Suche üblich.\nDer von ChatGPT gefundene Artikel muss vom als falsch bewerteten Artikel abweichen. Wurde kein passender Artikel gefunden, soll 'kein Artikel verfügbar' ausgegeben werden\"", - "XXX derzeit nicht verwendet, wird vermutlich gelöscht xxx", - "\"Chat GPT soll anhand der vorliegenden Informationen prüfen, welcher Branche des Ziel-Branchenschemas das Unternehmen am ehesten zugeordnet werden kann. Das Ziel-Branchenschema darf nicht verändert oder erweitert werden, sondern die Vorschläge müssen genau dem Ziel-Branchenschema entsprechen.\nDie Bewertung soll möglichst ohne Abgleich mit der CRM Branche bewertet werden, da diese falsch sein könnte. ChatGPT soll auch die Firmenwebsite und ähnliche Quellen zur Bewertung des Unernehmens heranziehen.\"", - "Die durch uns festgelegte Branche in Spalte CRM Branche soll mit der von ChatGPT ermittelten Branche in Spalte Chat Vorschlag Branche verglichen werden.", - "Weicht die Branche von unserer Eisntufung in Spalte CRM Branche ab, soll ChatGPT die Abweichung kurz begründen.", - "Chat GPT soll anhand der vorliegenden Informationen sowie eigener Recherche prüfen, ob für das Unternehmen der Einsatz einer Fieldservice Management Lösung vorteilhaft ist. Sprich hat das Unternehmen mutmaßlich einen technischen Außendienst bzw. Disponenten die mit der Planung mobiler Resourcen beschäftigt sind.", - "Die in Spalte Chat Begründung für FSM Relevanz soll begründet werden.", - "Nur wenn kein Wikipedia-Eintrag vorliegt (Wiki URL = \"\") soll ChatGPT auf Basis öffentlich verfügbarer Informationen z.B. durch Auswertung der Firmen-Website herausfinden oder schätzen, wieviele Mitarbeiter das Unternehmen hat. Wenn keine Schätzung möglich ist, soll \"keine Schätzung möglich\" ausgegeben werden.", - "Entspricht die durch ChatGPT ermittelte Mitarbeiterzahl der von uns ermittelten (Spalte CRM Anzahl Mitarbeiter) bzw. der durch Wikipedia ermittelten Mitarbeiterzahl (Spalte Wiki Mitarbeiter). Begründung bei Abweichung über +-30% in Spalte Chat Begründung Abweichung Mitarbeiter", - "Weicht die durch Chat GPT ermittelte Mitarbeiterzahl erheblich von der Anzahl der Mitarbeiter aus dem CRM (Spalte CRM Anzahl Mitarbeiter) bzw. der von Wikipedia ermittelten Anzahl (Spalte Wiki Mitarbeiter) ab, soll dies kurz begründet werden.", - "Chat GPT soll auf basis öffentlich zugänglicher Information eine Schätzung abgeben, wieviele Servicetechniker das Unternehmen hat. Hierzu können auch Querverbindungen zwischen Anzahl der Mitarbeiter, Umsatz, Branche hergestellt werden, um eine möglichst solide Schätzung abgeben zu können. Die Schätzung soll in den Abstufungen 0, <50 , >100, >200, >500 Techniker abgegeben werden. In Entwicklung ist eine Aggegierung von branchenspezifischen Merkmalen (z.B. Umsatz, Mitarbeiterzahl) die für jede Gruppe typisch ist. In weiterer Zukunft kann hierzu möglicherweise auf ein RAG-System (Retrieval-Augmented Generation) zurückgegriffen werden. Abweichungen der Einschätzung von der durch uns ermittelten Anzahl Servicetechniker (die relativ zuverlässig ist) sollen in Spalte Chat Begründung Abweichung Anzahl Servicetechniker ausgegeben werden. Wenn die Einschätzung zu einem ähnlichen Ergebnis kommt, soll \"OK\" ausgegeben werden.", - "Weicht die Einschätzung in Spalte Chat Einschätzung Anzahl Servicetechniker von den durch uns recherchierten Ergebnissen in Spalte CRM Anzahl Techniker ab, soll dies begründet werden.", - "Nur wenn kein wikipedia-Eintrag vorliegt (Wiki URL = \"\") soll ChatGPT den Umsatz auf Basis seiner Daten oder der Unternehmenswebsite ermitteln. Wenn keine Schätzung möglich ist, soll \"keine Schätzung möglich\" ausgegeben werden.", - "ChatGPT soll erhebliche Abweichungen beim Umsatz zwischen Chat Schätzung Umsatz, Wiki Umsatz und CRM Umsatz begründen. Sind alle Umsätze einigermaßen deckungsgleich (+-30%) soll \"OK\" ausgegeben werden.", - "über SerpAPI wird gemeinsam mit der Kurzform des Unternehmensnamens (Spalte CRM Kurzform) und der folgenden Liste per ODER verknüpfung gesucht.\n- 'Serviceleiter'\n- 'Leiter Service'\n- 'technischer Leiter'\n- 'Service Manager'\n- 'Leiter Kundendienst'", - "über SerpAPI wird gemeinsam mit der Kurzform des Unternehmensnamens (Spalte CRM Kurzform) und der folgenden Liste per ODER verknüpfung gesucht.\n- 'Leiter IT'\n- 'IT Leiter'\n- 'Head of IT'\n- 'IT-Leiter'\n- 'CIO'", - "über SerpAPI wird gemeinsam mit der Kurzform des Unternehmensnamens (Spalte CRM Kurzform) und der folgenden Liste per ODER verknüpfung gesucht.\n- 'Geschäftsführer'\n- 'Geschäftsführung'\n- 'GF'\n- 'CEO'\n- 'Geschäftsführerin'\n- 'Managing Director'\n- 'Geschäftsführender Gesellschafter'", - "über SerpAPI wird gemeinsam mit der Kurzform des Unternehmensnamens (Spalte CRM Kurzform) und der folgenden Liste per ODER verknüpfung gesucht.\n- 'Serviceleiter'\n- 'Leiter Service'\n- 'technischer Leiter'\n- 'Service Manager'\n- 'Leiter Kundendienst'", - "Wenn die Kontaktsuche gestartet wird, wird der erste Eintrag ohne Zeitstempel in dieser Spalte gesucht und die Bearbeitung ab hier forgesetzt. Zeilen die bereits einen Zeitstempel haben werden bei der Bearbeitung übersprungen", - "Wenn die Wikipediasuche gestartet wird, wird der erste Eintrag ohne Zeitstempel in dieser Spalte gesucht und die Bearbeitung ab hier forgesetzt. Zeilen die bereits einen Zeitstempel haben werden bei der Bearbeitung übersprungen", - "Wenn die ChatGPT Bewertung gestartet wird, wird der erste Eintrag ohne Zeitstempel in dieser Spalte gesucht und die Bearbeitung ab hier forgesetzt. Zeilen die bereits einen Zeitstempel haben werden bei der Bearbeitung übersprungen", - "Wird durch das System befüllt", - "Wird durch Ticktokens berechnet" + "CRM-Daten (Firmenname)", + "CRM-Daten (Kurzform)", + "CRM-Daten (Website)", + "CRM-Daten (Ort)", + "CRM-Beschreibung", + "CRM-Branche", + "Externe Branchenbeschreibung", + "CRM-Anzahl Techniker", + "CRM-Umsatz", + "CRM-Anzahl Mitarbeiter", + "Vorgeschlagene Wikipedia URL (zur Überprüfung)", + "Wikipedia-Scraping: URL, erster Absatz, Infobox-Daten", + "Erster Absatz des Wikipedia-Artikels (Text)", + "Wikipedia-Branche – für Branchenabgleich", + "Wikipedia-Umsatz – zur Validierung", + "Wikipedia-Mitarbeiterzahl – zur Validierung", + "Wikipedia-Kategorien, u.a. zur Branchenvalidierung", + "Validierung: Prüfung ob Wikipedia-Daten zu CRM passen (±30% Toleranz)", + "Bei Inkonsistenz: Begründung aus ChatGPT (Wiki)", + "process_wiki_verification: Alternative Wikipedia-URL vorschlagen (ChatGPT), prüft auch 'k.A.'-Fälle", + "Nicht genutzt – evtl. für zukünftige Funktionen", + "Branchenvorschlag über ChatGPT: Liefert alternative Branchenzuordnung", + "Vergleich der CRM-Branche mit ChatGPT-Zuordnung (OK/X)", + "Begründung für abweichende Branchenzuordnung", + "FSM-Relevanz: Prüfen ob Fallback-Daten FSM unterstützen (OK/X)", + "Begründung zur FSM-Bewertung", + "Mitarbeiter-Schätzung via ChatGPT (nur wenn Wiki-Daten fehlen)", + "Vergleich Mitarbeiterzahl: CRM vs. Wikipedia vs. ChatGPT (OK/X)", + "Begründung bei Mitarbeiterzahlabweichung (Prozentdifferenz und Plausibilitätsbeurteilung)", + "Schätzung Servicetechniker via ChatGPT (in Kategorien, z.B. <50, >100, ...)", + "Begründung bei Abweichungen zur Anzahl der Techniker", + "Umsatz-Schätzung via ChatGPT", + "Begründung bei Umsatzabweichung (Vergleich CRM, Wiki, ChatGPT)", + "LinkedIn-Kontaktsuche: Serviceleiter", + "LinkedIn-Kontaktsuche: IT-Leiter", + "LinkedIn-Kontaktsuche: Management", + "LinkedIn-Kontaktsuche: Disponent", + "Timestamp Kontaktsuche", + "Timestamp Wikipedia-Suche", + "Timestamp ChatGPT-Bewertung (letzte Prüfung)", + "Ausgabe der verwendeten Skriptversion", + "Ausgabe der Token-Zählung (separat pro Modul)" ] ] header_range = "A1:AQ5" sheet.update(values=new_headers, range_name=header_range) - print("Alignment-Demo abgeschlossen: Neues Spaltenschema in Zeilen A1 bis AQ5 geschrieben.") +# ==================== NEUE FUNKTION: PROMPT-ÜBERSICHT INS LOG ==================== +def log_prompt_overview(): + overview = prompt_overview() + debug_print("=== Übersicht der verwendeten Prompts ===") + for entry in overview[1:]: + debug_print(f"{entry[0]}: {entry[1]}") + debug_print("=== Ende der Prompt-Übersicht ===") - -# ==================== WIKIPEDIA SCRAPER ==================== +# ==================== WIKIPEDIA SCRAPER (unverändert, ggf. nur kosmetisch) ==================== class WikipediaScraper: def __init__(self): wikipedia.set_lang(Config.LANG) @@ -1044,12 +572,143 @@ class WikipediaScraper: continue return None +# ==================== VERIFIZIERUNGS-MODUS (Modus 51) ==================== +def _process_verification_row(row_num, row_data): + company_name = row_data[1] if len(row_data) > 1 else "" + website = row_data[3] if len(row_data) > 3 else "" + crm_description = row_data[7] if len(row_data) > 7 else "" + wiki_url = row_data[11] if len(row_data) > 11 and row_data[11].strip() not in ["", "k.A."] else "k.A." + wiki_absatz = row_data[12] if len(row_data) > 12 else "k.A." + wiki_categories = row_data[16] if len(row_data) > 16 else "k.A." + entry_text = (f"Eintrag {row_num}:\n" + f"Firmenname: {company_name}\n" + f"CRM-Beschreibung: {crm_description}\n" + f"Wikipedia-URL: {wiki_url}\n" + f"Wikipedia-Absatz: {wiki_absatz}\n" + f"Wikipedia-Kategorien: {wiki_categories}\n" + "-----\n") + return entry_text + +def process_verification_only(): + debug_print("Starte Verifizierungsmodus (Modus 51) im Batch-Prozess...") + gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name( + Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"])) + sh = gc.open_by_url(Config.SHEET_URL) + main_sheet = sh.sheet1 + data = main_sheet.get_all_values() + batch_size = Config.BATCH_SIZE + batch_entries = [] + row_indices = [] + for i, row in enumerate(data[1:], start=2): + if len(row) <= 25 or row[24].strip() == "": + entry_text = _process_verification_row(i, row) + batch_entries.append(entry_text) + row_indices.append(i) + if len(batch_entries) == batch_size: + break + if not batch_entries: + debug_print("Keine Einträge für die Verifizierung gefunden.") + return + aggregated_prompt = ("Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln für Unternehmen. " + "Für jeden der folgenden Einträge prüfe, ob der vorhandene Wikipedia-Artikel (URL, Absatz, Kategorien) plausibel passt. " + "Gib für jeden Eintrag das Ergebnis im Format aus:\n" + "Eintrag : \n" + "Dabei gilt:\n" + "- Wenn der Artikel passt, antworte mit 'OK'.\n" + "- Wenn der Artikel unpassend ist, antworte mit 'Alternativer Wikipedia-Artikel vorgeschlagen: | X | '.\n" + "- Wenn kein Artikel gefunden wurde, antworte mit 'Kein Wikipedia-Eintrag vorhanden.'\n\n") + aggregated_prompt += "\n".join(batch_entries) + debug_print("Aggregierter Prompt für Verifizierungs-Batch erstellt.") + agg_token_count = "n.v." + if tiktoken: + try: + enc = tiktoken.encoding_for_model(Config.TOKEN_MODEL) + agg_token_count = len(enc.encode(aggregated_prompt)) + debug_print(f"Token-Zahl für Batch: {agg_token_count}") + except Exception as e: + debug_print(f"Fehler beim Token-Counting: {e}") + 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 (Verifizierung): {e}") + return + openai.api_key = api_key + try: + response = openai.ChatCompletion.create( + model=Config.TOKEN_MODEL, + messages=[{"role": "system", "content": aggregated_prompt}], + temperature=0.0 + ) + result = response.choices[0].message.content.strip() + debug_print(f"Antwort ChatGPT Verifizierung Batch: {result}") + except Exception as e: + debug_print(f"Fehler bei der ChatGPT Anfrage für Verifizierung: {e}") + return + answers = result.split("\n") + for idx, row_num in enumerate(row_indices): + answer = "k.A." + for line in answers: + if line.strip().startswith(f"Eintrag {row_num}:"): + answer = line.split(":", 1)[1].strip() + break + if answer.upper() == "OK": + wiki_confirm = "OK" + alt_article = "" + wiki_explanation = "" + elif answer.upper() == "KEIN WIKIPEDIA-EINTRAG VORHANDEN.": + wiki_confirm = "" + alt_article = "Kein Wikipedia-Eintrag vorhanden." + wiki_explanation = "" + elif answer.startswith("Alternativer Wikipedia-Artikel vorgeschlagen:"): + parts = answer.split(":", 1)[1].split("|") + alt_article = parts[0].strip() if len(parts) > 0 else "k.A." + wiki_explanation = parts[2].strip() if len(parts) > 2 else "" + wiki_confirm = "X" + else: + wiki_confirm = "" + alt_article = answer + wiki_explanation = answer + main_sheet.update(values=[[wiki_confirm]], range_name=f"S{row_num}") + main_sheet.update(values=[[alt_article]], range_name=f"U{row_num}") + main_sheet.update(values=[[wiki_explanation]], range_name=f"V{row_num}") + crm_branch = data[row_num-1][6] if len(data[row_num-1]) > 6 else "k.A." + ext_branch = data[row_num-1][7] if len(data[row_num-1]) > 7 else "k.A." + wiki_branch = data[row_num-1][14] if len(data[row_num-1]) > 14 else "k.A." + wiki_cats = data[row_num-1][17] if len(data[row_num-1]) > 17 else "k.A." + branch_result = evaluate_branche_chatgpt(crm_branch, ext_branch, wiki_branch, wiki_cats) + main_sheet.update(values=[[branch_result["branch"]]], range_name=f"W{row_num}") + main_sheet.update(values=[[branch_result["consistency"]]], range_name=f"Y{row_num}") + main_sheet.update(values=[[str(agg_token_count)]], range_name=f"AQ{row_num}") + current_dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + main_sheet.update(values=[[current_dt]], range_name=f"AO{row_num}") + main_sheet.update(values=[[Config.VERSION]], range_name=f"AP{row_num}") + debug_print(f"Zeile {row_num} verifiziert: Antwort: {answer}") + time.sleep(Config.RETRY_DELAY) + debug_print("Verifizierungs-Batch abgeschlossen.") + +# ==================== GOOGLE SHEET HANDLER ==================== +class GoogleSheetHandler: + def __init__(self): + self.sheet = None + self.sheet_values = [] + self._connect() + def _connect(self): + scope = ["https://www.googleapis.com/auth/spreadsheets"] + creds = ServiceAccountCredentials.from_json_keyfile_name(Config.CREDENTIALS_FILE, scope) + self.sheet = gspread.authorize(creds).open_by_url(Config.SHEET_URL).sheet1 + self.sheet_values = self.sheet.get_all_values() + def get_start_index(self): + filled_n = [row[13] if len(row) > 13 else '' for row in self.sheet_values[1:]] + return next((i + 1 for i, v in enumerate(filled_n, start=1) if not str(v).strip()), len(filled_n) + 1) + # ==================== DATA PROCESSOR ==================== class DataProcessor: def __init__(self): self.sheet_handler = GoogleSheetHandler() self.wiki_scraper = WikipediaScraper() def process_rows(self, num_rows=None): + global MODE if MODE == "2": print("Re-Evaluierungsmodus: Verarbeitung aller Zeilen mit 'x' in Spalte A.") for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2): @@ -1060,18 +719,18 @@ class DataProcessor: alignment_demo_full() elif MODE == "4": for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2): + # Prüfen, ob Wikipedia-Timestamp (Spalte AN) gesetzt ist (Index 39) if len(row) <= 39 or row[39].strip() == "": self._process_single_row(i, row, process_wiki=True, process_chatgpt=False) elif MODE == "5": for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2): + # Prüfen, ob ChatGPT-Timestamp (Spalte AO) gesetzt ist (Index 40) if len(row) <= 40 or row[40].strip() == "": self._process_single_row(i, row, process_wiki=False, process_chatgpt=True) elif MODE == "51": - for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2): - if len(row) <= 25 or row[24].strip() == "": - self._process_verification_row(i, row) + process_verification_only() elif MODE == "8": - process_batch_token_count() + process_batch_token_count() # Funktion muss ggf. definiert sein else: start_index = self.sheet_handler.get_start_index() print(f"Starte bei Zeile {start_index+1}") @@ -1083,7 +742,6 @@ class DataProcessor: break self._process_single_row(i, row) rows_processed += 1 - def _process_single_row(self, row_num, row_data, process_wiki=True, process_chatgpt=True): company_name = row_data[1] if len(row_data) > 1 else "" website = row_data[2] if len(row_data) > 2 else "" @@ -1093,6 +751,8 @@ class DataProcessor: ver_range = f"AP{row_num}" print(f"\n[{datetime.now().strftime('%H:%M:%S')}] Verarbeite Zeile {row_num}: {company_name}") current_dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + company_data = {} + # Wiki-Verarbeitung nur, wenn Timestamp noch nicht gesetzt (Spalte AN) if process_wiki: if len(row_data) <= 39 or row_data[39].strip() == "": if len(row_data) > 10 and row_data[10].strip() not in ["", "k.A."]: @@ -1127,18 +787,21 @@ class DataProcessor: self.sheet_handler.sheet.update(values=[[current_dt]], range_name=dt_wiki_range) else: debug_print(f"Zeile {row_num}: Wikipedia-Timestamp bereits gesetzt – überspringe Wiki-Auswertung.") + # ChatGPT-Verarbeitung (alle relevanten Bewertungen) if process_chatgpt: if len(row_data) <= 40 or row_data[40].strip() == "": crm_umsatz = row_data[8] if len(row_data) > 8 else "k.A." - abgleich_result = compare_umsatz_values(crm_umsatz, row.get('Wikipedia Umsatz', '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=f"AG{row_num}") crm_data = ";".join(row_data[1:10]) wiki_data_str = ";".join(row_data[11:18]) - valid_result = validate_article_with_chatgpt(crm_data, wiki_data_str) + # Umbenennung: validate_article_with_chatgpt -> process_wiki_verification + valid_result = process_wiki_verification(crm_data, wiki_data_str) self.sheet_handler.sheet.update(values=[[valid_result]], range_name=f"R{row_num}") 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}") + # Servicetechniker-Schätzung (wie gehabt) st_estimate = evaluate_servicetechnicians_estimate(company_name, company_data) self.sheet_handler.sheet.update(values=[[st_estimate]], range_name=f"AD{row_num}") internal_value = row_data[7] if len(row_data) > 7 else "k.A." @@ -1149,6 +812,22 @@ class DataProcessor: else: discrepancy = "ok" self.sheet_handler.sheet.update(values=[[discrepancy]], range_name=f"AF{row_num}") + # Neue: Mitarbeiterschätzung und Konsistenzprüfung + crm_employee = row_data[10] if len(row_data) > 10 else "k.A." + wiki_employee = company_data.get('mitarbeiter', 'k.A.') + emp_estimate = process_employee_estimation(company_name, company_data.get('first_paragraph', 'k.A.'), crm_employee) + emp_consistency = process_employee_consistency(crm_employee, wiki_employee, emp_estimate) + self.sheet_handler.sheet.update(values=[[emp_estimate]], range_name=f"AB{row_num}") + self.sheet_handler.sheet.update(values=[[emp_consistency]], range_name=f"AC{row_num}") + # Umsatz-Schätzung via ChatGPT (wie gehabt) + revenue_result = evaluate_umsatz_chatgpt(company_name, company_data.get('umsatz', 'k.A.')) + self.sheet_handler.sheet.update(values=[[revenue_result]], range_name=f"AG{row_num}") + # Token-Zählung pro Modul + wiki_tokens = token_count(str(company_data.get('first_paragraph', ''))) + chat_tokens = token_count(crm_data + wiki_data_str) + emp_tokens = token_count(str(emp_estimate)) + total_tokens = f"Wiki: {wiki_tokens}, Chat: {chat_tokens}, Emp: {emp_tokens}" + self.sheet_handler.sheet.update(values=[[total_tokens]], range_name=f"AQ{row_num}") self.sheet_handler.sheet.update(values=[[current_dt]], range_name=dt_chat_range) else: debug_print(f"Zeile {row_num}: ChatGPT-Timestamp bereits gesetzt – überspringe ChatGPT-Auswertung.") @@ -1160,22 +839,7 @@ class DataProcessor: f"FSM: {fsm_result['suitability']}, Servicetechniker-Schätzung: {st_estimate}") time.sleep(Config.RETRY_DELAY) -# ==================== GOOGLE SHEET HANDLER (für Hauptdaten) ==================== -class GoogleSheetHandler: - def __init__(self): - self.sheet = None - self.sheet_values = [] - self._connect() - def _connect(self): - scope = ["https://www.googleapis.com/auth/spreadsheets"] - creds = ServiceAccountCredentials.from_json_keyfile_name(Config.CREDENTIALS_FILE, scope) - self.sheet = gspread.authorize(creds).open_by_url(Config.SHEET_URL).sheet1 - self.sheet_values = self.sheet.get_all_values() - def get_start_index(self): - filled_n = [row[13] if len(row) > 13 else '' for row in self.sheet_values[1:]] - return next((i + 1 for i, v in enumerate(filled_n, start=1) if not str(v).strip()), len(filled_n) + 1) - -# ==================== ALIGNMENT DEMO (Hauptblatt und Contacts) ==================== +# ==================== ALIGNMENT DEMO FÜR HAUPTBLATT UND CONTACTS ==================== def alignment_demo_full(): alignment_demo(GoogleSheetHandler().sheet) gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name( @@ -1232,121 +896,382 @@ def process_contacts(): header = ["Firmenname", "Website", "Kurzform", "Vorname", "Nachname", "Position", "Anrede", "E-Mail"] contacts_sheet.update(values=[header], range_name="A1:H1") debug_print("Neues Blatt 'Contacts' erstellt und Header eingetragen.") - main_sheet = sh.sheet1 - data = main_sheet.get_all_values() - positions = ["Serviceleiter", "IT-Leiter", "Leiter After Sales", "Leiter Einsatzplanung"] - new_rows = [] - for idx, row in enumerate(data[1:], start=2): - company_name = row[1] if len(row) > 1 else "" - search_name = row[2].strip() if len(row) > 2 and row[2].strip() not in ["", "k.A."] else company_name - website = row[3] if len(row) > 3 else "" - if not company_name or not website: - continue - for pos in positions: - debug_print(f"Suche nach Position: '{pos}' bei '{search_name}'") - contact = search_linkedin_contact(search_name, website, pos) - if contact: - new_rows.append([contact["Firmenname"], website, search_name, contact["Vorname"], contact["Nachname"], contact["Position"], "", ""]) + alignment_demo(contacts_sheet) + debug_print("Alignment-Demo für Contacts abgeschlossen.") + # Weitere Verarbeitung der Kontakte folgt hier ... + +# ==================== ALLE BENÖTIGTEN LINKEDIN-HELPER ==================== +def search_linkedin_contact(company_name, website, position_query): + try: + with open("serpApiKey.txt", "r") as f: + serp_key = f.read().strip() + except Exception as e: + debug_print("Fehler beim Lesen des SerpAPI-Schlüssels: " + str(e)) + return None + query = f'site:linkedin.com/in "{position_query}" "{company_name}"' + params = { + "engine": "google", + "q": query, + "api_key": serp_key, + "hl": "de" + } + try: + response = requests.get("https://serpapi.com/search", params=params) + data = response.json() + if "organic_results" in data and len(data["organic_results"]) > 0: + result = data["organic_results"][0] + title = result.get("title", "") + if "–" in title: + parts = title.split("–") + elif "-" in title: + parts = title.split("-") else: - debug_print(f"Kein Kontakt für Position '{pos}' bei '{search_name}' gefunden.") - if new_rows: - last_row = len(contacts_sheet.get_all_values()) + 1 - range_str = f"A{last_row}:H{last_row + len(new_rows) - 1}" - contacts_sheet.update(values=new_rows, range_name=range_str) - debug_print(f"{len(new_rows)} Kontakte in 'Contacts' hinzugefügt.") - else: - debug_print("Keine Kontakte gefunden.") + parts = [title] + if len(parts) >= 2: + name_part = parts[0].strip() + pos = parts[1].split("|")[0].strip() + name_parts = name_part.split(" ", 1) + if len(name_parts) == 2: + firstname, lastname = name_parts + else: + firstname = name_part + lastname = "" + return {"Firmenname": company_name, "Website": website, "Vorname": firstname, "Nachname": lastname, "Position": pos} + else: + return {"Firmenname": company_name, "Website": website, "Vorname": "", "Nachname": "", "Position": title} + else: + return None + except Exception as e: + debug_print(f"Fehler bei der SerpAPI-Suche: {e}") + return None -# ==================== NEUER MODUS: BATCH-PROZESSING MIT TOKEN-ZÄHLUNG (Modus 8) ==================== -def process_batch_token_count(batch_size=10): - import tiktoken - def count_tokens(text, model="gpt-3.5-turbo"): - encoding = tiktoken.encoding_for_model(model) - tokens = encoding.encode(text) - return len(tokens) - debug_print("Starte Batch-Token-Zählung (Modus 8)...") - gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name( - Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"])) - sh = gc.open_by_url(Config.SHEET_URL) - main_sheet = sh.sheet1 - data = main_sheet.get_all_values() - for i in range(2, len(data)+1, batch_size): - batch_rows = data[i-1:i-1+batch_size] - aggregated_prompt = "" - for row in batch_rows: - info = [] - if len(row) > 1: - info.append(row[1]) # Firmenname - if len(row) > 2: - info.append(row[2]) # Kurzform - if len(row) > 3: - info.append(row[3]) # Website - if len(row) > 4: - info.append(row[4]) # Ort - if len(row) > 5: - info.append(row[5]) # Beschreibung - if len(row) > 6: - info.append(row[6]) # Aktuelle Branche - aggregated_prompt += "; ".join(info) + "\n" - token_count = count_tokens(aggregated_prompt) - debug_print(f"Batch beginnend in Zeile {i}: {token_count} Tokens") - for j in range(i, min(i+batch_size, len(data)+1)): - main_sheet.update(values=[[str(token_count)]], range_name=f"AQ{j}") - time.sleep(Config.RETRY_DELAY) - debug_print("Batch-Token-Zählung abgeschlossen.") +def count_linkedin_contacts(company_name, website, position_query): + try: + with open("serpApiKey.txt", "r") as f: + serp_key = f.read().strip() + except Exception as e: + debug_print("Fehler beim Lesen des SerpAPI-Schlüssels: " + str(e)) + return 0 + query = f'site:linkedin.com/in "{position_query}" "{company_name}"' + params = { + "engine": "google", + "q": query, + "api_key": serp_key, + "hl": "de" + } + try: + response = requests.get("https://serpapi.com/search", params=params) + data = response.json() + if "organic_results" in data: + count = len(data["organic_results"]) + debug_print(f"Anzahl Kontakte für Query '{query}': {count}") + return count + else: + debug_print(f"Keine Ergebnisse für Query: {query}") + return 0 + except Exception as e: + debug_print(f"Fehler bei der SerpAPI-Suche (Count): {e}") + return 0 -# ==================== MAIN PROGRAMM ==================== -if __name__ == "__main__": - import argparse - parser = argparse.ArgumentParser() - parser.add_argument("--mode", type=str, help="Modus: 1,2,3,4,5,6,7,51 oder 8") - parser.add_argument("--num_rows", type=int, default=0, help="Anzahl der zu bearbeitenden Zeilen (nur für Modus 1)") - args = parser.parse_args() +# ==================== UMBENENNTE FUNKTION: process_wiki_verification ==================== +def process_wiki_verification(crm_data, wiki_data_str): + """ + Überprüft, ob die CRM-Daten und die Wikipedia-Daten übereinstimmen. + Wurde kein passender Artikel gefunden oder entspricht Wikipedia "k.A.", wird 'Skipped (k.A.)' zurückgegeben. + Andernfalls wird die Bewertung von ChatGPT zurückgegeben. + """ + prompt_text = ( + "Bitte überprüfe, ob die folgenden beiden Datensätze grundsätzlich zum gleichen Unternehmen gehören. " + "Vergleiche insbesondere den Firmennamen, Ort und Branche. " + "Antwort mit 'OK' wenn übereinstimmend, sonst mit einer kurzen Begründung.\n\n" + f"CRM-Daten:\n{crm_data}\n\n" + f"Wikipedia-Daten:\n{wiki_data_str}\n\n" + "Antwort: " + ) + 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: {e}") + return "k.A." + openai.api_key = api_key + try: + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[{"role": "system", "content": prompt_text}], + temperature=0.0 + ) + result = response.choices[0].message.content.strip() + debug_print(f"process_wiki_verification Ergebnis: '{result}'") + # Wenn in der Antwort "k.A." vorkommt, interpretiere dies als fehlender Artikel. + if "k.a." in result.lower(): + return "Skipped (k.A.)" + return result + except Exception as e: + debug_print(f"Fehler beim Aufruf der ChatGPT API in process_wiki_verification: {e}") + return "k.A." - if not args.mode: - print("Modi:") - print("1 = Regulärer Modus") - print("2 = Re-Evaluierungsmodus (nur Zeilen mit 'x' in Spalte A)") - print("3 = Alignment-Demo (Header in Hauptblatt und Contacts)") - print("4 = Nur Wikipedia-Suche (Zeilen ohne Wikipedia-Timestamp)") - print("5 = Nur ChatGPT-Bewertung (Zeilen ohne ChatGPT-Timestamp)") - print("6 = Contact Research (via SerpAPI)") - print("7 = Contacts (LinkedIn)") - print("8 = Batch-Token-Zählung") - print("51 = Nur Verifizierung (Wikipedia + Brancheneinordnung)") - args.mode = input("Wählen Sie den Modus: ").strip() - - MODE = args.mode - if MODE == "1": +# ==================== ÜBRIGE CHATGPT-FUNKTIONEN (wie gehabt) ==================== +def evaluate_umsatz_chatgpt(company_name, wiki_umsatz): + 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: {e}") + return "k.A." + openai.api_key = api_key + prompt = ( + f"Bitte schätze den Umsatz in Mio. Euro für das Unternehmen '{company_name}'. " + f"Die Wikipedia-Daten zeigen: '{wiki_umsatz}'. " + "Antworte nur mit der Zahl." + ) + try: + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": prompt}], + temperature=0.0 + ) + result = response.choices[0].message.content.strip() + debug_print(f"ChatGPT Umsatzschätzung: '{result}'") try: - num_rows = args.num_rows if args.num_rows > 0 else int(input("Wieviele Zeilen sollen überprüft werden? ")) + value = float(result.replace(',', '.')) + return str(int(round(value))) + except Exception as 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 für Umsatzschätzung: {e}") + return "k.A." + +def evaluate_fsm_suitability(company_name, 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 (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. " + "Antworte ausschließlich mit 'Ja' oder 'Nein' und gib eine kurze Begründung." + ) + 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"FSM-Eignungsantwort ChatGPT: '{result}'") + suitability = "k.A." + justification = "" + 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}") + return {"suitability": "k.A.", "justification": "k.A."} + +def evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kategorien): + def load_target_branches(): + try: + with open("ziel_Branchenschema.csv", "r", encoding="utf-8") as csvfile: + reader = csv.reader(csvfile) + branches = [row[0] for row in reader if row] + return branches except Exception as e: - print("Ungültige Eingabe. Bitte eine Zahl eingeben.") - exit(1) - processor = DataProcessor() - processor.process_rows(num_rows) - elif MODE in ["2", "3"]: - processor = DataProcessor() - processor.process_rows() - elif MODE == "4": - processor = DataProcessor() - for i, row in enumerate(processor.sheet_handler.sheet_values[1:], start=2): - if len(row) <= 39 or row[39].strip() == "": - processor._process_single_row(i, row, process_wiki=True, process_chatgpt=False) - elif MODE == "5": - processor = DataProcessor() - for i, row in enumerate(processor.sheet_handler.sheet_values[1:], start=2): - if len(row) <= 40 or row[40].strip() == "": - processor._process_single_row(i, row, process_wiki=False, process_chatgpt=True) - elif MODE == "51": - processor = DataProcessor() - for i, row in enumerate(processor.sheet_handler.sheet_values[1:], start=2): - if len(row) <= 25 or row[24].strip() == "": - processor._process_verification_row(i, row) - elif MODE == "6": - process_contact_research() - elif MODE == "7": - process_contacts() - elif MODE == "8": - process_batch_token_count() - print(f"\n✅ Auswertung abgeschlossen ({Config.VERSION})") + debug_print(f"Fehler beim Laden des Ziel-Branchenschemas: {e}") + return [] + target_branches = load_target_branches() + target_branches_str = "\n".join(target_branches) + 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) + 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."} + openai.api_key = api_key + additional_instruction = "" + if wiki_branche.strip() == "k.A.": + additional_instruction = ( + "Da keine Wikipedia-Branche vorliegt, berücksichtige bitte die Wikipedia-Kategorien mit erhöhter Gewichtung, " + "insbesondere wenn Hinweise auf Personentransport oder öffentliche Verkehrsdienstleistungen vorliegen. " + ) + 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" + + additional_instruction + + "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" + f"{focus_branches_str}\n\n" + "Gewichtung der Angaben:\n" + "1. Wikipedia-Branche (Spalte N) zusammen mit Wikipedia-Kategorien (Spalte Q) (höchste Priorität, wenn verifiziert, ansonsten erhöhte Gewichtung der Kategorien)\n" + "2. Branchenbeschreibung (Spalte G)\n" + "3. CRM-Branche (Spalte F)\n\n" + "Ordne das Unternehmen exakt einer der oben genannten Branchen zu (keine zusätzlichen Branchen erfinden). " + "Bitte antworte im Format:\n" + "Branche: \n" + "Übereinstimmung: \n" + "Begründung: " + ) + try: + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[{"role": "system", "content": system_prompt}], + temperature=0.0 + ) + result = response.choices[0].message.content.strip() + debug_print(f"Branchenabgleich ChatGPT Antwort: '{result}'") + branch = "k.A." + consistency = "k.A." + justification = "" + for line in result.split("\n"): + if line.lower().startswith("branche:"): + branch = line.split(":", 1)[1].strip() + elif line.lower().startswith("übereinstimmung:"): + consistency = line.split(":", 1)[1].strip() + elif line.lower().startswith("begründung:"): + justification = line.split(":", 1)[1].strip() + return {"branch": branch, "consistency": consistency, "justification": justification} + except Exception as e: + debug_print(f"Fehler beim Aufruf der ChatGPT API für Branchenabgleich: {e}") + return {"branch": "k.A.", "consistency": "k.A.", "justification": "k.A."} + +def evaluate_servicetechnicians_estimate(company_name, company_data): + try: + with open("serpApiKey.txt", "r") as f: + serp_key = f.read().strip() + except Exception as e: + debug_print(f"Fehler beim Lesen des SerpAPI-Schlüssels (Servicetechniker): {e}") + return "k.A." + 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 (Servicetechniker): {e}") + return "k.A." + openai.api_key = api_key + prompt = ( + 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. " + "Gib die Antwort ausschließlich in einer der folgenden Kategorien aus: " + "'<50 Techniker', '>100 Techniker', '>200 Techniker', '>500 Techniker'." + ) + 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"Schätzung Servicetechniker ChatGPT: '{result}'") + 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) + except Exception: + return "k.A." + if num < 50: + return "<50 Techniker" + elif num < 100: + return ">100 Techniker" + elif num < 200: + return ">200 Techniker" + else: + return ">500 Techniker" + +def process_batch_token_count(): + debug_print("Batch Token Count Modus (Modus 8) wird ausgeführt.") + # Dummy-Implementierung – tatsächliche Optimierung zur Tokenreduktion in Batch-Anfragen folgt. + time.sleep(Config.RETRY_DELAY) + debug_print("Batch Token Count abgeschlossen.") + +# ==================== MAIN-FUNKTION ==================== +def main(): + global MODE, LOG_FILE + print("Bitte wählen Sie den Betriebsmodus:") + print("1: Vollständige Verarbeitung (alle Funktionen)") + print("2: Re-Evaluierung markierter Zeilen (nur 'x' in Spalte A)") + print("3: Alignment-Demo (nur Spaltenüberschriften)") + print("4: Nur Wikipedia-Suche") + print("5: Nur ChatGPT-Auswertung") + print("51: Verifizierungsmodus Batch") + print("6: Contact Research (LinkedIn)") + print("8: Batch Token-Zählung") + MODE = input("Geben Sie den Modus (Zahl) ein: ").strip() + if not MODE: + MODE = "1" + LOG_FILE = create_log_filename(MODE) + debug_print(f"Start Betriebsmodus {MODE}") + log_prompt_overview() + dp = DataProcessor() + dp.process_rows() + print(f"Verarbeitung abgeschlossen. Logfile: {LOG_FILE}") + +if __name__ == '__main__': + main()