+ soup.find(class_='content') # Oder
+ # Füge ggf. weitere spezifische Selektoren hinzu, die du oft siehst + ) + + if content_area: + debug_print(f"Gezielten Inhaltsbereich gefunden ({content_area.name}#{content_area.get('id')} oder .{content_area.get('class')}) für {url}") + else: + # --- Fallback: Body nehmen, ABER Banner versuchen zu entfernen --- + debug_print(f"Kein spezifischer Inhaltsbereich gefunden für {url}. Nutze Body und versuche Banner zu entfernen.") content_area = soup.find('body') if content_area: - banner_selectors = ['[id*="cookie"]', '[class*="cookie"]', '[id*="consent"]', '[class*="consent"]', '[id*="banner"]', '[class*="banner"]', '[role="dialog"]'] + # Versuche, häufige Cookie-Banner Strukturen zu entfernen + banner_selectors = [ + '[id*="cookie"]', # IDs die "cookie" enthalten + '[class*="cookie"]', # Klassen die "cookie" enthalten + '[id*="consent"]', # IDs die "consent" enthalten + '[class*="consent"]', # Klassen die "consent" enthalten + '[id*="banner"]', # IDs die "banner" enthalten (vorsichtig!) + '[class*="banner"]', # Klassen die "banner" enthalten (vorsichtig!) + '[role="dialog"]' # Oft für Popups/Banner genutzt (vorsichtig!) + ] banners_removed_count = 0 for selector in banner_selectors: try: potential_banners = content_area.select(selector) for banner in potential_banners: + # Zusätzliche Prüfung: Enthält das Element typischen Banner-Text? banner_text = banner.get_text(" ", strip=True).lower() keywords = ["cookie", "zustimm", "ablehnen", "einverstanden", "datenschutz", "privacy", "akzeptier"] if any(keyword in banner_text for keyword in keywords): - # debug_print(f"Entferne potenzielles Banner ({selector})...") - banner.decompose(); banners_removed_count += 1 - except Exception as e_select: debug_print(f"Fehler Banner-Entfernung '{selector}': {e_select}") - # if banners_removed_count > 0: debug_print(f"{banners_removed_count} Banner-Elemente entfernt.") + debug_print(f"Entferne potenzielles Banner ({selector}) mit Text: {banner_text[:100]}...") + banner.decompose() # Entferne das Element aus dem Baum + banners_removed_count += 1 + except Exception as e_select: + debug_print(f"Fehler beim Versuch Banner mit Selektor '{selector}' zu entfernen: {e_select}") + if banners_removed_count > 0: + debug_print(f"{banners_removed_count} potenzielle Banner-Elemente entfernt.") + + # --- Text extrahieren aus gefundenem Bereich (oder Body) --- if content_area: - for script_or_style in content_area(["script", "style"]): script_or_style.decompose() - text = content_area.get_text(separator=' ', strip=True); text = re.sub(r'\s+', ' ', text) + for script_or_style in content_area(["script", "style"]): # Skripte/Styles entfernen + script_or_style.decompose() + + text = content_area.get_text(separator=' ', strip=True) + text = re.sub(r'\s+', ' ', text) # Normalisiere Whitespace + + # --- Zusätzliche Prüfung: Ist der extrahierte Text *nur* Banner-Text? --- banner_keywords_strict = ["cookie", "zustimmen", "ablehnen", "einverstanden", "datenschutz", "privacy", "akzeptier", "einstellung", "partner", "analyse", "marketing"] - text_lower = text.lower(); keyword_hits = sum(1 for keyword in banner_keywords_strict if keyword in text_lower) - if len(text) < 500 and keyword_hits >= 3: debug_print(f"WARNUNG: Text für {url} scheint nur Banner zu sein. Verwerfe."); return "k.A. (Nur Cookie-Banner erkannt)" + text_lower = text.lower() + keyword_hits = sum(1 for keyword in banner_keywords_strict if keyword in text_lower) + + # Heuristik: Wenn der Text kurz ist UND viele Banner-Keywords enthält -> Verwerfen + if len(text) < 500 and keyword_hits >= 3: + debug_print(f"WARNUNG: Extrahierter Text für {url} scheint nur Cookie-Banner zu sein (Länge {len(text)}, {keyword_hits} Keywords). Verwerfe Text.") + return "k.A. (Nur Cookie-Banner erkannt)" + result = text[:max_length] - # debug_print(f"Website {url} OK. Text ({len(result)}): {result[:100]}...") + debug_print(f"Website {url} erfolgreich gescrapt. Extrahierter Text (Länge {len(result)}): {result[:100]}...") return result - else: debug_print(f"Kein gefunden in {url}"); return "k.A." + else: + debug_print(f"Kein oder spezifischer Inhaltsbereich gefunden in {url}") + return "k.A." except requests.exceptions.SSLError as e: - debug_print(f"SSL-Fehler {url}: {e}. Versuche ohne verify...") - if verify_cert: return get_website_raw(url, max_length, verify_cert=False) - else: return "k.A." - except requests.exceptions.RequestException as e: debug_print(f"Netzwerk/HTTP Fehler {url}: {e}"); return "k.A." - except Exception as e: debug_print(f"Allgemeiner Fehler Scraping {url}: {e}"); return "k.A." + debug_print(f"SSL-Fehler beim Abrufen der Website {url}: {e}. Versuche ohne Zertifikatsprüfung...") + if verify_cert: + return get_website_raw(url, max_length, verify_cert=False) + else: + return "k.A." + except requests.exceptions.RequestException as e: + debug_print(f"Netzwerk-/HTTP-Fehler beim Abrufen der Website {url}: {e}") + return "k.A." + except Exception as e: + debug_print(f"Allgemeiner Fehler beim Scraping von {url}: {e}") + return "k.A." + +# Die Hilfsfunktion summarize_batch_openai wird weiterhin benötigt +# (Code dafür bleibt wie in der Antwort von 16:24 Uhr) +@retry_on_failure +def summarize_batch_openai(tasks_data): + """ + Fasst eine Liste von Rohtexten in einem einzigen OpenAI API Call zusammen. + Die Prüfung auf das Token-Limit wird jetzt primär der API überlassen. + + Args: + tasks_data (list): Eine Liste von Dictionaries, jedes enthält: + {'row_num': int, 'raw_text': str} + + Returns: + dict: Ein Dictionary, das Zeilennummern auf ihre Zusammenfassungen mappt. + z.B. {2122: "Zusammenfassung A", 2123: "Zusammenfassung B"} + Bei Fehlern oder fehlenden Zusammenfassungen wird "k.A." verwendet. + """ + if not tasks_data: return {} + + # Filtere Tasks, die gültigen Text haben + valid_tasks = [t for t in tasks_data if t.get("raw_text") and t["raw_text"] not in ["k.A.", "k.A. (Nur Cookie-Banner erkannt)", "k.A. (Fehler)"] and str(t.get("raw_text")).strip()] + if not valid_tasks: + debug_print("Keine gültigen Rohtexte für Batch-Zusammenfassung gefunden.") + return {t['row_num']: "k.A. (Kein gültiger Rohtext)" for t in tasks_data} + + debug_print(f"Starte Batch-Zusammenfassung für {len(valid_tasks)} gültige Texte (Zeilen: {[t['row_num'] for t in valid_tasks]})...") + + # --- Aggregierten Prompt erstellen --- + prompt_parts = [ + "Du bist ein KI-Assistent...", # (Rest des Prompts wie gehabt) + "RESULTAT : ", + "\n--- Texte zur Zusammenfassung ---" + ] + text_block = "" + row_numbers_in_batch = [] # Zeilen, die tatsächlich im Prompt landen + + # Baue den Textblock ohne interne Längenprüfung zusammen + for task in valid_tasks: + row_num = task['row_num'] + raw_text = task['raw_text'] + # Kürzen sollte in get_website_raw passieren, aber zur Sicherheit: + raw_text = raw_text[:1500] # Limitiere jeden Text auf max 1500 Zeichen im Prompt + + entry_text = f"\n--- TEXT Zeile {row_num} ---\n{raw_text}\n--- ENDE TEXT Zeile {row_num} ---\n" + text_block += entry_text + row_numbers_in_batch.append(row_num) # Füge die Zeilennummer hinzu + + # --- Interne Längenprüfung ENTFERNT --- + # max_chars_per_batch = 15000 # Nicht mehr relevant für die Logik hier + # if total_chars + len(entry_text) > max_chars_per_batch: # ENTFERNT + # debug_print(f"WARNUNG: ...") # ENTFERNT + # continue # ENTFERNT + + if not row_numbers_in_batch: + # Sollte nur passieren, wenn valid_tasks leer war + debug_print("Keine Texte im Batch für OpenAI.") + return {t['row_num']: "k.A. (Validierungsfehler?)" for t in tasks_data} + + prompt_parts.append(text_block) + prompt_parts.append("--- Ende der Texte ---") + prompt_parts.append("Bitte gib NUR die 'RESULTAT : ...' Zeilen zurück.") + final_prompt = "\n".join(prompt_parts) + + # Optional: Token zählen zur Info, aber nicht zur Blockade + try: + prompt_tokens = token_count(final_prompt) + debug_print(f"Geschätzte Prompt-Tokens für Batch: {prompt_tokens} (Limit ca. 4096 für gpt-3.5-turbo)") + if prompt_tokens > 3500: # Nur eine Warnung + debug_print("WARNUNG: Geschätzte Prompt-Tokens hoch, API könnte Fehler werfen.") + except Exception as e_tc: + debug_print(f"Fehler beim Token-Zählen: {e_tc}") + + + # --- OpenAI API Call (Die API wirft Fehler bei Token-Limit) --- + chat_response = call_openai_chat(final_prompt, temperature=0.2) + + # --- Antwort parsen (wie gehabt) --- + summaries = {row_num: "k.A. (Keine Antwort geparst)" for row_num in row_numbers_in_batch} + if chat_response: + # ... (Parsing-Logik bleibt gleich) ... + lines = chat_response.strip().split('\n'); parsed_count = 0 + for line in lines: + match = re.match(r"RESULTAT (\d+): (.*)", line.strip()) + if match: + row_num = int(match.group(1)); summary_text = match.group(2).strip() + if row_num in summaries: summaries[row_num] = summary_text; parsed_count += 1 + debug_print(f"Batch-Zusammenfassung: {parsed_count} von {len(row_numbers_in_batch)} erfolgreich geparst.") + if parsed_count < len(row_numbers_in_batch): debug_print(f"WARNUNG: Nicht alle Zusammenfassungen geparst. Antwort: {chat_response[:500]}...") + else: + debug_print("Fehler: Keine gültige Antwort von OpenAI für Batch-Zusammenfassung erhalten.") + # Wenn der API Call fehlschlägt (z.B. Token Limit), ist chat_response None, + # alle summaries bleiben "k.A." + + # Füge k.A. für Tasks hinzu, die ungültigen Rohtext hatten (aus valid_tasks gefiltert) + for task in tasks_data: + if task['row_num'] not in summaries: + summaries[task['row_num']] = "k.A. (Ungültiger Rohtext o.ä.)" + + return summaries + # ==================== OPENAI / CHATGPT FUNCTIONS ==================== @retry_on_failure -def call_openai_chat(prompt, temperature=0.3, model=None): # Unverändert +def call_openai_chat(prompt, temperature=0.3, model=None): """Zentrale Funktion für OpenAI Chat API Aufrufe.""" - if not Config.API_KEYS.get('openai'): debug_print("Fehler: OpenAI API Key fehlt."); return None - if not prompt: debug_print("Fehler: Leerer Prompt."); return None + if not Config.API_KEYS.get('openai'): + debug_print("Fehler: OpenAI API Key nicht konfiguriert.") + return None + if not prompt: + debug_print("Fehler: Leerer Prompt für OpenAI.") + return None + current_model = model if model else Config.TOKEN_MODEL + try: - # Optional: Token zählen vor Senden + # Token zählen vor dem Senden (optional, aber gut für Debugging) # prompt_tokens = token_count(prompt) # debug_print(f"Sende Prompt an OpenAI ({current_model}, {prompt_tokens} Tokens)...") + response = openai.ChatCompletion.create( - model=current_model, messages=[{"role": "user", "content": prompt}], temperature=temperature ) + model=current_model, + messages=[{"role": "user", "content": prompt}], + temperature=temperature + ) result = response.choices[0].message.content.strip() - # Optional: Token zählen Antwort - # completion_tokens = token_count(result); total_tokens = response.usage.total_tokens - # debug_print(f"OpenAI Antwort erhalten ({completion_tokens}/{total_tokens} Tokens).") + + # Token zählen für die Antwort + # completion_tokens = token_count(result) + # total_tokens = response.usage.total_tokens + # debug_print(f"OpenAI Antwort erhalten ({completion_tokens} Completion Tokens, {total_tokens} Gesamt).") + return result except openai.error.InvalidRequestError as e: debug_print(f"OpenAI Invalid Request Error: {e}") - if "maximum context length" in str(e): debug_print("Fehler scheint Token Limit zu sein.") + # Hier könnte man prüfen, ob es am Token Limit liegt + if "maximum context length" in str(e): + debug_print("Fehler scheint Token Limit zu sein. Prompt evtl. zu lang.") + # TODO: Strategie für zu lange Prompts (kürzen, splitten?) return None - except openai.error.OpenAIError as e: debug_print(f"OpenAI API Fehler: {e}"); raise e # Für Retry - except Exception as e: debug_print(f"Allgemeiner Fehler bei OpenAI-Aufruf: {e}"); raise e # Für Retry + except openai.error.OpenAIError as e: # Fängt RateLimitError, APIError etc. ab + debug_print(f"OpenAI API Fehler: {e}") + raise # Fehler weitergeben, damit retry_on_failure greifen kann + except Exception as e: + debug_print(f"Allgemeiner Fehler bei OpenAI-Aufruf: {e}") + raise # Fehler weitergeben -def summarize_website_content(raw_text): # Unverändert +def summarize_website_content(raw_text): """Erstellt Zusammenfassung von Website-Rohtext via OpenAI.""" - if not raw_text or raw_text == "k.A." or raw_text == "k.A. (Nur Cookie-Banner erkannt)" or raw_text.strip() == "": + if not raw_text or raw_text == "k.A." or raw_text.strip() == "": return "k.A." - max_raw_length = 3000 + + # Kürze den Rohtext, falls er sehr lang ist, um Token zu sparen/Limits zu vermeiden + max_raw_length = 3000 # Zeichenlimit für den Input der Zusammenfassung if len(raw_text) > max_raw_length: - # debug_print(f"Kürze Rohtext für Zusammenfassung: {len(raw_text)} -> {max_raw_length} Zeichen.") + debug_print(f"Kürze Rohtext für Zusammenfassung von {len(raw_text)} auf {max_raw_length} Zeichen.") raw_text = raw_text[:max_raw_length] + prompt = ( "Du bist ein KI-Assistent, der Webinhalte analysiert.\n" "Fasse den folgenden Text einer Unternehmenswebsite prägnant zusammen. " @@ -908,87 +2142,233 @@ def summarize_website_content(raw_text): # Unverändert "- Wichtigste Produkte und/oder Dienstleistungen\n" "- Zielgruppe (falls erkennbar)\n\n" f"Website-Text:\n```\n{raw_text}\n```\n\n" - "Zusammenfassung (max. 100 Wörter):" ) + "Zusammenfassung (max. 100 Wörter):" + ) summary = call_openai_chat(prompt, temperature=0.2) return summary if summary else "k.A." -def evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kategorien, website_summary): # Unverändert - """ Ordnet Unternehmen exakt einer Branche aus dem Ziel-Schema zu via ChatGPT. """ + +def evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kategorien, website_summary): + """ + Ordnet das Unternehmen basierend auf den angegebenen Informationen exakt einer Branche + aus dem Ziel-Branchenschema (nur Kurzformen) zu. Validiert den ChatGPT-Vorschlag + strikt gegen die erlaubten Kurzformen und führt einen Fallback auf die (extrahierte) + CRM-Kurzform durch, falls der Vorschlag ungültig ist. + + Args: + crm_branche (str): Branche laut CRM (kann noch Präfix enthalten). + beschreibung (str): Unternehmensbeschreibung (CRM). + wiki_branche (str): Branche aus Wikipedia (falls vorhanden). + wiki_kategorien (str): Wikipedia-Kategorien. + website_summary (str): Zusammenfassung des Website-Inhalts. + + Returns: + dict: Enthält "branch" (die finale, gültige Kurzform oder Fehler), + "consistency" ('ok', 'X', 'fallback_crm_valid', 'fallback_invalid') und + "justification" (Begründung von ChatGPT oder Fallback-Info). + """ + # Globale Variablen für Schema und erlaubte Branches verwenden global ALLOWED_TARGET_BRANCHES, TARGET_SCHEMA_STRING + + # Grundlegende Prüfung: Ist das Schema überhaupt geladen? if not ALLOWED_TARGET_BRANCHES: - debug_print("FEHLER in evaluate_branche_chatgpt: Ziel-Schema leer."); return {"branch": crm_branche, "consistency": "error_schema_missing", "justification": "Fehler: Ziel-Schema nicht geladen"} + debug_print("FEHLER in evaluate_branche_chatgpt: Ziel-Branchenschema (ALLOWED_TARGET_BRANCHES) ist leer. Abbruch.") + # Gib den CRM-Wert zurück, aber markiere als Fehler + return {"branch": crm_branche, "consistency": "error_schema_missing", "justification": "Fehler: Ziel-Schema nicht geladen"} + + # Erstelle ein Set/Dict der erlaubten Branches in Kleinbuchstaben für effizientes Nachschlagen + # Speichert die Originalschreibweise als Wert. allowed_branches_lookup = {b.lower(): b for b in ALLOWED_TARGET_BRANCHES} - prompt_parts = [TARGET_SCHEMA_STRING, "\nOrdne das Unternehmen anhand folgender Angaben exakt einer Branche des Ziel-Branchenschemas (Kurzformen) zu:"] + + # --- Prompt für ChatGPT erstellen --- + # Beginne mit den Regeln und der Liste der gültigen Kurzformen + prompt_parts = [TARGET_SCHEMA_STRING] # TARGET_SCHEMA_STRING sollte bereits die klare Anweisung enthalten + prompt_parts.append("\nOrdne das Unternehmen anhand folgender Angaben exakt einer Branche des Ziel-Branchenschemas (Kurzformen) zu:") + + # Füge nur vorhandene Informationen hinzu und kürze sie ggf. if crm_branche and crm_branche != "k.A.": prompt_parts.append(f"- CRM-Branche (Referenz): {crm_branche}") - if beschreibung and beschreibung != "k.A.": prompt_parts.append(f"- Beschreibung: {beschreibung[:500]}") + if beschreibung and beschreibung != "k.A.": prompt_parts.append(f"- Beschreibung: {beschreibung[:500]}") # Kürzen if wiki_branche and wiki_branche != "k.A.": prompt_parts.append(f"- Wikipedia-Branche: {wiki_branche}") - if wiki_kategorien and wiki_kategorien != "k.A.": prompt_parts.append(f"- Wikipedia-Kategorien: {wiki_kategorien[:500]}") - if website_summary and website_summary != "k.A.": prompt_parts.append(f"- Website-Zusammenfassung: {website_summary[:500]}") + if wiki_kategorien and wiki_kategorien != "k.A.": prompt_parts.append(f"- Wikipedia-Kategorien: {wiki_kategorien[:500]}") # Kürzen + if website_summary and website_summary != "k.A.": prompt_parts.append(f"- Website-Zusammenfassung: {website_summary[:500]}") # Kürzen + + # Fallback, wenn gar keine spezifischen Infos da sind if len(prompt_parts) <= 2: - debug_print("Warnung in evaluate_branche_chatgpt: Zu wenige Infos."); return {"branch": crm_branche, "consistency": "error_no_info", "justification": "Fehler: Zu wenige Informationen"} - prompt_parts.append("\nWICHTIG: Antworte NUR mit dem exakten Kurznamen einer Branche aus der obigen Liste. Verwende KEINE Präfixe.") - prompt_parts.append("\nAntworte ausschließlich im folgenden Format:") - prompt_parts.append("Branche: "); prompt_parts.append("Übereinstimmung: "); prompt_parts.append("Begründung: ") + debug_print("Warnung in evaluate_branche_chatgpt: Zu wenige Informationen für Branchenevaluierung.") + return {"branch": crm_branche, "consistency": "error_no_info", "justification": "Fehler: Zu wenige Informationen für eine Einschätzung"} + + # Füge die strengen Anweisungen für das Antwortformat hinzu + prompt_parts.append("\nWICHTIG: Antworte NUR mit dem exakten Kurznamen einer Branche aus der obigen Liste. Verwende KEINE Präfixe wie 'Hersteller / Produzenten >' oder 'Service provider (Dienstleister) >'.") + prompt_parts.append("\nAntworte ausschließlich im folgenden Format (keine Einleitung, kein Schlusssatz):") + prompt_parts.append("Branche: ") + prompt_parts.append("Übereinstimmung: ") + prompt_parts.append("Begründung: ") + prompt = "\n".join(prompt_parts) - chat_response = call_openai_chat(prompt, temperature=0.0) + + # --- ChatGPT aufrufen --- + chat_response = call_openai_chat(prompt, temperature=0.0) # Niedrige Temperatur für konsistente Zuordnung + if not chat_response: - debug_print("Fehler in evaluate_branche_chatgpt: Keine API Antwort."); return {"branch": crm_branche, "consistency": "error_api_no_response", "justification": "Fehler: Keine Antwort von API"} - lines = chat_response.strip().split("\n"); result = {"branch": None, "consistency": None, "justification": ""}; suggested_branch = "" + debug_print("Fehler in evaluate_branche_chatgpt: Keine Antwort von OpenAI erhalten.") + return {"branch": crm_branche, "consistency": "error_api_no_response", "justification": "Fehler: Keine Antwort von API"} + + # --- Antwort parsen --- + lines = chat_response.strip().split("\n") + result = {"branch": None, "consistency": None, "justification": ""} # Initialisiere mit None + suggested_branch = "" for line in lines: line_lower = line.lower() - if line_lower.startswith("branche:"): suggested_branch = line.split(":", 1)[1].strip().strip('"\'') - elif line_lower.startswith("begründung:"): result["justification"] = line.split(":", 1)[1].strip() + if line_lower.startswith("branche:"): + suggested_branch = line.split(":", 1)[1].strip() + # Entferne mögliche Anführungszeichen + suggested_branch = suggested_branch.strip('"\'') + elif line_lower.startswith("übereinstimmung:"): + # Wir überschreiben die Konsistenz später basierend auf unserer Logik + pass + elif line_lower.startswith("begründung:"): + result["justification"] = line.split(":", 1)[1].strip() + if not suggested_branch: - debug_print(f"Fehler in evaluate_branche_chatgpt: Parsing fehlgeschlagen: {chat_response}"); return {"branch": crm_branche, "consistency": "error_parsing", "justification": f"Fehler: Parsing API Antwort. Antwort: {chat_response}"} - final_branch = None; suggested_branch_lower = suggested_branch.lower() + debug_print(f"Fehler in evaluate_branche_chatgpt: Konnte 'Branche:' nicht aus Antwort parsen: {chat_response}") + # Optional: Versuche Begründung als Branche zu nehmen? Eher nicht. + return {"branch": crm_branche, "consistency": "error_parsing", "justification": f"Fehler: Parsing der API Antwort fehlgeschlagen. Antwort: {chat_response}"} + + # --- Validierung des ChatGPT-Vorschlags --- + final_branch = None + suggested_branch_lower = suggested_branch.lower() + if suggested_branch_lower in allowed_branches_lookup: - final_branch = allowed_branches_lookup[suggested_branch_lower]; result["consistency"] = "pending_comparison" - # debug_print(f"ChatGPT-Vorschlag '{suggested_branch}' ist gültig ('{final_branch}').") + final_branch = allowed_branches_lookup[suggested_branch_lower] # Nimm korrekte Schreibweise + debug_print(f"ChatGPT-Branchenvorschlag '{suggested_branch}' ist gültig ('{final_branch}').") + # Konsistenz wird später gesetzt + result["consistency"] = "pending_comparison" # Temporärer Status else: - debug_print(f"ChatGPT-Vorschlag '{suggested_branch}' ist NICHT im Ziel-Schema. Starte Fallback...") + # --- Fallback-Logik --- + debug_print(f"ChatGPT-Branchenvorschlag '{suggested_branch}' ist NICHT im Ziel-Schema ({len(ALLOWED_TARGET_BRANCHES)} Einträge) enthalten. Starte Fallback...") + + # Versuche Kurzform aus CRM-Branche zu extrahieren crm_short_branch = "k.A." - if crm_branche and ">" in crm_branche: crm_short_branch = crm_branche.split(">", 1)[1].strip() - elif crm_branche and crm_branche != "k.A.": crm_short_branch = crm_branche.strip() + if crm_branche and ">" in crm_branche: + crm_short_branch = crm_branche.split(">", 1)[1].strip() + elif crm_branche and crm_branche != "k.A.": # Wenn CRM schon Kurzform sein könnte + crm_short_branch = crm_branche.strip() + + # Prüfe, ob die extrahierte CRM-Kurzform gültig ist if crm_short_branch != "k.A." and crm_short_branch.lower() in allowed_branches_lookup: - final_branch = allowed_branches_lookup[crm_short_branch.lower()] - result["consistency"] = "fallback_crm_valid" + final_branch = allowed_branches_lookup[crm_short_branch.lower()] # Nimm korrekte Schreibweise + result["consistency"] = "fallback_crm_valid" # Setze Fallback-Status + # Kombiniere ChatGPT Begründung (falls vorhanden) mit Fallback-Info fallback_reason = f"Fallback: Ungültiger ChatGPT-Vorschlag ('{suggested_branch}'). Gültige CRM-Kurzform '{final_branch}' verwendet." result["justification"] = f"{fallback_reason} (ChatGPT Begründung war: {result.get('justification', 'Keine')})" debug_print(f"Fallback auf gültige CRM-Kurzform erfolgreich: '{final_branch}'") else: + # Wenn auch CRM-Kurzform ungültig oder nicht extrahierbar final_branch = suggested_branch # Behalte ungültigen Vorschlag - result["consistency"] = "fallback_invalid" + result["consistency"] = "fallback_invalid" # Setze Fehler-Fallback-Status error_reason = f"Fehler: Ungültiger ChatGPT-Vorschlag ('{suggested_branch}') und keine gültige CRM-Kurzform ('{crm_short_branch}') als Fallback verfügbar." result["justification"] = f"{error_reason} (ChatGPT Begründung war: {result.get('justification', 'Keine')})" debug_print(f"Fallback fehlgeschlagen. Ungültiger Vorschlag: '{final_branch}', Ungültige CRM-Kurzform: '{crm_short_branch}'") + # Alternativ: Gib einen speziellen Fehlerwert zurück + # final_branch = "FEHLER - UNGÜLTIGE ZUWEISUNG" + + # Setze den finalen Branch im Ergebnis-Dictionary result["branch"] = final_branch if final_branch else "FEHLER" + + # --- Konsistenzprüfung (Finale Bewertung) --- + # Extrahiere CRM-Kurzform für den Vergleich (erneut oder Variable von oben) crm_short_to_compare = "k.A." - if crm_branche and ">" in crm_branche: crm_short_to_compare = crm_branche.split(">", 1)[1].strip() - elif crm_branche and crm_branche != "k.A.": crm_short_to_compare = crm_branche.strip() + if crm_branche and ">" in crm_branche: + crm_short_to_compare = crm_branche.split(">", 1)[1].strip() + elif crm_branche and crm_branche != "k.A.": + crm_short_to_compare = crm_branche.strip() + + # Vergleiche finalen Branch (falls nicht FEHLER) mit CRM-Kurzform (case-insensitive) if result["branch"] != "FEHLER" and result["branch"].lower() == crm_short_to_compare.lower(): - if result["consistency"] == "pending_comparison": result["consistency"] = "ok" - elif result["consistency"] == "pending_comparison": result["consistency"] = "X" - if result["consistency"] == "pending_comparison": result["consistency"] = "error_comparison_failed" - # debug_print(f"Finale Branch-Evaluation: {result}") + # Wenn sie übereinstimmen UND *kein* Fallback stattgefunden hat, ist es 'ok'. + if result["consistency"] == "pending_comparison": + result["consistency"] = "ok" + # Wenn Fallback auf gültige CRM stattfand (Status 'fallback_crm_valid'), bleibt dieser Status. + elif result["consistency"] == "pending_comparison": + # Wenn sie nicht übereinstimmen und kein Fallback stattfand, ist es 'X'. + result["consistency"] = "X" + # Wenn der Status bereits 'fallback_crm_valid' oder 'fallback_invalid' ist, bleibt er unverändert. + elif result["consistency"] is None: # Sollte nicht passieren, aber zur Sicherheit + result["consistency"] = "error_unknown_state" + + + # Entferne den temporären Status, falls er noch da ist + if result["consistency"] == "pending_comparison": + result["consistency"] = "error_comparison_failed" + + # Debug-Ausgabe des finalen Ergebnisses vor Rückgabe + debug_print(f"Finale Branch-Evaluation: {result}") + return result -# --- Platzhalter für weitere, aktuell nicht genutzte oder unveränderte ChatGPT-Funktionen --- -def evaluate_fsm_suitability(company_name, company_data): return {"suitability": "k.A.", "justification": "Not Implemented"} -def evaluate_servicetechnicians_estimate(company_name, company_data): return "k.A. (Not Implemented)" -def map_internal_technicians(value): return "k.A. (Not Implemented)" -def evaluate_servicetechnicians_explanation(company_name, st_estimate, company_data): return "k.A. (Not Implemented)" -def process_employee_estimation(company_name, wiki_paragraph, crm_employee): return "k.A. (Not Implemented)" -def process_employee_consistency(crm_employee, wiki_employee, emp_estimate): return "k.A. (Not Implemented)" -def evaluate_umsatz_chatgpt(company_name, wiki_umsatz): return "k.A. (Not Implemented)" +# TODO: Weitere ChatGPT-Funktionen (evaluate_fsm_suitability, etc.) analog überarbeiten: +# - Prompts verbessern (klarere Anweisungen, Kontext nur bei Bedarf) +# - call_openai_chat verwenden +# - Parsing der Antworten robuster machen + +def process_wiki_verification(crm_data, wiki_data_str): + # Platzhalter - Implementierung anpassen oder entfernen, falls durch _process_batch abgedeckt + debug_print(f"TODO: process_wiki_verification aufrufen/implementieren für {crm_data}") + return "k.A. (Not Implemented)" + +def evaluate_fsm_suitability(company_name, company_data): + # Platzhalter - Implementierung anpassen + debug_print(f"TODO: evaluate_fsm_suitability aufrufen/implementieren für {company_name}") + return {"suitability": "k.A.", "justification": "Not Implemented"} + +def evaluate_servicetechnicians_estimate(company_name, company_data): + # Platzhalter - Implementierung anpassen + debug_print(f"TODO: evaluate_servicetechnicians_estimate aufrufen/implementieren für {company_name}") + return "k.A. (Not Implemented)" + +def map_internal_technicians(value): + # Platzhalter - Implementierung anpassen + debug_print(f"TODO: map_internal_technicians aufrufen/implementieren für {value}") + return "k.A. (Not Implemented)" + +def evaluate_servicetechnicians_explanation(company_name, st_estimate, company_data): + # Platzhalter - Implementierung anpassen + debug_print(f"TODO: evaluate_servicetechnicians_explanation aufrufen/implementieren für {company_name}") + return "k.A. (Not Implemented)" + +def process_employee_estimation(company_name, wiki_paragraph, crm_employee): + # Platzhalter - Implementierung anpassen + debug_print(f"TODO: process_employee_estimation aufrufen/implementieren für {company_name}") + return "k.A. (Not Implemented)" + +def process_employee_consistency(crm_employee, wiki_employee, emp_estimate): + # Platzhalter - Implementierung anpassen + debug_print(f"TODO: process_employee_consistency aufrufen/implementieren für {crm_employee} vs {wiki_employee} vs {emp_estimate}") + return "k.A. (Not Implemented)" + +def evaluate_umsatz_chatgpt(company_name, wiki_umsatz): + # Platzhalter - Implementierung anpassen + debug_print(f"TODO: evaluate_umsatz_chatgpt aufrufen/implementieren für {company_name}") + return "k.A. (Not Implemented)" + # ==================== BATCH PROCESSING FUNCTIONS ==================== -def _process_batch(sheet, batches, row_numbers): # Unverändert +def _process_batch(sheet, batches, row_numbers): """ Hilfsfunktion für process_verification_only: Verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen. - Aktualisiert NUR die Spalten S bis Y. Zeitstempel werden von der aufrufenden Funktion gesetzt. + Aktualisiert NUR die Spalten S bis Y. Zeitstempel (AN, AO, AP) werden von + der aufrufenden Funktion oder anderen Prozessen gesetzt. + + Args: + sheet (gspread.Worksheet): Das Worksheet-Objekt zum Schreiben. + batches (list): Liste der Prompt-Teile für den Batch. + row_numbers (list): Liste der zugehörigen Zeilennummern. """ - if not batches: return + if not batches: + return + + # --- Prompt Erstellung (wie gehabt) --- 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 zum Firmennamen und zur Beschreibung passt. " @@ -1000,28 +2380,266 @@ def _process_batch(sheet, batches, row_numbers): # Unverändert "- 'X | Kein passender Artikel gefunden | Begründung: ' (wenn der Artikel nicht passt und kein besserer gefunden wurde)\n" "- 'Kein Wikipedia-Eintrag vorhanden.' (wenn initial keine URL angegeben wurde und keine Suche erfolgreich war)\n\n" "Einträge:\n" - "----------\n" ) - aggregated_prompt += "".join(batches) + "----------\n" + ) + aggregated_prompt += "".join(batches) # Join ohne zusätzliches \n aggregated_prompt += "----------\nBitte nur die 'Eintrag X: Antwort'-Zeilen ausgeben." - # debug_print(f"Verarbeite Verifizierungs-Batch für Zeilen {row_numbers[0]} bis {row_numbers[-1]}.") + + debug_print(f"Verarbeite Verifizierungs-Batch für Zeilen {row_numbers[0]} bis {row_numbers[-1]} (nur S-Y)...") # Hinweis angepasst + + # Token Count für den Prompt prompt_tokens = token_count(aggregated_prompt) - # debug_print(f"Token-Zahl für Verifizierungs-Batch: {prompt_tokens}") + debug_print(f"Token-Zahl für Verifizierungs-Batch: {prompt_tokens}") + + # --- ChatGPT Aufruf (wie gehabt) --- chat_response = call_openai_chat(aggregated_prompt, temperature=0.0) - if not chat_response: debug_print(f"Fehler: Keine Antwort OpenAI für Verif.-Batch {row_numbers[0]}-{row_numbers[-1]}."); return - answers = {}; lines = chat_response.strip().split('\n') + + if not chat_response: + debug_print(f"Fehler: Keine Antwort von OpenAI für Verifizierungs-Batch {row_numbers[0]}-{row_numbers[-1]}.") + return + + # --- Antwort parsen (wie gehabt) --- + answers = {} + lines = chat_response.strip().split('\n') for line in lines: match = re.match(r"Eintrag (\d+): (.*)", line.strip()) if match: - row_num = int(match.group(1)); answer_text = match.group(2).strip() - if row_num in row_numbers: answers[row_num] = answer_text + row_num = int(match.group(1)) + answer_text = match.group(2).strip() + if row_num in row_numbers: + answers[row_num] = answer_text + # else: # Weniger Lärm im Log + # debug_print(f"Warnung: Antwort für unerwartete Zeilennummer {row_num} im Batch erhalten: {answer_text}") + + # --- Batch-Update vorbereiten (NUR S bis Y) --- updates = [] + # Timestamps und Version werden HIER NICHT mehr hinzugefügt + + for row_num in row_numbers: + answer = answers.get(row_num, "k.A. (Keine Antwort im Batch)") # Fallback + + # Variablen für die Spalten S-Y initialisieren + wiki_confirm = "" # Spalte S + alt_article = "" # Spalte T + wiki_explanation = "" # Spalte U + v_val, w_val, x_val, y_val = "", "", "", "" # Spalten V-Y + + # Logik zur Bestimmung der Werte für S, T, U basierend auf 'answer' (wie gehabt) + if answer.upper() == "OK": + wiki_confirm = "OK" + elif answer.upper() == "KEIN WIKIPEDIA-EINTRAG VORHANDEN.": + wiki_confirm = "X" + alt_article = "Kein Wikipedia-Eintrag vorhanden." + wiki_explanation = "Ursprünglich keine URL oder Suche erfolglos." + elif answer.startswith("X |"): + parts = answer.split("|", 2) + wiki_confirm = "X" + if len(parts) > 1: + detail = parts[1].strip() + if detail.startswith("Alternativer Artikel:"): + alt_article = detail.split(":", 1)[1].strip() + elif detail == "Kein passender Artikel gefunden": + alt_article = detail + else: + alt_article = detail + if len(parts) > 2: + reason_part = parts[2].strip() + if reason_part.startswith("Begründung:"): + wiki_explanation = reason_part.split(":", 1)[1].strip() + else: + wiki_explanation = reason_part + else: # Unerwartetes Format + wiki_confirm = "?" + wiki_explanation = f"Unerwartetes Format: {answer}" + + # Füge Updates NUR für S-Y zur Liste hinzu + updates.append({'range': f'S{row_num}', 'values': [[wiki_confirm]]}) + updates.append({'range': f'T{row_num}', 'values': [[alt_article]]}) + updates.append({'range': f'U{row_num}', 'values': [[wiki_explanation]]}) + updates.append({'range': f'V{row_num}:Y{row_num}', 'values': [[v_val, w_val, x_val, y_val]]}) + + # --- Führe das Batch-Update für S-Y durch --- + if updates: + try: + # Verwende das übergebene sheet-Objekt direkt + sheet.batch_update(updates, value_input_option='USER_ENTERED') + debug_print(f"Verifizierungs-Batch {row_numbers[0]}-{row_numbers[-1]} (S-Y) erfolgreich in Google Sheet aktualisiert.") + except Exception as e: + # Gib eine spezifischere Fehlermeldung aus + debug_print(f"FEHLER beim Batch-Update (S-Y) für Zeilen {row_numbers[0]}-{row_numbers[-1]}: {type(e).__name__} - {e}") + # Optional: Fehler weitergeben, wenn retry gewünscht wird (nicht empfohlen für Sheet-Updates hier) + # raise e + else: + debug_print(f"Keine Updates (S-Y) für Verifizierungs-Batch {row_numbers[0]}-{row_numbers[-1]} generiert.") + + # KEINE Pause hier mehr, wird in der aufrufenden Funktion gemacht + # time.sleep(Config.RETRY_DELAY) + + +def process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet): + """ + Batch-Prozess nur für Wikipedia-Verifizierung (Spalten S-Y). + Lädt Daten neu, prüft für jede Zeile im Bereich, ob Timestamp AX (Wiki Verif.) + bereits gesetzt ist, und überspringt diese ggf. Setzt AX für bearbeitete Zeilen. + AN wird hier *nicht* mehr gesetzt, das muss ggf. _process_single_row tun. + """ + debug_print(f"Starte Wikipedia-Verifizierungsmodus (Batch) für Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}...") + + if not sheet_handler.load_data(): + debug_print("FEHLER beim Laden der Daten in process_verification_only.") + return + all_data = sheet_handler.get_all_data_with_headers() + if not all_data or len(all_data) <= 5: + debug_print("FEHLER/WARNUNG: Keine Daten zum Verarbeiten in process_verification_only gefunden.") + return + + # Hole Index für AX Timestamp (Wiki Verif.) + timestamp_col_key = "Wiki Verif. Timestamp" # NEUER SCHLÜSSEL + timestamp_col_index = COLUMN_MAP.get(timestamp_col_key) + ts_col_letter = sheet_handler._get_col_letter(timestamp_col_index + 1) if timestamp_col_index is not None else "AX_FEHLER" + + if timestamp_col_index is None: + debug_print(f"FEHLER: Spaltenschlüssel '{timestamp_col_key}' nicht in COLUMN_MAP gefunden. Breche Wiki-Verifizierung ab.") + return + + batch_size = Config.BATCH_SIZE + current_batch = [] + current_row_numbers = [] + processed_count = 0 + skipped_count = 0 + + for i in range(start_row_index_in_sheet, end_row_index_in_sheet + 1): + row_index_in_list = i - 1 + if row_index_in_list >= len(all_data): continue + row = all_data[row_index_in_list] + + # --- Timestamp-Prüfung für jede Zeile (AX) --- + ts_value_ax = "INDEX_FEHLER" + ts_ax_is_set = False + if len(row) > timestamp_col_index: + ts_value_ax = row[timestamp_col_index] + ts_ax_is_set = bool(str(ts_value_ax).strip()) + + log_debug = (i < start_row_index_in_sheet + 5 or i > end_row_index_in_sheet - 5 or i % 500 == 0 or i in range(2122, 2132)) + if log_debug: + debug_print(f"Zeile {i} (Wiki Verif. Check): Prüfe Timestamp {ts_col_letter} (Index {timestamp_col_index}). Rohwert='{ts_value_ax}', Strip='{str(ts_value_ax).strip()}', Überspringen? -> {ts_ax_is_set}") + + if ts_ax_is_set: + skipped_count += 1 + continue + # --- Ende Timestamp-Prüfung --- + + # Nur wenn AX leer ist, wird die Zeile für den Batch vorbereitet + company_name = row[COLUMN_MAP.get("CRM Name", 1)] if len(row) > COLUMN_MAP.get("CRM Name", 1) else '' + crm_desc = row[COLUMN_MAP.get("CRM Beschreibung", 5)] if len(row) > COLUMN_MAP.get("CRM Beschreibung", 5) else '' + wiki_url_idx = COLUMN_MAP.get("Wiki URL") + wiki_url = row[wiki_url_idx] if wiki_url_idx is not None and len(row) > wiki_url_idx and row[wiki_url_idx].strip() not in ['', 'k.A.'] else 'k.A.' + wiki_para_idx = COLUMN_MAP.get("Wiki Absatz") + wiki_paragraph = row[wiki_para_idx] if wiki_para_idx is not None and len(row) > wiki_para_idx else 'k.A.' + wiki_cat_idx = COLUMN_MAP.get("Wiki Kategorien") + wiki_categories = row[wiki_cat_idx] if wiki_cat_idx is not None and len(row) > wiki_cat_idx else 'k.A.' + + entry_text = ( + f"Eintrag {i}:\n" + f" Firmenname: {company_name}\n" + f" CRM-Beschreibung: {crm_desc[:200]}...\n" + f" Wikipedia-URL: {wiki_url}\n" + f" Wiki-Absatz: {wiki_paragraph[:200]}...\n" + f" Wiki-Kategorien: {wiki_categories[:200]}...\n" + f"----\n" + ) + current_batch.append(entry_text) + current_row_numbers.append(i) + processed_count += 1 + + if len(current_batch) >= batch_size or i == end_row_index_in_sheet: + if current_batch: + # Rufe _process_batch auf (schreibt S-Y) + _process_batch(sheet_handler.sheet, current_batch, current_row_numbers) + + # Setze den AX Timestamp für die bearbeiteten Zeilen + wiki_ts_updates = [] + current_wiki_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + for row_num in current_row_numbers: + wiki_ts_updates.append({'range': f'{ts_col_letter}{row_num}', 'values': [[current_wiki_timestamp]]}) + + if wiki_ts_updates: + success_ts = sheet_handler.batch_update_cells(wiki_ts_updates) + if success_ts: + debug_print(f"Wiki Verif. Timestamp {ts_col_letter} für Batch {current_row_numbers[0]}-{current_row_numbers[-1]} gesetzt.") + else: + debug_print(f"FEHLER beim Setzen des Wiki Verif. Timestamps {ts_col_letter} für Batch.") + + time.sleep(Config.RETRY_DELAY) + + current_batch = [] + current_row_numbers = [] + + debug_print(f"Wikipedia-Verifizierungs-Batch abgeschlossen. {processed_count} Zeilen zur Verarbeitung weitergegeben, {skipped_count} Zeilen übersprungen.") + + +# Anpassung in _process_batch: Setzt jetzt *nicht* mehr AO/AP, sondern nur S-Y +def _process_batch(sheet, batches, row_numbers): + """ + Hilfsfunktion für process_verification_only: Verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen. + Aktualisiert NUR die Spalten S bis Y. Zeitstempel werden von der aufrufenden Funktion gesetzt. + """ + if not batches: return + # (Prompt Erstellung wie gehabt) + 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 zum Firmennamen und zur Beschreibung passt. " + "Gib das Ergebnis für jeden Eintrag ausschließlich im folgenden Format auf einer neuen Zeile aus:\n" + "Eintrag : \n\n" + "Mögliche Antworten:\n" + "- 'OK' (wenn der Artikel gut passt)\n" + "- 'X | Alternativer Artikel: | Begründung: ' (wenn der Artikel nicht passt, aber ein besserer gefunden wurde)\n" + "- 'X | Kein passender Artikel gefunden | Begründung: ' (wenn der Artikel nicht passt und kein besserer gefunden wurde)\n" + "- 'Kein Wikipedia-Eintrag vorhanden.' (wenn initial keine URL angegeben wurde und keine Suche erfolgreich war - dieser Fall sollte selten sein, da die Suche vorher stattfindet)\n\n" + "Einträge:\n" + "----------\n" + ) + aggregated_prompt += "".join(batches) + aggregated_prompt += "----------\nBitte nur die 'Eintrag X: Antwort'-Zeilen ausgeben." + + debug_print(f"Verarbeite Verifizierungs-Batch für Zeilen {row_numbers[0]} bis {row_numbers[-1]}.") + prompt_tokens = token_count(aggregated_prompt) + debug_print(f"Token-Zahl für Verifizierungs-Batch: {prompt_tokens}") + + chat_response = call_openai_chat(aggregated_prompt, temperature=0.0) + + if not chat_response: + debug_print(f"Fehler: Keine Antwort von OpenAI für Verifizierungs-Batch {row_numbers[0]}-{row_numbers[-1]}.") + return + + # Parse die aggregierte Antwort (wie gehabt) + answers = {} + lines = chat_response.strip().split('\n') + for line in lines: + match = re.match(r"Eintrag (\d+): (.*)", line.strip()) + if match: + row_num = int(match.group(1)) + answer_text = match.group(2).strip() + if row_num in row_numbers: answers[row_num] = answer_text + + # Bereite Batch-Update nur für Spalten S-Y vor + updates = [] + # current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Nicht mehr hier + # current_version = Config.VERSION # Nicht mehr hier + for row_num in row_numbers: answer = answers.get(row_num, "k.A. (Keine Antwort im Batch)") - wiki_confirm, alt_article, wiki_explanation = "", "", ""; v_val, w_val, x_val, y_val = "", "", "", "" + # debug_print(f"Zeile {row_num} Verifizierungsantwort: '{answer}'") # Optional weniger Lärm + + wiki_confirm, alt_article, wiki_explanation = "", "", "" + v_val, w_val, x_val, y_val = "", "", "", "" + if answer.upper() == "OK": wiki_confirm = "OK" - elif answer.upper() == "KEIN WIKIPEDIA-EINTRAG VORHANDEN.": wiki_confirm, alt_article, wiki_explanation = "X", "Kein Wikipedia-Eintrag vorhanden.", "Ursprünglich keine URL oder Suche erfolglos." + elif answer.upper() == "KEIN WIKIPEDIA-EINTRAG VORHANDEN.": + wiki_confirm, alt_article, wiki_explanation = "X", "Kein Wikipedia-Eintrag vorhanden.", "Ursprünglich keine URL oder Suche erfolglos." elif answer.startswith("X |"): - parts = answer.split("|", 2); wiki_confirm = "X" + parts = answer.split("|", 2) + wiki_confirm = "X" if len(parts) > 1: detail = parts[1].strip() if detail.startswith("Alternativer Artikel:"): alt_article = detail.split(":", 1)[1].strip() @@ -1031,548 +2649,846 @@ def _process_batch(sheet, batches, row_numbers): # Unverändert reason_part = parts[2].strip() if reason_part.startswith("Begründung:"): wiki_explanation = reason_part.split(":", 1)[1].strip() else: wiki_explanation = reason_part - else: wiki_confirm, wiki_explanation = "?", f"Unerwartetes Format: {answer}" - # Nutze COLUMN_MAP indirekt via sheet_handler._get_col_letter (besser wäre direkte Nutzung hier) - # Annahme: S=18, T=19, U=20, V=21, W=22, X=23, Y=24 (0-basiert) - s_l = GoogleSheetHandler()._get_col_letter(19); t_l = GoogleSheetHandler()._get_col_letter(20); u_l = GoogleSheetHandler()._get_col_letter(21) - v_l = GoogleSheetHandler()._get_col_letter(22); y_l = GoogleSheetHandler()._get_col_letter(25) # V bis Y - updates.append({'range': f'{s_l}{row_num}', 'values': [[wiki_confirm]]}) - updates.append({'range': f'{t_l}{row_num}', 'values': [[alt_article]]}) - updates.append({'range': f'{u_l}{row_num}', 'values': [[wiki_explanation]]}) - updates.append({'range': f'{v_l}{row_num}:{y_l}{row_num}', 'values': [[v_val, w_val, x_val, y_val]]}) + else: + wiki_confirm, wiki_explanation = "?", f"Unerwartetes Format: {answer}" + + # Füge Updates für S-Y hinzu + updates.append({'range': f'S{row_num}', 'values': [[wiki_confirm]]}) + updates.append({'range': f'T{row_num}', 'values': [[alt_article]]}) + updates.append({'range': f'U{row_num}', 'values': [[wiki_explanation]]}) + updates.append({'range': f'V{row_num}:Y{row_num}', 'values': [[v_val, w_val, x_val, y_val]]}) + + # Führe das Batch-Update für S-Y durch if updates: - try: sheet.batch_update(updates, value_input_option='USER_ENTERED'); debug_print(f"Verif.-Batch {row_numbers[0]}-{row_numbers[-1]} (S-Y) OK.") - except Exception as e: debug_print(f"FEHLER Batch-Update (S-Y) für Batch {row_numbers[0]}-{row_numbers[-1]}: {e}") + # Direkten Sheet-Zugriff nutzen, da sheet übergeben wird + try: + sheet.batch_update(updates) + debug_print(f"Verifizierungs-Batch {row_numbers[0]}-{row_numbers[-1]} (S-Y) erfolgreich in Google Sheet aktualisiert.") + except Exception as e: + debug_print(f"FEHLER beim Batch-Update (S-Y) für Batch {row_numbers[0]}-{row_numbers[-1]}: {e}") + else: + debug_print(f"Keine Updates (S-Y) für Verifizierungs-Batch {row_numbers[0]}-{row_numbers[-1]} generiert.") -def process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet): # Nutzt Config.HEADER_ROWS - """ Batch-Prozess nur für Wikipedia-Verifizierung (Spalten S-Y). Prüft AX. """ - debug_print(f"Starte Wiki-Verif.-Modus (Batch) {start_row_index_in_sheet}-{end_row_index_in_sheet}...") - if not sheet_handler.load_data(): debug_print("FEHLER Laden process_verification_only."); return - all_data = sheet_handler.get_all_data_with_headers() - if not all_data or len(all_data) <= Config.HEADER_ROWS: debug_print("FEHLER/WARNUNG: Keine Daten process_verification_only."); return - timestamp_col_key = "Wiki Verif. Timestamp"; timestamp_col_index = COLUMN_MAP.get(timestamp_col_key) - ts_col_letter = sheet_handler._get_col_letter(timestamp_col_index + 1) if timestamp_col_index is not None else "AX_FEHLER" - if timestamp_col_index is None: debug_print(f"FEHLER: '{timestamp_col_key}' nicht in COLUMN_MAP."); return + # Kurze Pause nach jedem Batch-API-Call (jetzt in der aufrufenden Funktion) + # time.sleep(Config.RETRY_DELAY) # Entfernt - batch_size = Config.BATCH_SIZE; current_batch = []; current_row_numbers = []; processed_count = 0; skipped_count = 0 - for i in range(start_row_index_in_sheet, end_row_index_in_sheet + 1): - row_index_in_list = i - 1 # 0-basierter Index in all_data - if row_index_in_list >= len(all_data): continue - row = all_data[row_index_in_list] +# Komplette Funktion process_website_batch (prüft jetzt Timestamp AT mit erzwungenem Debugging) +# Komplette Funktion process_website_batch (MIT Batched Google Sheet Updates) +# Komplette Funktion process_website_batch (NEUE STRUKTUR - ECHTER BATCH WORKFLOW) +# Komplette Funktion process_website_batch (NUR SCRAPING) +# Komplette Funktion process_website_batch (Korrigierte Config-Referenzen) +def process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet): + """ + Batch-Prozess NUR für Website-Scraping (Rohtext AR). + Lädt Daten neu, prüft Spalte AR auf Inhalt ('', 'k.A.', etc.) und überspringt Zeilen mit Inhalt. + Setzt AR + AP für bearbeitete Zeilen. Sendet Updates gebündelt. + """ + debug_print(f"Starte Website-Scraping NUR ROHDATEN (Batch) für Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}...") - ts_value_ax = "INDEX_FEHLER"; ts_ax_is_set = False - if len(row) > timestamp_col_index: ts_value_ax = row[timestamp_col_index]; ts_ax_is_set = bool(str(ts_value_ax).strip()) - # log_debug = (i < start_row_index_in_sheet + 2 or i > end_row_index_in_sheet - 2 or i % 500 == 0) - # if log_debug: debug_print(f"Zeile {i} (Wiki Verif. Check): TS {ts_col_letter}='{ts_value_ax}'. Überspringen? {ts_ax_is_set}") - if ts_ax_is_set: skipped_count += 1; continue - - # Daten für Prompt holen (mit Indexprüfung) - name_idx = COLUMN_MAP.get("CRM Name"); desc_idx = COLUMN_MAP.get("CRM Beschreibung") - url_idx = COLUMN_MAP.get("Wiki URL"); para_idx = COLUMN_MAP.get("Wiki Absatz"); cat_idx = COLUMN_MAP.get("Wiki Kategorien") - company_name = row[name_idx] if name_idx is not None and len(row) > name_idx else '' - crm_desc = row[desc_idx] if desc_idx is not None and len(row) > desc_idx else '' - wiki_url = row[url_idx] if url_idx is not None and len(row) > url_idx and row[url_idx].strip() not in ['', 'k.A.'] else 'k.A.' - wiki_paragraph = row[para_idx] if para_idx is not None and len(row) > para_idx else 'k.A.' - wiki_categories = row[cat_idx] if cat_idx is not None and len(row) > cat_idx else 'k.A.' - - entry_text = (f"Eintrag {i}:\n" f" Firmenname: {company_name}\n" f" CRM-Beschreibung: {crm_desc[:200]}...\n" f" Wikipedia-URL: {wiki_url}\n" f" Wiki-Absatz: {wiki_paragraph[:200]}...\n" f" Wiki-Kategorien: {wiki_categories[:200]}...\n" f"----\n") - current_batch.append(entry_text); current_row_numbers.append(i); processed_count += 1 - - if len(current_batch) >= batch_size or i == end_row_index_in_sheet: - if current_batch: - _process_batch(sheet_handler.sheet, current_batch, current_row_numbers) # Schreibt S-Y - # Setze AX Timestamp für bearbeitete Zeilen - wiki_ts_updates = []; current_wiki_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - for row_num in current_row_numbers: wiki_ts_updates.append({'range': f'{ts_col_letter}{row_num}', 'values': [[current_wiki_timestamp]]}) - if wiki_ts_updates: - success_ts = sheet_handler.batch_update_cells(wiki_ts_updates) - if success_ts: debug_print(f"Wiki Verif. TS {ts_col_letter} für Batch {current_row_numbers[0]}-{current_row_numbers[-1]} gesetzt.") - else: debug_print(f"FEHLER Setzen Wiki Verif. TS {ts_col_letter}.") - time.sleep(Config.RETRY_DELAY) # Pause nach API Call & Update - current_batch = []; current_row_numbers = [] - debug_print(f"Wiki-Verif.-Batch beendet. {processed_count} verarbeitet, {skipped_count} übersprungen.") - - -@retry_on_failure -def summarize_batch_openai(tasks_data): # Unverändert - """ Fasst Liste von Rohtexten in einem OpenAI Call zusammen. """ - if not tasks_data: return {} - valid_tasks = [t for t in tasks_data if t.get("raw_text") and t["raw_text"] not in ["k.A.", "k.A. (Nur Cookie-Banner erkannt)", "k.A. (Fehler)"] and str(t.get("raw_text")).strip()] - if not valid_tasks: return {t['row_num']: "k.A. (Kein gültiger Rohtext)" for t in tasks_data} - # debug_print(f"Starte Batch-Zusammenfassung für {len(valid_tasks)} gültige Texte...") - prompt_parts = ["Du bist ein KI-Assistent...", "Fasse jeden TEXT prägnant zusammen...", "Antworte NUR mit Zeilen im Format:", "RESULTAT : ", "\n--- Texte ---"] - text_block = ""; row_numbers_in_batch = [] - for task in valid_tasks: - row_num = task['row_num']; raw_text = task['raw_text'][:1500] # Kürzung hier - entry_text = f"\n--- TEXT Zeile {row_num} ---\n{raw_text}\n--- ENDE TEXT Zeile {row_num} ---\n" - text_block += entry_text; row_numbers_in_batch.append(row_num) - if not row_numbers_in_batch: return {t['row_num']: "k.A. (Fehler)" for t in tasks_data} - prompt_parts.append(text_block); prompt_parts.append("--- Ende der Texte ---"); prompt_parts.append("Bitte gib NUR die 'RESULTAT : ...' Zeilen zurück.") - final_prompt = "\n".join(prompt_parts) - # prompt_tokens = token_count(final_prompt); debug_print(f"Geschätzte Prompt-Tokens: {prompt_tokens}") - chat_response = call_openai_chat(final_prompt, temperature=0.2) - summaries = {row_num: "k.A. (Keine Antwort geparst)" for row_num in row_numbers_in_batch} - if chat_response: - lines = chat_response.strip().split('\n'); parsed_count = 0 - for line in lines: - match = re.match(r"RESULTAT (\d+): (.*)", line.strip()) - if match: - row_num = int(match.group(1)); summary_text = match.group(2).strip() - if row_num in summaries: summaries[row_num] = summary_text; parsed_count += 1 - # debug_print(f"Batch-Zusammenfassung: {parsed_count}/{len(row_numbers_in_batch)} geparst.") - # else: debug_print("Fehler: Keine Antwort von OpenAI für Batch-Zusammenfassung.") - for task in tasks_data: # Füge Fallback für ursprünglich ungültige Tasks hinzu - if task['row_num'] not in summaries: summaries[task['row_num']] = "k.A. (Ungültiger Rohtext o.ä.)" - return summaries - -def process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet): # Nutzt Config.HEADER_ROWS - """ Batch-Prozess NUR für Website-Scraping (Rohtext AR). Prüft AR. """ - debug_print(f"Starte Website-Scraping ROHDATEN (Batch) {start_row_index_in_sheet}-{end_row_index_in_sheet}...") + # --- Lade Daten --- if not sheet_handler.load_data(): return all_data = sheet_handler.get_all_data_with_headers() - if not all_data or len(all_data) <= Config.HEADER_ROWS: return - header_rows = Config.HEADER_ROWS + if not all_data or len(all_data) <= 5: return + header_rows = 5 - rohtext_col_key = "Website Rohtext"; rohtext_col_index = COLUMN_MAP.get(rohtext_col_key) - website_col_idx = COLUMN_MAP.get("CRM Website"); version_col_idx = COLUMN_MAP.get("Version") - if None in [rohtext_col_index, website_col_idx, version_col_idx]: debug_print(f"FEHLER: Indizes website_batch fehlen."); return + # --- Indizes und Buchstaben --- + rohtext_col_key = "Website Rohtext" + rohtext_col_index = COLUMN_MAP.get(rohtext_col_key) + website_col_idx = COLUMN_MAP.get("CRM Website") + version_col_idx = COLUMN_MAP.get("Version") + if None in [rohtext_col_index, website_col_idx, version_col_idx]: + debug_print(f"FEHLER: Benötigte Indizes für process_website_batch fehlen.") + return rohtext_col_letter = sheet_handler._get_col_letter(rohtext_col_index + 1) version_col_letter = sheet_handler._get_col_letter(version_col_idx + 1) - def scrape_raw_text_task(task_info): # Worker unverändert + # --- Worker-Funktion für Scraping (unverändert) --- + def scrape_raw_text_task(task_info): row_num = task_info['row_num']; url = task_info['url']; raw_text = "k.A."; error = None - try: raw_text = get_website_raw(url) - except Exception as e: error = f"Scraping Fehler Z{row_num}: {e}"; debug_print(error) + try: raw_text = get_website_raw(url) # Annahme: get_website_raw ist definiert + except Exception as e: error = f"Scraping Fehler Zeile {row_num}: {e}"; debug_print(error) return {"row_num": row_num, "raw_text": raw_text, "error": error} - tasks_for_processing_batch = []; all_sheet_updates = [] - total_processed_count = 0; total_skipped_count = 0; total_skipped_url_count = 0; total_error_count = 0 - processing_batch_size = Config.PROCESSING_BATCH_SIZE; max_scraping_workers = Config.MAX_SCRAPING_WORKERS; update_batch_row_limit = Config.UPDATE_BATCH_ROW_LIMIT + # --- Hauptlogik: Iteriere und sammle Batches --- + tasks_for_processing_batch = [] + all_sheet_updates = [] + total_processed_count = 0 + total_skipped_count = 0 + total_skipped_url_count = 0 + total_error_count = 0 + + # Werte aus Config holen + processing_batch_size = Config.PROCESSING_BATCH_SIZE + max_scraping_workers = Config.MAX_SCRAPING_WORKERS + update_batch_row_limit = Config.UPDATE_BATCH_ROW_LIMIT empty_values_for_skip = ["", "k.a.", "k.a. (nur cookie-banner erkannt)", "k.a. (fehler)"] for i in range(start_row_index_in_sheet, end_row_index_in_sheet + 1): - row_index_in_list = i - 1 # 0-basierter Index in all_data + row_index_in_list = i - 1 if row_index_in_list >= len(all_data): continue row = all_data[row_index_in_list] - should_skip = False; cell_value_ar_str_lower = "INDEX_FEHLER" + # --- Prüfung, ob AR schon Inhalt hat --- + should_skip = False + cell_value_ar_str_lower = "INDEX_FEHLER" if len(row) > rohtext_col_index: cell_value_ar_str_lower = str(row[rohtext_col_index]).strip().lower() - if cell_value_ar_str_lower not in empty_values_for_skip: should_skip = True - # log_debug = (i < start_row_index_in_sheet + 2 or i > end_row_index_in_sheet - 2 or i % 500 == 0) - # if log_debug: debug_print(f"Zeile {i} (Website AR Check): Wert='{cell_value_ar_str_lower}'. Skip? {should_skip}") - if should_skip: total_skipped_count += 1; continue + if cell_value_ar_str_lower not in empty_values_for_skip: + should_skip = True + log_debug = (i < start_row_index_in_sheet + 5 or i > end_row_index_in_sheet - 5 or i % 500 == 0) + if log_debug: + debug_print(f"Zeile {i} (Website AR Check): Prüfe Inhalt Spalte {rohtext_col_letter}. Wert='{cell_value_ar_str_lower}'. Überspringen (da schon Inhalt)? -> {should_skip}") + + if should_skip: + total_skipped_count += 1 + continue + # --- Ende AR Prüfung --- + + # URL Prüfung website_url = row[website_col_idx] if len(row) > website_col_idx else "" - if not website_url or website_url.strip().lower() == "k.a.": total_skipped_url_count += 1; continue + if not website_url or website_url.strip().lower() == "k.a.": + total_skipped_url_count += 1 + continue tasks_for_processing_batch.append({"row_num": i, "url": website_url}) + # --- Verarbeitungs-Batch ausführen --- if len(tasks_for_processing_batch) >= processing_batch_size or i == end_row_index_in_sheet: if tasks_for_processing_batch: - batch_start_row = tasks_for_processing_batch[0]['row_num']; batch_end_row = tasks_for_processing_batch[-1]['row_num']; batch_task_count = len(tasks_for_processing_batch) - # debug_print(f"\n--- Scraping-Batch ({batch_task_count} Tasks, {batch_start_row}-{batch_end_row}) ---") - scraping_results = {}; batch_error_count = 0 - # debug_print(f" Scrape {batch_task_count} Websites parallel (max {max_scraping_workers} worker)...") + batch_start_row = tasks_for_processing_batch[0]['row_num'] + batch_end_row = tasks_for_processing_batch[-1]['row_num'] + batch_task_count = len(tasks_for_processing_batch) + debug_print(f"\n--- Starte Scraping-Batch ({batch_task_count} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") + + scraping_results = {} + batch_error_count = 0 # Fehlerzähler für diesen spezifischen Batch + debug_print(f" Scrape {batch_task_count} Websites parallel (max {max_scraping_workers} worker)...") with concurrent.futures.ThreadPoolExecutor(max_workers=max_scraping_workers) as executor: future_to_task = {executor.submit(scrape_raw_text_task, task): task for task in tasks_for_processing_batch} for future in concurrent.futures.as_completed(future_to_task): task = future_to_task[future] + # --- KORRIGIERTER TRY-EXCEPT Block --- try: - result = future.result(); scraping_results[result['row_num']] = result['raw_text'] - if result['error']: batch_error_count += 1; total_error_count += 1 + result = future.result() + scraping_results[result['row_num']] = result['raw_text'] + if result['error']: + batch_error_count += 1 + total_error_count += 1 except Exception as exc: - row_num = task['row_num']; err_msg = f"Gener. Fehler Scraping Z{row_num}: {exc}"; debug_print(err_msg) - scraping_results[row_num] = "k.A. (Fehler)"; batch_error_count += 1; total_error_count +=1 - current_batch_processed_count = len(scraping_results) + row_num = task['row_num'] + err_msg = f"Generischer Fehler Scraping Task Zeile {row_num}: {exc}" + debug_print(err_msg) + scraping_results[row_num] = "k.A. (Fehler)" + batch_error_count += 1 + total_error_count +=1 + # --- Ende Korrektur --- + + current_batch_processed_count = len(scraping_results) # Anzahl Ergebnisse (inkl. Fehler) total_processed_count += current_batch_processed_count - # debug_print(f" Scraping Batch beendet. {current_batch_processed_count} Ergebnisse ({batch_error_count} Fehler).") + debug_print(f" Scraping für Batch beendet. {current_batch_processed_count} Ergebnisse erhalten ({batch_error_count} Fehler in diesem Batch).") + + # Sheet Updates vorbereiten (AR und AP) if scraping_results: - current_version = Config.VERSION; batch_sheet_updates = [] + current_version = Config.VERSION + batch_sheet_updates = [] for row_num, raw_text_res in scraping_results.items(): - row_updates = [{'range': f'{rohtext_col_letter}{row_num}', 'values': [[raw_text_res]]}, {'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]}] + # Updates für AR und AP + row_updates = [ + {'range': f'{rohtext_col_letter}{row_num}', 'values': [[raw_text_res]]}, + {'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]} + ] batch_sheet_updates.extend(row_updates) - all_sheet_updates.extend(batch_sheet_updates) - tasks_for_processing_batch = [] - if len(all_sheet_updates) >= update_batch_row_limit * 2: # *2 Updates pro Zeile + all_sheet_updates.extend(batch_sheet_updates) # Sammle für größeren Batch-Update + + tasks_for_processing_batch = [] # Batch leeren + + # Sheet Updates senden (wenn update_batch_row_limit erreicht) + # Prüfe die Anzahl der *Zeilen*, für die Updates gesammelt wurden + # Da wir jetzt Updates für alle Ergebnisse sammeln, prüfen wir direkt die Länge von all_sheet_updates + if len(all_sheet_updates) >= update_batch_row_limit * 2: # *2 weil 2 Updates pro Zeile debug_print(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") success = sheet_handler.batch_update_cells(all_sheet_updates) - if success: debug_print(f" Sheet-Update bis Z{batch_end_row} OK.") - else: debug_print(f" FEHLER Sheet-Update bis Z{batch_end_row}.") - all_sheet_updates = [] - if all_sheet_updates: debug_print(f"Sende finale Sheet-Updates ({len(all_sheet_updates)} Zellen)..."); sheet_handler.batch_update_cells(all_sheet_updates) - debug_print(f"Website-Scraping ROHDATEN beendet. {total_processed_count} verarbeitet ({total_error_count} Fehler), {total_skipped_count} wg. Inhalt übersprungen, {total_skipped_url_count} ohne URL übersprungen.") + if success: debug_print(f" Sheet-Update bis Zeile {batch_end_row} erfolgreich.") # Logge Endzeile des Batches + else: debug_print(f" FEHLER beim Sheet-Update bis Zeile {batch_end_row}.") + all_sheet_updates = [] # Zurücksetzen nach Senden -def process_website_summarization_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet): # Nutzt Config.HEADER_ROWS - """ Batch-Prozess NUR für Website-Zusammenfassung (AS). Prüft AR und AS. """ - debug_print(f"Starte Website-Zusammenfassung (Batch) {start_row_index_in_sheet}-{end_row_index_in_sheet}...") - openai_batch_size = Config.OPENAI_BATCH_SIZE_LIMIT; update_batch_row_limit = Config.UPDATE_BATCH_ROW_LIMIT + # Finale Sheet Updates senden + if all_sheet_updates: + debug_print(f"Sende finale Sheet-Updates ({len(all_sheet_updates)} Zellen)...") + sheet_handler.batch_update_cells(all_sheet_updates) + + debug_print(f"Website-Scraping NUR ROHDATEN abgeschlossen. {total_processed_count} Websites verarbeitet (inkl. Fehler), {total_error_count} Fehler, {total_skipped_count} Zeilen wg. Inhalt übersprungen, {total_skipped_url_count} Zeilen ohne URL übersprungen.") + + + +# NEUE Funktion process_website_summarization_batch +def process_website_summarization_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet): + """ + Batch-Prozess NUR für Website-Zusammenfassung (AS). + Lädt Daten neu, prüft, ob Rohtext (AR) vorhanden und Zusammenfassung (AS) fehlt. + Fasst Rohtexte im Batch via OpenAI zusammen und setzt AS + AP. + """ + debug_print(f"Starte Website-Zusammenfassung (OpenAI Batch) für Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}...") + + # --- Konfiguration --- + openai_batch_size = Config.OPENAI_BATCH_SIZE_LIMIT # Holt Wert aus Config (jetzt z.B. 1) + update_batch_row_limit = Config.UPDATE_BATCH_ROW_LIMIT # z.B. 50 + + # --- Lade Daten --- if not sheet_handler.load_data(): return all_data = sheet_handler.get_all_data_with_headers() - if not all_data or len(all_data) <= Config.HEADER_ROWS: return - header_rows = Config.HEADER_ROWS + if not all_data or len(all_data) <= 5: return + header_rows = 5 - rohtext_col_idx = COLUMN_MAP.get("Website Rohtext"); summary_col_idx = COLUMN_MAP.get("Website Zusammenfassung"); version_col_idx = COLUMN_MAP.get("Version") - if None in [rohtext_col_idx, summary_col_idx, version_col_idx]: return debug_print(f"FEHLER: Indizes Summary fehlen.") - summary_col_letter = sheet_handler._get_col_letter(summary_col_idx + 1); version_col_letter = sheet_handler._get_col_letter(version_col_idx + 1) + # --- Indizes und Buchstaben --- + rohtext_col_idx = COLUMN_MAP.get("Website Rohtext") + summary_col_idx = COLUMN_MAP.get("Website Zusammenfassung") + version_col_idx = COLUMN_MAP.get("Version") + if None in [rohtext_col_idx, summary_col_idx, version_col_idx]: return debug_print(f"FEHLER: Indizes fehlen.") + summary_col_letter = sheet_handler._get_col_letter(summary_col_idx + 1) + version_col_letter = sheet_handler._get_col_letter(version_col_idx + 1) - tasks_for_openai_batch = []; all_sheet_updates = []; rows_in_current_update_batch = 0 - processed_count = 0; skipped_no_rohtext = 0; skipped_summary_exists = 0 + # --- Verarbeitung --- + tasks_for_openai_batch = [] + all_sheet_updates = [] + rows_in_current_update_batch = 0 + processed_count = 0 + skipped_no_rohtext = 0 + skipped_summary_exists = 0 for i in range(start_row_index_in_sheet, end_row_index_in_sheet + 1): - row_index_in_list = i - 1 # 0-basierter Index in all_data + row_index_in_list = i - 1 if row_index_in_list >= len(all_data): continue row = all_data[row_index_in_list] - raw_text = ""; summary_exists = False + # Prüfung 1: Ist Rohtext vorhanden und gültig? + raw_text = "" if len(row) > rohtext_col_idx: raw_text = str(row[rohtext_col_idx]).strip() - if not raw_text or raw_text == "k.A." or raw_text == "k.A. (Nur Cookie-Banner erkannt)" or raw_text == "k.A. (Fehler)": skipped_no_rohtext += 1; continue - if len(row) > summary_col_idx and str(row[summary_col_idx]).strip() and str(row[summary_col_idx]).strip() != "k.A.": summary_exists = True + if not raw_text or raw_text == "k.A." or raw_text == "k.A. (Nur Cookie-Banner erkannt)" or raw_text == "k.A. (Fehler)": + skipped_no_rohtext += 1; continue + + # Prüfung 2: Fehlt die Zusammenfassung (AS)? + summary_exists = False + if len(row) > summary_col_idx and str(row[summary_col_idx]).strip() and str(row[summary_col_idx]).strip() != "k.A.": + summary_exists = True if summary_exists: skipped_summary_exists += 1; continue - tasks_for_openai_batch.append({'row_num': i, 'raw_text': raw_text}); processed_count += 1 + # Task hinzufügen + tasks_for_openai_batch.append({'row_num': i, 'raw_text': raw_text}) + processed_count += 1 - if tasks_for_openai_batch and (len(tasks_for_openai_batch) >= openai_batch_size or (processed_count > 0 and i == end_row_index_in_sheet)): - # debug_print(f" Verarbeite OpenAI Batch {len(tasks_for_openai_batch)} Tasks (Start: {tasks_for_openai_batch[0]['row_num']})...") - summaries_result = summarize_batch_openai(tasks_for_openai_batch) + # --- OpenAI Batch verarbeiten, wenn voll oder letzte Zeile --- + if tasks_for_openai_batch and \ + (len(tasks_for_openai_batch) >= openai_batch_size or (processed_count > 0 and i == end_row_index_in_sheet)): + debug_print(f" Verarbeite OpenAI Batch für {len(tasks_for_openai_batch)} Aufgaben (Start: {tasks_for_openai_batch[0]['row_num']})...") + summaries_result = summarize_batch_openai(tasks_for_openai_batch) # Ruft modifizierte Funktion auf + + # Sheet Updates für diesen OpenAI Batch vorbereiten current_version = Config.VERSION - for task in tasks_for_openai_batch: - row_num = task['row_num']; summary = summaries_result.get(row_num, "k.A. (Fehler Batch Zuordnung)") - row_updates = [{'range': f'{summary_col_letter}{row_num}', 'values': [[summary]]}, {'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]}] - all_sheet_updates.extend(row_updates); rows_in_current_update_batch += 1 - tasks_for_openai_batch = [] - time.sleep(Config.RETRY_DELAY) # Pause nach OpenAI Batch Call + for task in tasks_for_openai_batch: # Iteriere über die *gesendeten* Tasks + row_num = task['row_num'] + summary = summaries_result.get(row_num, "k.A. (Fehler Batch Zuordnung)") + row_updates = [ + {'range': f'{summary_col_letter}{row_num}', 'values': [[summary]]}, + {'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]} + ] + all_sheet_updates.extend(row_updates) + rows_in_current_update_batch += 1 - if all_sheet_updates and (rows_in_current_update_batch >= update_batch_row_limit or (processed_count > 0 and i == end_row_index_in_sheet)): + tasks_for_openai_batch = [] # OpenAI Batch leeren + + # --- Gesammelte Sheet Updates senden --- + if all_sheet_updates and \ + (rows_in_current_update_batch >= update_batch_row_limit or (processed_count > 0 and i == end_row_index_in_sheet)): debug_print(f" Sende Sheet-Update für {rows_in_current_update_batch} Zusammenfassungen...") success = sheet_handler.batch_update_cells(all_sheet_updates) - if success: debug_print(f" Sheet-Update bis Z{i} OK.") - else: debug_print(f" FEHLER Sheet-Update bis Z{i}.") + if success: debug_print(f" Sheet-Update bis Zeile {i} erfolgreich.") + else: debug_print(f" FEHLER beim Sheet-Update bis Zeile {i}.") all_sheet_updates = []; rows_in_current_update_batch = 0 - if all_sheet_updates: debug_print(f"Sende LETZTES Sheet-Update für {rows_in_current_update_batch} Zusammenfassungen..."); sheet_handler.batch_update_cells(all_sheet_updates) - debug_print(f"Website-Zusammenfassung Batch beendet. {processed_count} angefordert, {skipped_no_rohtext} ohne Rohtext, {skipped_summary_exists} mit Summary übersprungen.") -def process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet, force_process=False): # Nutzt Config.HEADER_ROWS - """ Batch-Prozess für Brancheneinschätzung. Prüft AO (außer bei force_process). """ - mode_desc = "(Force Process)" if force_process else "(Timestamp AO Check)" - debug_print(f"Starte Brancheneinschätzung (Parallel Batch) {start_row_index_in_sheet}-{end_row_index_in_sheet} {mode_desc}...") + # Letzten Sheet Update Batch senden + if all_sheet_updates: + debug_print(f"Sende LETZTES Sheet-Update für {rows_in_current_update_batch} Zusammenfassungen...") + sheet_handler.batch_update_cells(all_sheet_updates) + + debug_print(f"Website-Zusammenfassungs-Batch abgeschlossen. {processed_count} Zusammenfassungen angefordert, {skipped_no_rohtext} wg. fehlendem Rohtext übersprungen, {skipped_summary_exists} wg. vorhandener Zusammenfassung übersprungen.") + +# Komplette Funktion process_branch_batch (prüft jetzt Timestamp AO mit erzwungenem Debugging) +# Komplette Funktion process_branch_batch (MIT Korrektur und Prüfung auf AO) +def process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet): + """ + Batch-Prozess für Brancheneinschätzung mit paralleler Verarbeitung via Threads. + Prüft Timestamp AO, führt evaluate_branche_chatgpt parallel aus (limitiert), + setzt W, X, Y, AO + AP und sendet Sheet-Updates GEBÜNDELT PRO VERARBEITUNGS-BATCH. + """ + debug_print(f"Starte Brancheneinschätzung (Parallel Batch) für Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}...") + if not sheet_handler.load_data(): return all_data = sheet_handler.get_all_data_with_headers() - if not all_data or len(all_data) <= Config.HEADER_ROWS: return - header_rows = Config.HEADER_ROWS + if not all_data or len(all_data) <= 5: return + header_rows = 5 + # --- Indizes und Buchstaben --- timestamp_col_key = "Timestamp letzte Prüfung"; timestamp_col_index = COLUMN_MAP.get(timestamp_col_key) branche_crm_idx = COLUMN_MAP.get("CRM Branche"); beschreibung_idx = COLUMN_MAP.get("CRM Beschreibung") branche_wiki_idx = COLUMN_MAP.get("Wiki Branche"); kategorien_wiki_idx = COLUMN_MAP.get("Wiki Kategorien") summary_web_idx = COLUMN_MAP.get("Website Zusammenfassung"); version_col_idx = COLUMN_MAP.get("Version") - branch_w_idx = COLUMN_MAP.get("Chat Vorschlag Branche"); branch_x_idx = COLUMN_MAP.get("Chat Konsistenz Branche"); branch_y_idx = COLUMN_MAP.get("Chat Begründung Abweichung Branche") + branch_w_idx = COLUMN_MAP.get("Chat Vorschlag Branche"); branch_x_idx = COLUMN_MAP.get("Chat Konsistenz Branche") + branch_y_idx = COLUMN_MAP.get("Chat Begründung Abweichung Branche") required_indices = [timestamp_col_index, branche_crm_idx, beschreibung_idx, branche_wiki_idx, kategorien_wiki_idx, summary_web_idx, version_col_idx, branch_w_idx, branch_x_idx, branch_y_idx] - if None in required_indices: return debug_print(f"FEHLER: Indizes Branch fehlen.") + if None in required_indices: return debug_print(f"FEHLER: Indizes fehlen.") ts_col_letter = sheet_handler._get_col_letter(timestamp_col_index + 1) version_col_letter = sheet_handler._get_col_letter(version_col_idx + 1) - branch_w_letter = sheet_handler._get_col_letter(branch_w_idx + 1); branch_x_letter = sheet_handler._get_col_letter(branch_x_idx + 1); branch_y_letter = sheet_handler._get_col_letter(branch_y_idx + 1) + branch_w_letter = sheet_handler._get_col_letter(branch_w_idx + 1) + branch_x_letter = sheet_handler._get_col_letter(branch_x_idx + 1) + branch_y_letter = sheet_handler._get_col_letter(branch_y_idx + 1) - MAX_BRANCH_WORKERS = Config.MAX_BRANCH_WORKERS; OPENAI_CONCURRENCY_LIMIT = Config.OPENAI_CONCURRENCY_LIMIT - openai_semaphore_branch = threading.Semaphore(OPENAI_CONCURRENCY_LIMIT); PROCESSING_BRANCH_BATCH_SIZE = Config.PROCESSING_BRANCH_BATCH_SIZE + # --- Konfiguration --- + MAX_BRANCH_WORKERS = Config.MAX_BRANCH_WORKERS + OPENAI_CONCURRENCY_LIMIT = Config.OPENAI_CONCURRENCY_LIMIT + openai_semaphore_branch = threading.Semaphore(OPENAI_CONCURRENCY_LIMIT) + PROCESSING_BRANCH_BATCH_SIZE = Config.PROCESSING_BRANCH_BATCH_SIZE + update_batch_row_limit = Config.UPDATE_BATCH_ROW_LIMIT # Wird derzeit nicht verwendet, da wir pro Batch senden - def evaluate_branch_task(task_data): # Worker unverändert - row_num = task_data['row_num']; result = {"branch": "k.A. (Fehler Task)", "consistency": "error", "justification": "Fehler Worker-Task"}; error = None + # --- Worker Funktion --- + def evaluate_branch_task(task_data): + row_num = task_data['row_num']; result = {"branch": "k.A. (Fehler Task)", "consistency": "error", "justification": "Fehler in Worker-Task"}; error = None try: with openai_semaphore_branch: + # debug_print(f" Task {row_num}: Warte auf Semaphore...") # Sehr detailliertes Logging + # time.sleep(0.1) # Minimale Pause reduziert manchmal Race Conditions bei hoher Last + # debug_print(f" Task {row_num}: Semaphore erhalten, starte evaluate_branche_chatgpt...") result = evaluate_branche_chatgpt( task_data['crm_branche'], task_data['beschreibung'], task_data['wiki_branche'], task_data['wiki_kategorien'], task_data['website_summary']) - except Exception as e: error = f"Fehler Branch Eval Z{row_num}: {e}"; debug_print(error); result['justification'] = error[:500]; result['consistency'] = 'error_task' + # debug_print(f" Task {row_num}: evaluate_branche_chatgpt beendet.") + except Exception as e: + error = f"Fehler bei Branchenevaluation Zeile {row_num}: {e}"; debug_print(error); result['justification'] = error[:500]; result['consistency'] = 'error_task' return {"row_num": row_num, "result": result, "error": error} - tasks_for_processing_batch = []; total_processed_count = 0; total_skipped_count = 0; total_error_count = 0 + # --- Hauptverarbeitung --- + tasks_for_processing_batch = [] + total_processed_count = 0; total_skipped_count = 0; total_error_count = 0 + if not ALLOWED_TARGET_BRANCHES: load_target_schema(); if not ALLOWED_TARGET_BRANCHES: return debug_print("FEHLER: Ziel-Schema nicht geladen.") for i in range(start_row_index_in_sheet, end_row_index_in_sheet + 1): - row_index_in_list = i - 1 # 0-basierter Index in all_data + row_index_in_list = i - 1 if row_index_in_list >= len(all_data): continue row = all_data[row_index_in_list] + # Timestamp-Prüfung (AO) should_skip = False - if not force_process: - if len(row) > timestamp_col_index and str(row[timestamp_col_index]).strip(): should_skip = True + if len(row) > timestamp_col_index and str(row[timestamp_col_index]).strip(): should_skip = True if should_skip: total_skipped_count += 1; continue - task_data = { "row_num": i, - "crm_branche": row[branche_crm_idx] if len(row) > branche_crm_idx else "", - "beschreibung": row[beschreibung_idx] if len(row) > beschreibung_idx else "", - "wiki_branche": row[branche_wiki_idx] if len(row) > branche_wiki_idx else "", - "wiki_kategorien": row[kategorien_wiki_idx] if len(row) > kategorien_wiki_idx else "", - "website_summary": row[summary_web_idx] if len(row) > summary_web_idx else "" } + # Task sammeln + task_data = { "row_num": i, "crm_branche": row[branche_crm_idx] if len(row) > branche_crm_idx else "", "beschreibung": row[beschreibung_idx] if len(row) > beschreibung_idx else "", "wiki_branche": row[branche_wiki_idx] if len(row) > branche_wiki_idx else "", "wiki_kategorien": row[kategorien_wiki_idx] if len(row) > kategorien_wiki_idx else "", "website_summary": row[summary_web_idx] if len(row) > summary_web_idx else ""} tasks_for_processing_batch.append(task_data) + # --- Verarbeitungs-Batch ausführen --- if len(tasks_for_processing_batch) >= PROCESSING_BRANCH_BATCH_SIZE or i == end_row_index_in_sheet: if tasks_for_processing_batch: - batch_start_row = tasks_for_processing_batch[0]['row_num']; batch_end_row = tasks_for_processing_batch[-1]['row_num']; batch_task_count = len(tasks_for_processing_batch) - debug_print(f"\n--- Branch-Eval Batch ({batch_task_count} Tasks, {batch_start_row}-{batch_end_row}) ---") + batch_start_row = tasks_for_processing_batch[0]['row_num']; batch_end_row = tasks_for_processing_batch[-1]['row_num'] + batch_task_count = len(tasks_for_processing_batch) + debug_print(f"\n--- Starte Branch-Evaluation Batch ({batch_task_count} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") + results_list = []; batch_error_count = 0 - # debug_print(f" Evaluiere {batch_task_count} parallel (max {MAX_BRANCH_WORKERS} worker, {OPENAI_CONCURRENCY_LIMIT} OpenAI)...") + debug_print(f" Evaluiere {batch_task_count} Zeilen parallel (max {MAX_BRANCH_WORKERS} worker, {OPENAI_CONCURRENCY_LIMIT} OpenAI gleichzeitig)...") + # *** BEGINN PARALLELE VERARBEITUNG *** with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_BRANCH_WORKERS) as executor: future_to_task = {executor.submit(evaluate_branch_task, task): task for task in tasks_for_processing_batch} + # Warte auf Ergebnisse und sammle sie for future in concurrent.futures.as_completed(future_to_task): task = future_to_task[future] try: result_data = future.result(); results_list.append(result_data); except Exception as exc: - row_num = task['row_num']; err_msg = f"Gener. Fehler Branch Z{row_num}: {exc}"; debug_print(err_msg) + row_num = task['row_num']; err_msg = f"Generischer Fehler Branch Task Zeile {row_num}: {exc}"; debug_print(err_msg) results_list.append({"row_num": row_num, "result": {"branch": "FEHLER", "consistency": "error_task", "justification": err_msg[:500]}, "error": err_msg}) batch_error_count += 1; total_error_count +=1 + # Zähle Fehler aus dem Ergebnis-Dict if results_list[-1]['error']: batch_error_count += 1; total_error_count +=1 - current_batch_processed_count = len(results_list); total_processed_count += current_batch_processed_count - # debug_print(f" Branch-Eval Batch beendet. {current_batch_processed_count} Ergebnisse ({batch_error_count} Fehler).") + + # *** ENDE PARALLELE VERARBEITUNG *** + current_batch_processed_count = len(results_list) + total_processed_count += current_batch_processed_count + debug_print(f" Branch-Evaluation für Batch beendet. {current_batch_processed_count} Ergebnisse erhalten ({batch_error_count} Fehler in diesem Batch).") + + # Sheet Updates vorbereiten FÜR DIESEN BATCH if results_list: - current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S"); current_version = Config.VERSION; batch_sheet_updates = [] + current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + current_version = Config.VERSION + batch_sheet_updates = [] + # Sortiere Ergebnisse nach Zeilennummer für geordnetes Schreiben (optional) results_list.sort(key=lambda x: x['row_num']) for res_data in results_list: row_num = res_data['row_num']; result = res_data['result'] - # debug_print(f" Z{row_num}: Ergebnis -> B='{result.get('branch')}', C='{result.get('consistency')}', J='{result.get('justification', '')[:50]}...'") + # Logge das individuelle Ergebnis VOR dem Update + debug_print(f" Zeile {row_num}: Ergebnis -> Branch='{result.get('branch')}', Consistency='{result.get('consistency')}', Justification='{result.get('justification', '')[:50]}...'") row_updates = [ {'range': f'{branch_w_letter}{row_num}', 'values': [[result.get("branch", "Fehler")]]}, {'range': f'{branch_x_letter}{row_num}', 'values': [[result.get("consistency", "Fehler")]]}, {'range': f'{branch_y_letter}{row_num}', 'values': [[result.get("justification", "Fehler")]]}, {'range': f'{ts_col_letter}{row_num}', 'values': [[current_timestamp]]}, - {'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]} ] + {'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]} + ] batch_sheet_updates.extend(row_updates) + + # Sende Updates für DIESEN Batch SOFORT if batch_sheet_updates: debug_print(f" Sende Sheet-Update für {len(results_list)} Zeilen ({len(batch_sheet_updates)} Zellen)...") success = sheet_handler.batch_update_cells(batch_sheet_updates) - if success: debug_print(f" Sheet-Update Batch {batch_start_row}-{batch_end_row} OK.") - else: debug_print(f" FEHLER Sheet-Update Batch {batch_start_row}-{batch_end_row}.") - tasks_for_processing_batch = [] - debug_print(f"--- Verarbeitungs-Batch {batch_start_row}-{batch_end_row} abgeschlossen ---") - time.sleep(1) # Kurze Pause nach Batch - debug_print(f"Brancheneinschätzung (Parallel Batch) beendet. {total_processed_count} verarbeitet ({total_error_count} Fehler), {total_skipped_count} übersprungen.") + if success: debug_print(f" Sheet-Update für Batch {batch_start_row}-{batch_end_row} erfolgreich.") + else: debug_print(f" FEHLER beim Sheet-Update für Batch {batch_start_row}-{batch_end_row}.") + else: debug_print(f" Keine Sheet-Updates für Batch {batch_start_row}-{batch_end_row} vorbereitet.") -# ==================== DISPATCHER ==================== -def run_dispatcher(mode, sheet_handler, row_limit=None): # Nutzt Config.HEADER_ROWS, Übergibt force_process - """ Wählt passenden Batch-Prozess und ermittelt Startzeile dynamisch. """ - debug_print(f"Starte Dispatcher Modus '{mode}', Limit={row_limit}.") - header_rows = Config.HEADER_ROWS - start_col_key = "Timestamp letzte Prüfung"; min_start_row = 7 # Standard AO - if mode == "website": start_col_key = "Website Rohtext" - elif mode == "wiki": start_col_key = "Wiki Verif. Timestamp" - elif mode == "branch": start_col_key = "Timestamp letzte Prüfung" - elif mode == "summarize": start_col_key = "Website Zusammenfassung" - elif mode == "combined": start_col_key = "Timestamp letzte Prüfung" - debug_print(f"Dispatcher: Ermittle Startzeile ({start_col_key})...") + tasks_for_processing_batch = [] # Batch leeren + debug_print(f"--- Verarbeitungs-Batch {batch_start_row}-{batch_end_row} abgeschlossen ---") + # Kurze Pause NACHDEM ein Batch komplett verarbeitet und geschrieben wurde + # debug_print(" Warte 1 Sekunde...") # Test-Log + time.sleep(1) + + # !!! HIER DARF KEIN SLEEP STEHEN !!! + # time.sleep(Config.RETRY_DELAY) # <<< DIESE ZEILE MUSS WEG SEIN in deinem Code! + + debug_print(f"Brancheneinschätzung (Parallel Batch) abgeschlossen. {total_processed_count} Zeilen verarbeitet (inkl. Fehler), {total_error_count} Fehler, {total_skipped_count} Zeilen wg. Timestamp übersprungen.") + + +# Annahmen: +# - Funktionen debug_print, process_verification_only, process_website_batch, process_branch_batch sind definiert. +# - sheet_handler ist eine initialisierte Instanz von GoogleSheetHandler (mit der korrekten get_start_row_index Methode). +# - Globale Konstante header_rows (oder besser, hol sie vom sheet_handler?) + +# Komplette run_dispatcher Funktion (Start immer basierend auf AO) +# Komplette run_dispatcher Funktion (Keine Änderungen hier nötig) +def run_dispatcher(mode, sheet_handler, row_limit=None): + """ + Wählt den passenden Batch-Prozess basierend auf dem Modus. + Ermittelt die Startzeile dynamisch basierend auf der relevanten Spalte für den Modus. + """ + debug_print(f"Starte Dispatcher im Modus '{mode}' mit row_limit={row_limit}.") + header_rows = 5 + + # Startspalte für jeden Modus + start_col_key = "Timestamp letzte Prüfung" # Standard AO + min_start_row = 7 + if mode == "website": start_col_key = "Website Rohtext" # AR + elif mode == "wiki": start_col_key = "Wiki Verif. Timestamp" # AX + elif mode == "branch": start_col_key = "Timestamp letzte Prüfung" # AO + elif mode == "summarize": start_col_key = "Website Zusammenfassung" # AS + elif mode == "combined": start_col_key = "Timestamp letzte Prüfung" # AO + + debug_print(f"Dispatcher: Ermittle Startzeile basierend auf Spalte '{start_col_key}'...") + # get_start_row_index prüft jetzt auf exakt "" start_data_index = sheet_handler.get_start_row_index(check_column_key=start_col_key, min_sheet_row=min_start_row) + if start_data_index == -1: return debug_print(f"FEHLER: Startspalte '{start_col_key}' prüfen!") + start_row_index_in_sheet = start_data_index + header_rows + 1 - total_sheet_rows = len(sheet_handler.sheet_values) # Gesamtzahl Zeilen aus Handler nehmen - if start_data_index >= len(sheet_handler.get_data()): return debug_print("Startindex liegt hinter der letzten Datenzeile. Keine Verarbeitung.") - if start_row_index_in_sheet > total_sheet_rows: return debug_print("Ungültige Startzeile berechnet.") - end_row_index_in_sheet = total_sheet_rows # Standard: bis Ende + total_sheet_rows = len(sheet_handler.sheet_values) + + # Prüfungen (wie gehabt) + if start_data_index >= len(sheet_handler.get_data()): return debug_print("Start nach Ende.") + if start_row_index_in_sheet > total_sheet_rows: return debug_print("Ungültige Startzeile.") + + # Endzeile if row_limit is not None and row_limit > 0: end_row_index_in_sheet = min(start_row_index_in_sheet + row_limit - 1, total_sheet_rows) - elif row_limit == 0: return debug_print("Limit 0 -> Keine Verarbeitung.") + elif row_limit == 0: return debug_print("Limit 0.") + else: end_row_index_in_sheet = total_sheet_rows debug_print(f"Dispatcher: Verarbeitung geplant für Sheet-Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}.") - if start_row_index_in_sheet > end_row_index_in_sheet: return debug_print("Startzeile liegt hinter Endzeile. Keine Verarbeitung.") + if start_row_index_in_sheet > end_row_index_in_sheet: return debug_print("Start nach Ende (berechnet).") + + # Modusauswahl try: if mode == "wiki": process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) - elif mode == "website": process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) - elif mode == "branch": process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet, force_process=False) # Standard mit TS Check + elif mode == "website": process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AR, Setzt AR+AP + elif mode == "branch": process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) elif mode == "summarize": process_website_summarization_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) elif mode == "combined": - debug_print("--- Combined: Wiki (AX Check) ---"); process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet); time.sleep(1) - debug_print("--- Combined: Website Scrape (AR Check) ---"); process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet); time.sleep(1) - debug_print("--- Combined: Website Summarize (AS Check) ---"); process_website_summarization_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet); time.sleep(1) - debug_print("--- Combined: Branch (Force Process) ---"); process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet, force_process=True) # Erzwingen + debug_print("--- Start Combined Mode: Wiki ---"); process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet); time.sleep(1) + debug_print("--- Start Combined Mode: Website Scraping ---"); process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet); time.sleep(1) + debug_print("--- Start Combined Mode: Website Summarization ---"); process_website_summarization_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet); time.sleep(1) + debug_print("--- Start Combined Mode: Branch ---"); process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) debug_print("--- Combined Mode abgeschlossen ---") - else: debug_print(f"Ungültiger Dispatcher-Modus '{mode}'.") - except Exception as e: debug_print(f"FEHLER im Dispatcher: {e}"); debug_print(traceback.format_exc()) + else: debug_print(f"Ungültiger Modus '{mode}'.") + except Exception as e: debug_print(f"FEHLER im Dispatcher: {e}"); import traceback; debug_print(traceback.format_exc()) + +# --- Ende run_dispatcher Funktion --- # ==================== SERP API / LINKEDIN FUNCTIONS ==================== @retry_on_failure -def serp_website_lookup(company_name): # Unverändert +def serp_website_lookup(company_name): """Ermittelt Website via SERP API (Google Suche).""" - serp_key = Config.API_KEYS.get('serpapi'); blacklist = ["bloomberg.com", "northdata.de", "finanzen.net", "handelsblatt.com", "wikipedia.org", "linkedin.com"] - if not serp_key: debug_print("Fehler: SerpAPI Key fehlt."); return "k.A." + serp_key = Config.API_KEYS.get('serpapi') + if not serp_key: + debug_print("Fehler: SerpAPI Key nicht verfügbar für Website Lookup.") + return "k.A." if not company_name: return "k.A." - query = f'{company_name} offizielle Website'; params = {"engine": "google", "q": query, "api_key": serp_key, "hl": "de", "gl": "de"} + + # Blacklist unerwünschter Domains + blacklist = ["bloomberg.com", "northdata.de", "finanzen.net", "handelsblatt.com", "wikipedia.org", "linkedin.com"] + + query = f'{company_name} offizielle Website' # Präzisere Query + params = { + "engine": "google", + "q": query, + "api_key": serp_key, + "hl": "de", + "gl": "de" # Geolocation auf Deutschland setzen + } api_url = "https://serpapi.com/search" + try: - response = requests.get(api_url, params=params, timeout=10); response.raise_for_status(); data = response.json() + response = requests.get(api_url, params=params, timeout=10) + response.raise_for_status() + data = response.json() + + # 1. Knowledge Graph prüfen (oft die offizielle Seite) if "knowledge_graph" in data and "website" in data["knowledge_graph"]: kg_url = data["knowledge_graph"]["website"] - if kg_url and not any(bad in kg_url for bad in blacklist): - norm_url = simple_normalize_url(kg_url) - if norm_url != "k.A.": debug_print(f"SERP: Website '{norm_url}' aus KG."); return norm_url + if kg_url and not any(bad_domain in kg_url for bad_domain in blacklist): + normalized_url = simple_normalize_url(kg_url) + if normalized_url != "k.A.": + debug_print(f"SERP Lookup: Website '{normalized_url}' aus Knowledge Graph für '{company_name}' gefunden.") + return normalized_url + + # 2. Organische Ergebnisse prüfen if "organic_results" in data: for result in data["organic_results"]: url = result.get("link", "") - if url and not any(bad in url for bad in blacklist) and url.startswith("http"): - norm_url = simple_normalize_url(url) - if norm_url != "k.A.": - domain = norm_url.replace('www.', '').split('.')[0] - if domain in normalize_company_name(company_name): debug_print(f"SERP: Website '{norm_url}' aus Organic."); return norm_url - # else: debug_print(f"SERP: URL '{norm_url}' übersprungen (Domain passt nicht).") - debug_print(f"SERP: Keine passende Website für '{company_name}'."); return "k.A." - except requests.exceptions.RequestException as e: debug_print(f"Fehler SERP Website Lookup '{company_name}': {e}"); return "k.A." - except Exception as e: debug_print(f"Allg. Fehler SERP Website Lookup '{company_name}': {e}"); return "k.A." + # Prüfe Blacklist und ob es eine "echte" Website ist (nicht nur Suche etc.) + if url and not any(bad_domain in url for bad_domain in blacklist) and url.startswith("http"): + normalized_url = simple_normalize_url(url) + if normalized_url != "k.A.": + # Zusätzliche Plausibilitätsprüfung: Enthält die Domain Teile des Firmennamens? + domain_part = normalized_url.replace('www.', '').split('.')[0] + if domain_part in normalize_company_name(company_name): + debug_print(f"SERP Lookup: Website '{normalized_url}' aus Organic Results für '{company_name}' gefunden.") + return normalized_url + else: + debug_print(f"SERP Lookup: URL '{normalized_url}' übersprungen (Domain passt nicht zu '{company_name}').") + + debug_print(f"SERP Lookup: Keine passende Website für '{company_name}' gefunden.") + return "k.A." + except requests.exceptions.RequestException as e: + debug_print(f"Fehler beim SERP API Website Lookup für '{company_name}': {e}") + return "k.A." + except Exception as e: + debug_print(f"Allgemeiner Fehler beim SERP API Website Lookup für '{company_name}': {e}") + return "k.A." + @retry_on_failure -def search_linkedin_contacts(company_name, website, position_query, crm_kurzform, num_results=10): # Unverändert +def search_linkedin_contacts(company_name, website, position_query, crm_kurzform, num_results=10): """Sucht LinkedIn Kontakte via SERP API.""" serp_key = Config.API_KEYS.get('serpapi') - if not serp_key: debug_print("Fehler: SerpAPI Key fehlt."); return [] - if not all([company_name, position_query, crm_kurzform]): return [] - query = f'site:linkedin.com/in "{position_query}" "{crm_kurzform}"' # Suche Kurzform im Titel - params = {"engine": "google", "q": query, "api_key": serp_key, "hl": "de", "gl": "de", "num": num_results } + if not serp_key: + debug_print("Fehler: SerpAPI Key nicht verfügbar für LinkedIn Suche.") + return [] + if not all([company_name, position_query, crm_kurzform]): + return [] + + # Query anpassen für bessere Ergebnisse + query = f'site:linkedin.com/in "{position_query}" "{crm_kurzform}"' # Suche nach Kurzform im Titel + # query = f'site:linkedin.com/in "{position_query}" "{company_name}"' # Original Query + params = { + "engine": "google", + "q": query, + "api_key": serp_key, + "hl": "de", + "gl": "de", + "num": num_results # Google's num Parameter (max 100, aber oft weniger geliefert) + } api_url = "https://serpapi.com/search" + try: - response = requests.get(api_url, params=params, timeout=15); response.raise_for_status(); data = response.json(); contacts = [] + response = requests.get(api_url, params=params, timeout=15) # Längerer Timeout + response.raise_for_status() + data = response.json() + contacts = [] + if "organic_results" in data: for result in data["organic_results"]: - title = result.get("title", ""); linkedin_url = result.get("link", "") - if not linkedin_url or "linkedin.com/in/" not in linkedin_url: continue - if crm_kurzform.lower() not in title.lower(): # debug_print(f"LinkedIn Skip: '{crm_kurzform}' nicht in '{title}'"); + title = result.get("title", "") + linkedin_url = result.get("link", "") + + # Filter: Muss LinkedIn URL sein und Kurzform muss im Titel vorkommen + if not linkedin_url or "linkedin.com/in/" not in linkedin_url: continue - name_part = ""; pos_part = position_query; separators = ["–", "-", "|", " at ", " bei "]; title_cleaned = title.replace("...", "").strip(); found_sep = False + if crm_kurzform.lower() not in title.lower(): + debug_print(f"LinkedIn Treffer übersprungen: Kurzform '{crm_kurzform}' nicht in Titel '{title}'") + continue + + # Extrahiere Name und Position aus Titel + name_part = "" + pos_part = position_query # Fallback auf Suchbegriff + + # Versuche gängige Trennzeichen + separators = ["–", "-", "|", " at ", " bei "] + title_cleaned = title.replace("...", "").strip() # Bereinige Titel + + found_sep = False for sep in separators: if sep in title_cleaned: - parts = title_cleaned.split(sep, 1); name_part = parts[0].strip().replace(" | LinkedIn", "").replace(" - LinkedIn", "").replace(" - Profil", "").strip() + parts = title_cleaned.split(sep, 1) + name_part = parts[0].strip() + # Versuche, LinkedIn/Profil etc. aus Namen zu entfernen + name_part = name_part.replace(" | LinkedIn", "").replace(" - LinkedIn", "").replace(" - Profil", "").strip() + + # Positionsteil kann komplex sein, nehme alles nach dem Trenner potential_pos = parts[1].strip() - if crm_kurzform.lower() in potential_pos.lower(): potential_pos = potential_pos.replace(crm_kurzform, "", 1).strip() # Case-sensitive replacement? - potential_pos = potential_pos.split(" | LinkedIn")[0].split(" - LinkedIn")[0].strip(); pos_part = potential_pos if potential_pos else position_query; found_sep = True; break - if not found_sep: + # Entferne Firmenteil, wenn er dem Kurznamen ähnelt + if crm_kurzform.lower() in potential_pos.lower(): + potential_pos = potential_pos.replace(crm_kurzform, "", 1).strip() # Nur erste Instanz ersetzen + # Entferne generische Endungen + potential_pos = potential_pos.split(" | LinkedIn")[0].split(" - LinkedIn")[0].strip() + pos_part = potential_pos if potential_pos else position_query + found_sep = True + break + + if not found_sep: # Kein Trennzeichen gefunden name_part = title_cleaned.split(" | LinkedIn")[0].split(" - LinkedIn")[0].strip() - if position_query.lower() in name_part.lower(): name_part = name_part.replace(position_query, "", 1).strip() # Case-sensitive replacement? - firstname = ""; lastname = ""; name_parts = name_part.split() - if len(name_parts) > 1: firstname = name_parts[0]; lastname = " ".join(name_parts[1:]) - elif len(name_parts) == 1: firstname = name_parts[0] - if not firstname: debug_print(f"Kontakt übersprungen: Name nicht extrahiert aus '{title}'"); continue - contact_data = {"Firmenname": company_name, "CRM Kurzform": crm_kurzform, "Website": website, "Vorname": firstname, "Nachname": lastname, "Position": pos_part, "LinkedInURL": linkedin_url} - contacts.append(contact_data); # debug_print(f"Gefundener LinkedIn Kontakt: {firstname} {lastname} - {pos_part}") - debug_print(f"LinkedIn Suche '{position_query}' @ '{crm_kurzform}' -> {len(contacts)} Kontakte."); return contacts - except requests.exceptions.RequestException as e: debug_print(f"Fehler SERP LinkedIn Suche: {e}"); return [] - except Exception as e: debug_print(f"Allg. Fehler SERP LinkedIn Suche: {e}"); return [] + # Prüfe, ob der Suchbegriff im verbleibenden Namensteil ist + if position_query.lower() in name_part.lower(): + name_part = name_part.replace(position_query, "", 1).strip() # Versuche Position zu entfernen -def process_contact_research(sheet_handler): # Nutzt Config.HEADER_ROWS, COLUMN_MAP durchgängiger + # Teile Namen in Vor- und Nachname + firstname = "" + lastname = "" + name_parts = name_part.split() + if len(name_parts) > 1: + firstname = name_parts[0] + lastname = " ".join(name_parts[1:]) + elif len(name_parts) == 1: + firstname = name_parts[0] # Nur Vorname gefunden? + + if not firstname: # Wenn Name nicht extrahiert werden konnte, überspringe + debug_print(f"Kontakt übersprungen: Name konnte nicht extrahiert werden aus Titel '{title}'") + continue + + contact_data = { + "Firmenname": company_name, # Originalname für Kontext + "CRM Kurzform": crm_kurzform, + "Website": website, + "Vorname": firstname, + "Nachname": lastname, + "Position": pos_part, + "LinkedInURL": linkedin_url + } + contacts.append(contact_data) + debug_print(f"Gefundener LinkedIn Kontakt: {firstname} {lastname} - {pos_part}") + + debug_print(f"LinkedIn Suche für '{position_query}' bei '{crm_kurzform}' ergab {len(contacts)} Kontakte.") + return contacts + + except requests.exceptions.RequestException as e: + debug_print(f"Fehler bei der SERP API LinkedIn Suche: {e}") + return [] + except Exception as e: + debug_print(f"Allgemeiner Fehler bei der SERP API LinkedIn Suche: {e}") + return [] + + +# Funktion count_linkedin_contacts wurde entfernt, da search_linkedin_contacts jetzt die Liste liefert +# und len() darauf angewendet werden kann. + + +def process_contact_research(sheet_handler): """Sucht LinkedIn Kontakte und trägt sie in 'Contacts' Sheet ein.""" debug_print("Starte Contact Research (LinkedIn)...") - if not sheet_handler.load_data(): return # Lade Daten zuerst - main_sheet = sheet_handler.sheet; all_data = sheet_handler.get_all_data_with_headers(); header_rows = Config.HEADER_ROWS - if not all_data or len(all_data) <= header_rows: return - - # Indizes holen - try: - ts_col_idx = COLUMN_MAP["Contact Search Timestamp"] - name_col_idx = COLUMN_MAP["CRM Name"] - kurz_col_idx = COLUMN_MAP["CRM Kurzform"] - web_col_idx = COLUMN_MAP["CRM Website"] - sl_col_idx = COLUMN_MAP["Linked Serviceleiter gefunden"] - it_col_idx = COLUMN_MAP["Linked It-Leiter gefunden"] - mg_col_idx = COLUMN_MAP["Linked Management gefunden"] - di_col_idx = COLUMN_MAP["Linked Disponent gefunden"] - except KeyError as e: - debug_print(f"FEHLER: Benötigter Schlüssel '{e}' für Contact Research nicht in COLUMN_MAP."); return - - # Finde Startzeile basierend auf Timestamp in Spalte AM + + main_sheet = sheet_handler.sheet + all_data = sheet_handler.get_all_data_with_headers() + header_rows = 5 + + # Finde Startzeile basierend auf Timestamp in Spalte AM (Index 38) + timestamp_col_index = COLUMN_MAP["Contact Search Timestamp"] start_row_index_in_sheet = -1 for i in range(header_rows + 1, len(all_data) + 1): if i < 7: continue # Normalerweise ab Zeile 7 - row_index_in_list = i - 1; row = all_data[row_index_in_list] - if len(row) <= ts_col_idx or not row[ts_col_idx].strip(): - start_row_index_in_sheet = i; break - if start_row_index_in_sheet == -1: debug_print("Keine Zeile ohne Contact Search TS (AM, ab Z7). Skip."); return + row_index_in_list = i - 1 + row = all_data[row_index_in_list] + if len(row) <= timestamp_col_index or not row[timestamp_col_index].strip(): + start_row_index_in_sheet = i + break + + if start_row_index_in_sheet == -1: + debug_print("Keine Zeile ohne Contact Search Timestamp (Spalte AM, ab Zeile 7) gefunden. Überspringe.") + return + debug_print(f"Contact Research startet ab Zeile {start_row_index_in_sheet}.") - + # Kontakte-Blatt öffnen oder erstellen - try: contacts_sheet = sheet_handler.sheet.spreadsheet.worksheet("Contacts"); debug_print("Blatt 'Contacts' gefunden.") + try: + contacts_sheet = sheet_handler.sheet.spreadsheet.worksheet("Contacts") + debug_print("Blatt 'Contacts' gefunden.") except gspread.exceptions.WorksheetNotFound: - debug_print("Blatt 'Contacts' nicht gefunden, erstelle..."); contacts_sheet = sheet_handler.sheet.spreadsheet.add_worksheet(title="Contacts", rows="1000", cols="12") - header = ["Firmenname", "CRM Kurzform", "Website", "Geschlecht", "Vorname", "Nachname", "Position", "Suchbegriffskategorie", "E-Mail-Adresse", "LinkedIn-Link", "Timestamp"] - contacts_sheet.update(values=[header], range_name="A1:K1"); debug_print("Neues Blatt 'Contacts' erstellt.") + debug_print("Blatt 'Contacts' nicht gefunden, erstelle neu...") + contacts_sheet = sheet_handler.sheet.spreadsheet.add_worksheet(title="Contacts", rows="1000", cols="12") + header = ["Firmenname", "CRM Kurzform", "Website", "Geschlecht", "Vorname", "Nachname", "Position", + "Suchbegriffskategorie", "E-Mail-Adresse", "LinkedIn-Link", "Timestamp"] + contacts_sheet.update(values=[header], range_name="A1:K1") + # Optional: Alignment Demo hier nicht mehr aufrufen + # alignment_demo(contacts_sheet) # NICHT MEHR NÖTIG/FALSCH + debug_print("Neues Blatt 'Contacts' erstellt und Header eingetragen.") + # Positionen, nach denen gesucht wird positions_to_search = ["Serviceleiter", "Leiter Kundendienst", "IT-Leiter", "Leiter IT", "Geschäftsführer", "Vorstand", "Disponent", "Einsatzleiter"] + + # Gehe Zeilen im Hauptblatt durch + for i in range(start_row_index_in_sheet, len(all_data) + 1): + row_index_in_list = i - 1 + row = all_data[row_index_in_list] - # Nutze get_data für Iteration - data_to_process = sheet_handler.get_data() - start_data_index = start_row_index_in_sheet - header_rows - 1 + company_name = row[COLUMN_MAP["CRM Name"]] if len(row) > COLUMN_MAP["CRM Name"] else "" + crm_kurzform = row[COLUMN_MAP["CRM Kurzform"]] if len(row) > COLUMN_MAP["CRM Kurzform"] else "" + website = row[COLUMN_MAP["CRM Website"]] if len(row) > COLUMN_MAP["CRM Website"] else "" - for idx in range(start_data_index, len(data_to_process)): - row_num_in_sheet = idx + header_rows + 1 - row = data_to_process[idx] + if not all([company_name, crm_kurzform, website]): + debug_print(f"Zeile {i}: Übersprungen (fehlende CRM Daten: Name, Kurzform oder Website).") + continue - company_name = row[name_col_idx] if len(row) > name_col_idx else "" - crm_kurzform = row[kurz_col_idx] if len(row) > kurz_col_idx else "" - website = row[web_col_idx] if len(row) > web_col_idx else "" - - if not all([company_name, crm_kurzform, website]): debug_print(f"Zeile {row_num_in_sheet}: Übersprungen (fehlende CRM Daten)."); continue - - debug_print(f"Zeile {row_num_in_sheet}: Suche Kontakte für '{crm_kurzform}'...") - all_found_contacts = []; contact_counts = {pos: 0 for pos in ["Serviceleiter", "IT-Leiter", "Geschäftsführer", "Disponent"]} + debug_print(f"Zeile {i}: Suche Kontakte für '{crm_kurzform}'...") + all_found_contacts = [] + contact_counts = {pos: 0 for pos in ["Serviceleiter", "IT-Leiter", "Geschäftsführer", "Disponent"]} # Für die Zählung im Hauptblatt for position in positions_to_search: + # Suche max. 5 Kontakte pro Position, um API Calls/Kosten zu begrenzen found_contacts = search_linkedin_contacts(company_name, website, position, crm_kurzform, num_results=5) - cat = "Serviceleiter" if any(k in position.lower() for k in ["serviceleiter", "kundendienst", "einsatzleiter"]) else \ - "IT-Leiter" if any(k in position.lower() for k in ["it-leiter", "leiter it"]) else \ - "Geschäftsführer" if any(k in position.lower() for k in ["geschäftsführer", "vorstand"]) else \ - "Disponent" if "disponent" in position.lower() else None - if cat: contact_counts[cat] += len(found_contacts) - for contact in found_contacts: contact["Suchbegriffskategorie"] = position; all_found_contacts.append(contact) - time.sleep(1.5) # Pause zwischen Suchen + + # Zählung für das Hauptblatt (vereinfachte Kategorien) + if "serviceleiter" in position.lower() or "kundendienst" in position.lower() or "einsatzleiter" in position.lower(): + contact_counts["Serviceleiter"] += len(found_contacts) + elif "it-leiter" in position.lower() or "leiter it" in position.lower(): + contact_counts["IT-Leiter"] += len(found_contacts) + elif "geschäftsführer" in position.lower() or "vorstand" in position.lower(): + contact_counts["Geschäftsführer"] += len(found_contacts) + elif "disponent" in position.lower(): + contact_counts["Disponent"] += len(found_contacts) - rows_to_append = []; timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - unique_contacts = {c['LinkedInURL']: c for c in all_found_contacts}.values() + # Füge gefundene Kontakte zur Liste hinzu (mit Suchkategorie) + for contact in found_contacts: + contact["Suchbegriffskategorie"] = position + all_found_contacts.append(contact) + + time.sleep(1.5) # Kleine Pause zwischen SerpAPI-Aufrufen + + # Verarbeite gefundene Kontakte und schreibe ins Contacts-Sheet + rows_to_append = [] + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + unique_contacts = {c['LinkedInURL']: c for c in all_found_contacts}.values() # Deduplizieren basierend auf URL for contact in unique_contacts: - firstname = contact.get("Vorname", ""); lastname = contact.get("Nachname", ""); gender_value = get_gender(firstname); email = get_email_address(firstname, lastname, website) - contact_row = [contact.get("Firmenname", ""), contact.get("CRM Kurzform", ""), contact.get("Website", ""), gender_value, firstname, lastname, contact.get("Position", ""), contact.get("Suchbegriffskategorie", ""), email, contact.get("LinkedInURL", ""), timestamp] + firstname = contact.get("Vorname", "") + lastname = contact.get("Nachname", "") + gender_value = get_gender(firstname) + email = get_email_address(firstname, lastname, website) + + contact_row = [ + contact.get("Firmenname", ""), + contact.get("CRM Kurzform", ""), + contact.get("Website", ""), + gender_value, + firstname, + lastname, + contact.get("Position", ""), + contact.get("Suchbegriffskategorie", ""), + email, + contact.get("LinkedInURL", ""), + timestamp + ] rows_to_append.append(contact_row) if rows_to_append: - try: contacts_sheet.append_rows(rows_to_append, value_input_option='USER_ENTERED'); debug_print(f"Zeile {row_num_in_sheet}: {len(rows_to_append)} Kontakte zu 'Contacts' hinzugefügt.") - except Exception as e: debug_print(f"Zeile {row_num_in_sheet}: Fehler Schreiben Contacts-Sheet: {e}") + try: + # Verwende append_rows für Effizienz + contacts_sheet.append_rows(rows_to_append, value_input_option='USER_ENTERED') + debug_print(f"Zeile {i}: {len(rows_to_append)} neue Kontakte zum 'Contacts'-Blatt hinzugefügt.") + except Exception as e: + debug_print(f"Zeile {i}: Fehler beim Hinzufügen von Kontakten zum Sheet: {e}") + # Evtl. einzeln versuchen bei Fehler? + + # Aktualisiere Trefferzahlen und Timestamp im Hauptblatt (Batch Update) + main_sheet_updates = [] + main_sheet_updates.append({'range': f'AI{i}', 'values': [[str(contact_counts["Serviceleiter"])]]}) + main_sheet_updates.append({'range': f'AJ{i}', 'values': [[str(contact_counts["IT-Leiter"])]]}) + main_sheet_updates.append({'range': f'AK{i}', 'values': [[str(contact_counts["Geschäftsführer"])]]}) + main_sheet_updates.append({'range': f'AL{i}', 'values': [[str(contact_counts["Disponent"])]]}) + main_sheet_updates.append({'range': f'AM{i}', 'values': [[timestamp]]}) # Contact Search Timestamp - # Batch Update für Hauptblatt (Zähler + Timestamp) - # Verwende _get_col_letter für mehr Robustheit - sl_l = sheet_handler._get_col_letter(sl_col_idx + 1); it_l = sheet_handler._get_col_letter(it_col_idx + 1) - mg_l = sheet_handler._get_col_letter(mg_col_idx + 1); di_l = sheet_handler._get_col_letter(di_col_idx + 1) - ts_l = sheet_handler._get_col_letter(ts_col_idx + 1) - main_sheet_updates = [ - {'range': f'{sl_l}{row_num_in_sheet}', 'values': [[str(contact_counts["Serviceleiter"])]]}, - {'range': f'{it_l}{row_num_in_sheet}', 'values': [[str(contact_counts["IT-Leiter"])]]}, - {'range': f'{mg_l}{row_num_in_sheet}', 'values': [[str(contact_counts["Geschäftsführer"])]]}, - {'range': f'{di_l}{row_num_in_sheet}', 'values': [[str(contact_counts["Disponent"])]]}, - {'range': f'{ts_l}{row_num_in_sheet}', 'values': [[timestamp]]} # Contact Search Timestamp - ] sheet_handler.batch_update_cells(main_sheet_updates) - debug_print(f"Zeile {row_num_in_sheet}: Kontaktzahlen Hauptblatt aktualisiert: {contact_counts} – TS in {ts_l}.") - time.sleep(Config.RETRY_DELAY) # Pause nach Firma + debug_print(f"Zeile {i}: Kontaktzahlen im Hauptblatt aktualisiert: {contact_counts} – Timestamp in AM gesetzt.") + + # Pause nach Verarbeitung einer Firma + time.sleep(Config.RETRY_DELAY) debug_print("Contact Research abgeschlossen.") # ==================== ALIGNMENT DEMO (Hauptblatt) ==================== -def alignment_demo(sheet): # Korrigierte Version aus v1.6.5 - """Schreibt die Header-Struktur (Zeilen 1-5, bis Spalte AX) ins angegebene Sheet.""" - new_headers = [ # Spalten A bis AX - ["ReEval Flag", "CRM Name", "CRM Kurzform", "CRM Website", "CRM Ort", "CRM Beschreibung", "CRM Branche", "CRM Beschreibung Branche extern", "CRM Anzahl Techniker", "CRM Umsatz", "CRM Anzahl Mitarbeiter", "CRM Vorschlag Wiki URL", "Wiki URL", "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", "Chat Wiki Konsistenzprüfung", "Chat Begründung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", "Begründung bei Abweichung", "Chat Vorschlag Branche", "Chat Konsistenz Branche", "Chat Begründung Abweichung Branche", "Chat Prüfung FSM Relevanz", "Chat Begründung für FSM Relevanz", "Chat Schätzung Anzahl Mitarbeiter", "Chat Konsistenzprüfung Mitarbeiterzahl", "Chat Begründung Abweichung Mitarbeiterzahl", "Chat Einschätzung Anzahl Servicetechniker", "Chat Begründung Abweichung Anzahl Servicetechniker", "Chat Schätzung Umsatz", "Chat Begründung Abweichung Umsatz", "Linked Serviceleiter gefunden", "Linked It-Leiter gefunden", "Linked Management gefunden", "Linked Disponent gefunden", "Contact Search Timestamp", "Wikipedia Timestamp", "Timestamp letzte Prüfung", "Version", "Tokens", "Website Rohtext", "Website Zusammenfassung", "Website Scrape Timestamp", "Geschätzter Techniker Bucket", "Finaler Umsatz (Wiki>CRM)", "Finaler Mitarbeiter (Wiki>CRM)", "Wiki Verif. Timestamp"], - ["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", "Web Scraper", "Chat GPT API", "System", "ML Modell / Skript", "Skript (Wiki/CRM)", "Skript (Wiki/CRM)", "System"], - ["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", "Website-Content", "Website Zusammenfassung", "Timestamp", "Anzahl Servicetechniker Bucket", "Umsatz", "Anzahl Mitarbeiter", "Timestamp"], - ["Systemspalte...", "Enthält den Firmennamen...", "Manuell gepflegte Kurzform...", "Website des Unternehmens.", "Ort des Unternehmens.", "Kurze Beschreibung...", "Aktuelle Branchenzuweisung...", "Externe Branchenbeschreibung...", "Recherchierte Anzahl...", "Umsatz in Mio. € (CRM).", "Anzahl Mitarbeiter (CRM).", "Vorgeschlagene Wikipedia URL...", "Wikipedia URL...", "Erster Absatz...", "Wikipedia-Branche...", "Wikipedia-Umsatz...", "Wikipedia-Mitarbeiterzahl...", "Liste der Wikipedia-Kategorien.", "\"OK\" oder \"X\" – Ergebnis...", "Begründung bei Inkonsistenz...", "Chat-Vorschlag Wiki Artikel...", "Nicht genutzt...", "Branchenvorschlag via ChatGPT...", "Vergleich: Übereinstimmung CRM vs. ...", "Begründung bei abweichender...", "FSM-Relevanz: Bewertung...", "Begründung zur FSM-Bewertung.", "Schätzung Anzahl Mitarbeiter...", "Vergleich CRM vs. Wiki vs. ...", "Begründung bei Mitarbeiterabweichung...", "Schätzung Servicetechniker...", "Begründung bei Abweichung...", "Schätzung Umsatz via ChatGPT.", "Begründung bei Umsatzabweichung.", "Anzahl Kontakte (Serviceleiter)...", "Anzahl Kontakte (IT-Leiter)...", "Anzahl Kontakte (Management)...", "Anzahl Kontakte (Disponent)...", "Timestamp der Kontaktsuche.", "Timestamp der Wikipedia-Suche/Extraktion.", "Timestamp der ChatGPT-Bewertung / Letzte Prüfung der Zeile.", "Ausgabe der Skriptversion...", "Token-Zählung...", "Roh extrahierter Text...", "Zusammenfassung des Webseiteninhalts...", "Timestamp des letzten Website-Scrapings (AR, AS).", "Geschätzter Bucket (1-7) für Servicetechniker...", "Konsolidierter Umsatz (Mio €) nach Priorität Wiki > CRM.", "Konsolidierte Mitarbeiterzahl nach Priorität Wiki > CRM.", "Timestamp der letzten Wiki-Verifikation (Spalten S-Y)."], - ["Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Wird durch Wikipedia Scraper bereitgestellt", "Wird zunächst nicht verwendet...", "Wird u.a. zur finalen Ermittlung...", "Wird u.a. mit CRM-Umsatz...", "Wird u.a. mit CRM-Anzahl...", "Wenn Website-Daten fehlen...", "\"Es soll durch ChatGPT geprüft werden...", "\"Liegt eine Inkonsistenz...", "\"Sollte durch die Wikipedia-Suche...", "XXX derzeit nicht verwendet...", "\"ChatGPT soll anhand der vorliegenden...", "Die in Spalte CRM festgelegte...", "Weicht die von ChatGPT ermittelte...", "ChatGPT soll anhand der vorliegenden Daten prüfen...", "Die in 'Chat Begründung für FSM Relevanz'...", "Nur wenn kein Wikipedia-Eintrag...", "Entspricht die durch ChatGPT ermittelte...", "Weicht die von ChatGPT geschätzte...", "ChatGPT soll auf Basis öffentlich...", "Weicht die von ChatGPT geschätzte...", "Nur wenn kein Wikipedia-Eintrag...", "ChatGPT soll signifikante Umsatzabweichungen...", "Über SerpAPI wird zusammen...", "Über SerpAPI wird zusammen...", "Über SerpAPI wird zusammen...", "Über SerpAPI wird zusammen...", "Wenn die Kontaktsuche gestartet wird...", "Wenn die Wikipedia-Suche gestartet wird...", "Wenn die ChatGPT-Bewertung gestartet wird...", "Wird durch das System befüllt", "Wird durch tiktoken berechnet", "Wird durch Web Scraper...", "Wird durch ChatGPT API...", "Timestamp wird gesetzt, wenn Website Rohtext/Zusammenfassung geschrieben werden.", "Ergebnis der Schätzung durch das trainierte ML-Modell.", "Vom Skript berechneter Wert, priorisiert Wiki > CRM...", "Vom Skript berechneter Wert, priorisiert Wiki > CRM...", "Timestamp wird gesetzt, wenn Wiki-Verifikation (S-Y) durchgeführt wurde."] +# ==================== ALIGNMENT DEMO (Hauptblatt) ==================== +# Diese Funktion ist bereits im Code vorhanden (Zeile ~1230 in der vorherigen Version) +# Sie bleibt unverändert: +def alignment_demo(sheet): + """Schreibt die Header-Struktur (Zeilen 1-5, jetzt bis Spalte AX) ins angegebene Sheet.""" + new_headers = [ + [ # Spaltenname (Zeile 1) + "ReEval Flag", "CRM Name", "CRM Kurzform", "CRM Website", "CRM Ort", "CRM Beschreibung", "CRM Branche", "CRM Beschreibung Branche extern", "CRM Anzahl Techniker", "CRM Umsatz", "CRM Anzahl Mitarbeiter", "CRM Vorschlag Wiki URL", "Wiki URL", "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", "Chat Wiki Konsistenzprüfung", "Chat Begründung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", "Begründung bei Abweichung", "Chat Vorschlag Branche", "Chat Konsistenz Branche", "Chat Begründung Abweichung Branche", "Chat Prüfung FSM Relevanz", "Chat Begründung für FSM Relevanz", "Chat Schätzung Anzahl Mitarbeiter", "Chat Konsistenzprüfung Mitarbeiterzahl", "Chat Begründung Abweichung Mitarbeiterzahl", "Chat Einschätzung Anzahl Servicetechniker", "Chat Begründung Abweichung Anzahl Servicetechniker", "Chat Schätzung Umsatz", "Chat Begründung Abweichung Umsatz", "Linked Serviceleiter gefunden", "Linked It-Leiter gefunden", "Linked Management gefunden", "Linked Disponent gefunden", "Contact Search Timestamp", "Wikipedia Timestamp", "Timestamp letzte Prüfung", "Version", "Tokens", "Website Rohtext", "Website Zusammenfassung", + "Website Scrape Timestamp", # AT + "Geschätzter Techniker Bucket", # AU + "Finaler Umsatz (Wiki>CRM)", # AV + "Finaler Mitarbeiter (Wiki>CRM)", # AW + "Wiki Verif. Timestamp" # AX (NEU) + ], + [ # Quelle der Daten (Zeile 2) + "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", "Web Scraper", "Chat GPT API", + "System", # AT + "ML Modell / Skript", # AU + "Skript (Wiki/CRM)", # AV + "Skript (Wiki/CRM)", # AW + "System" # AX (NEU) - Timestamp vom Wiki-Verifizierungs-Prozess + ], + [ # Feldkategorie (Zeile 3) + "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", "Website-Content", "Website Zusammenfassung", + "Timestamp", # AT + "Anzahl Servicetechniker Bucket", # AU + "Umsatz", # AV + "Anzahl Mitarbeiter", # AW + "Timestamp" # AX (NEU) + ], + [ # Kurze Beschreibung (Zeile 4) + "Systemspalte...", "Enthält den Firmennamen...", "Manuell gepflegte Kurzform...", "Website des Unternehmens.", "Ort des Unternehmens.", "Kurze Beschreibung...", "Aktuelle Branchenzuweisung...", "Externe Branchenbeschreibung...", "Recherchierte Anzahl...", "Umsatz in Mio. € (CRM).", "Anzahl Mitarbeiter (CRM).", "Vorgeschlagene Wikipedia URL...", "Wikipedia URL...", "Erster Absatz...", "Wikipedia-Branche...", "Wikipedia-Umsatz...", "Wikipedia-Mitarbeiterzahl...", "Liste der Wikipedia-Kategorien.", "\"OK\" oder \"X\" – Ergebnis...", "Begründung bei Inkonsistenz...", "Chat-Vorschlag Wiki Artikel...", "Nicht genutzt...", "Branchenvorschlag via ChatGPT...", "Vergleich: Übereinstimmung CRM vs. ...", "Begründung bei abweichender...", "FSM-Relevanz: Bewertung...", "Begründung zur FSM-Bewertung.", "Schätzung Anzahl Mitarbeiter...", "Vergleich CRM vs. Wiki vs. ...", "Begründung bei Mitarbeiterabweichung...", "Schätzung Servicetechniker...", "Begründung bei Abweichung...", "Schätzung Umsatz via ChatGPT.", "Begründung bei Umsatzabweichung.", "Anzahl Kontakte (Serviceleiter)...", "Anzahl Kontakte (IT-Leiter)...", "Anzahl Kontakte (Management)...", "Anzahl Kontakte (Disponent)...", "Timestamp der Kontaktsuche.", "Timestamp der Wikipedia-Suche/Extraktion.", "Timestamp der ChatGPT-Bewertung / Letzte Prüfung der Zeile.", "Ausgabe der Skriptversion...", "Token-Zählung...", "Roh extrahierter Text...", "Zusammenfassung des Webseiteninhalts...", + "Timestamp des letzten Website-Scrapings (AR, AS).", # AT + "Geschätzter Bucket (1-7) für Servicetechniker...", # AU + "Konsolidierter Umsatz (Mio €) nach Priorität Wiki > CRM.", # AV + "Konsolidierte Mitarbeiterzahl nach Priorität Wiki > CRM.", # AW + "Timestamp der letzten Wiki-Verifikation (Spalten S-Y)." # AX (NEU) + ], + [ # Aufgabe / Funktion (Zeile 5) - Ergänzt um AX + "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Wird durch Wikipedia Scraper bereitgestellt", "Wird zunächst nicht verwendet...", "Wird u.a. zur finalen Ermittlung...", "Wird u.a. mit CRM-Umsatz...", "Wird u.a. mit CRM-Anzahl...", "Wenn Website-Daten fehlen...", "\"Es soll durch ChatGPT geprüft werden...", "\"Liegt eine Inkonsistenz...", "\"Sollte durch die Wikipedia-Suche...", "XXX derzeit nicht verwendet...", "\"ChatGPT soll anhand der vorliegenden...", "Die in Spalte CRM festgelegte...", "Weicht die von ChatGPT ermittelte...", "ChatGPT soll anhand der vorliegenden Daten prüfen...", "Die in 'Chat Begründung für FSM Relevanz'...", "Nur wenn kein Wikipedia-Eintrag...", "Entspricht die durch ChatGPT ermittelte...", "Weicht die von ChatGPT geschätzte...", "ChatGPT soll auf Basis öffentlich...", "Weicht die von ChatGPT geschätzte...", "Nur wenn kein Wikipedia-Eintrag...", "ChatGPT soll signifikante Umsatzabweichungen...", "Über SerpAPI wird zusammen...", "Über SerpAPI wird zusammen...", "Über SerpAPI wird zusammen...", "Über SerpAPI wird zusammen...", "Wenn die Kontaktsuche gestartet wird...", "Wenn die Wikipedia-Suche gestartet wird...", "Wenn die ChatGPT-Bewertung gestartet wird...", "Wird durch das System befüllt", "Wird durch tiktoken berechnet", "Wird durch Web Scraper...", "Wird durch ChatGPT API...", + "Timestamp wird gesetzt, wenn Website Rohtext/Zusammenfassung geschrieben werden.", # AT + "Ergebnis der Schätzung durch das trainierte ML-Modell.", # AU + "Vom Skript berechneter Wert, priorisiert Wiki > CRM...", # AV + "Vom Skript berechneter Wert, priorisiert Wiki > CRM...", # AW + "Timestamp wird gesetzt, wenn Wiki-Verifikation (S-Y) durchgeführt wurde." # AX (NEU) + ] ] num_cols = len(new_headers[0]) - def colnum_string(n): # Korrigierte innere Funktion + def colnum_string(n): string = "" - while n > 0: - n, remainder = divmod(n - 1, 26) - string = chr(65 + remainder) + string + while n > 0: n, remainder = divmod(n - 1, 26); string = chr(65 + remainder) + string return string end_col_letter = colnum_string(num_cols) header_range = f"A1:{end_col_letter}{len(new_headers)}" try: sheet.update(values=new_headers, range_name=header_range) - print(f"Alignment-Demo: Header in Bereich {header_range} geschrieben.") + print(f"Alignment-Demo abgeschlossen: Header in Bereich {header_range} geschrieben.") debug_print(f"Alignment-Demo: Header in Bereich {header_range} geschrieben.") except Exception as e: print(f"FEHLER beim Schreiben der Alignment-Demo Header: {e}") debug_print(f"FEHLER beim Schreiben der Alignment-Demo Header: {e}") +# ==================== DATA PROCESSOR ==================== class DataProcessor: """ Verarbeitet Daten aus dem Google Sheet, führt verschiedene Anreicherungs- @@ -1595,273 +3511,359 @@ class DataProcessor: Verarbeitet die Daten für eine einzelne Zeile, prüft Timestamps für jeden Teilbereich und stellt sicher, dass aktuelle Wiki-Daten für Branch-Eval verwendet werden. """ - debug_print(f"--- Starte Verarbeitung Zeile {row_num_in_sheet} ---") - updates = []; now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S"); any_processing_done = False + debug_print(f"--- Starte Verarbeitung für Zeile {row_num_in_sheet} ---") + updates = [] + now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + any_processing_done = False + + # Hilfsfunktion def get_cell_value(key): - idx = COLUMN_MAP.get(key); + idx = COLUMN_MAP.get(key) if idx is not None and len(row_data) > idx: return row_data[idx] return "" - company_name = get_cell_value("CRM Name"); website_url = get_cell_value("CRM Website"); original_website = website_url - crm_branche = get_cell_value("CRM Branche"); crm_beschreibung = get_cell_value("CRM Beschreibung"); crm_wiki_url = get_cell_value("CRM Vorschlag Wiki URL") - konsistenz_s = get_cell_value("Chat Wiki Konsistenzprüfung"); website_raw = get_cell_value("Website Rohtext") or "k.A."; website_summary = get_cell_value("Website Zusammenfassung") or "k.A." - wiki_data = {'url': get_cell_value("Wiki URL") or 'k.A.', 'first_paragraph': get_cell_value("Wiki Absatz") or 'k.A.', 'branche': get_cell_value("Wiki Branche") or 'k.A.', 'umsatz': get_cell_value("Wiki Umsatz") or 'k.A.', 'mitarbeiter': get_cell_value("Wiki Mitarbeiter") or 'k.A.', 'categories': get_cell_value("Wiki Kategorien") or 'k.A.'} - wiki_data_updated_in_this_run = False - # 1. Website (AT) + # Lese initiale Werte + company_name = get_cell_value("CRM Name") + website_url = get_cell_value("CRM Website"); original_website = website_url + crm_branche = get_cell_value("CRM Branche"); crm_beschreibung = get_cell_value("CRM Beschreibung") + crm_wiki_url = get_cell_value("CRM Vorschlag Wiki URL") + konsistenz_s = get_cell_value("Chat Wiki Konsistenzprüfung") + website_raw = get_cell_value("Website Rohtext") or "k.A." + website_summary = get_cell_value("Website Zusammenfassung") or "k.A." + # Initialisiere wiki_data mit Werten aus dem Sheet (Fallback) + wiki_data = { + 'url': get_cell_value("Wiki URL") or 'k.A.', 'first_paragraph': get_cell_value("Wiki Absatz") or 'k.A.', + 'branche': get_cell_value("Wiki Branche") or 'k.A.', 'umsatz': get_cell_value("Wiki Umsatz") or 'k.A.', + 'mitarbeiter': get_cell_value("Wiki Mitarbeiter") or 'k.A.', 'categories': get_cell_value("Wiki Kategorien") or 'k.A.' + } + wiki_data_updated_in_this_run = False # Flag, ob Wiki neu geparst wurde + + # --- 1. Website Handling (prüft AT) --- website_ts_needed = process_website and not get_cell_value("Website Scrape Timestamp").strip() if website_ts_needed: - any_processing_done = True; debug_print(f"Z{row_num_in_sheet}: Website Verarbeitung...") + any_processing_done = True; debug_print(f"Zeile {row_num_in_sheet}: Starte Website Verarbeitung...") + # --- Lookup & Scraping --- if not website_url or website_url.strip().lower() == "k.a.": new_website = serp_website_lookup(company_name) if new_website != "k.A.": website_url = new_website; - # Use helper function to get column letter - if website_url != original_website: updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["CRM Website"] + 1)}{row_num_in_sheet}', 'values': [[website_url]]}) + if website_url != original_website: updates.append({'range': f'D{row_num_in_sheet}', 'values': [[website_url]]}) if website_url and website_url.strip().lower() != "k.a.": new_website_raw = get_website_raw(website_url); new_website_summary = summarize_website_content(new_website_raw) - if new_website_raw != website_raw: updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [[new_website_raw]]}); website_raw = new_website_raw - if new_website_summary != website_summary: updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [[new_website_summary]]}); website_summary = new_website_summary + if new_website_raw != website_raw: updates.append({'range': f'AR{row_num_in_sheet}', 'values': [[new_website_raw]]}); website_raw = new_website_raw + if new_website_summary != website_summary: updates.append({'range': f'AS{row_num_in_sheet}', 'values': [[new_website_summary]]}); website_summary = new_website_summary else: - if website_raw != "k.A.": updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [['k.A.']]}) - if website_summary != "k.A.": updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [['k.A.']]}) + if website_raw != "k.A.": updates.append({'range': f'AR{row_num_in_sheet}', 'values': [['k.A.']]}) + if website_summary != "k.A.": updates.append({'range': f'AS{row_num_in_sheet}', 'values': [['k.A.']]}) website_raw, website_summary = "k.A.", "k.A." - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Scrape Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) - elif process_website: pass # debug_print(f"Z{row_num_in_sheet}: Skip Website (AT vorhanden).") + updates.append({'range': f'AT{row_num_in_sheet}', 'values': [[now_timestamp]]}) + elif process_website: debug_print(f"Zeile {row_num_in_sheet}: Überspringe Website (AT vorhanden).") - # 2. Wikipedia (AN oder S='X (URL Copied)') - wiki_ts_an_missing = not get_cell_value("Wikipedia Timestamp").strip(); status_s_indicates_reparse = konsistenz_s.strip().upper() == "X (URL COPIED)" + # --- 2. Wikipedia Handling (prüft AN oder S='X (URL Copied)') --- + wiki_ts_an_missing = not get_cell_value("Wikipedia Timestamp").strip() + status_s_indicates_reparse = konsistenz_s.strip().upper() == "X (URL COPIED)" reparse_wiki_needed = process_wiki and (wiki_ts_an_missing or status_s_indicates_reparse) + if reparse_wiki_needed: - any_processing_done = True; debug_print(f"Z{row_num_in_sheet}: Wikipedia Verarbeitung (AN fehlt? {wiki_ts_an_missing}, S='X(Copied)'? {status_s_indicates_reparse})...") - new_wiki_data_extracted = None; url_to_parse = get_cell_value("Wiki URL").strip() + any_processing_done = True; debug_print(f"Zeile {row_num_in_sheet}: Starte Wikipedia Verarbeitung (AN fehlt? {wiki_ts_an_missing}, S='X(Copied)'? {status_s_indicates_reparse})...") + new_wiki_data_extracted = None + + # --- Priorisiere URL aus Spalte M --- + url_to_parse = get_cell_value("Wiki URL").strip() # Holt die URL, die ggf. von update_wiki kopiert wurde if url_to_parse and url_to_parse.lower() not in ["k.a.", "kein artikel gefunden"] and url_to_parse.lower().startswith("http"): - debug_print(f" -> Nutze URL aus M: {url_to_parse}") + debug_print(f" -> Nutze vorhandene URL aus Spalte M: {url_to_parse}") new_wiki_data_extracted = self.wiki_scraper.extract_company_data(url_to_parse) else: - debug_print(f" -> M ('{url_to_parse}') ungültig/leer. Starte Suche..."); article_page = None + debug_print(f" -> Spalte M ('{url_to_parse}') ungültig/leer. Starte Wiki-Suche...") valid_crm_wiki_url = crm_wiki_url if crm_wiki_url and crm_wiki_url.strip() not in ["", "k.A."] else None + article_page = None # Initialisiere article_page current_website_for_validation = website_url if website_url and website_url != 'k.A.' else original_website + + # --- KORREKTE EINRÜCKUNG HIER --- if valid_crm_wiki_url: debug_print(f" -> Prüfe CRM Vorschlag L: {valid_crm_wiki_url}") - try: # Use try-except for page loading - # Get page title from URL for wikipedia.page() - page_title = unquote(valid_crm_wiki_url.split('/wiki/', 1)[-1]).replace('_', ' ') - page = wikipedia.page(page_title, auto_suggest=False, preload=False) # Use preload=False initially - _ = page.content # Access content to trigger load, may raise exception - except Exception as page_load_error: - debug_print(f" -> Fehler beim Laden der Seite für CRM Vorschlag '{valid_crm_wiki_url}': {page_load_error}") - page = None - if page and self.wiki_scraper._validate_article(page, company_name, current_website_for_validation): article_page = page - else: debug_print(f" -> CRM Vorschlag L nicht validiert. Starte Suche..."); article_page = self.wiki_scraper.search_company_article(company_name, current_website_for_validation) - else: debug_print(f" -> Kein CRM Vorschlag L. Starte Suche..."); article_page = self.wiki_scraper.search_company_article(company_name, current_website_for_validation) - if article_page: debug_print(f" -> Artikel durch Suche: {article_page.url}"); new_wiki_data_extracted = self.wiki_scraper.extract_company_data(article_page.url) - else: debug_print(f" -> Kein Artikel durch Suche."); new_wiki_data_extracted = {'url': 'Kein Artikel gefunden', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} + page = self.wiki_scraper._fetch_page_content(valid_crm_wiki_url.split('/')[-1]) + if page and self.wiki_scraper._validate_article(page, company_name, current_website_for_validation): + article_page = page + else: + debug_print(f" -> CRM Vorschlag L nicht validiert. Starte Suche...") + # Wenn CRM-Vorschlag nicht validiert, Suche trotzdem starten + article_page = self.wiki_scraper.search_company_article(company_name, current_website_for_validation) + else: + # --- DIESE ZEILE IST JETZT KORREKT EINGERÜCKT UNTER DEM ELSE --- + debug_print(f" -> Kein CRM Vorschlag L. Starte Suche...") + article_page = self.wiki_scraper.search_company_article(company_name, current_website_for_validation) + # --- ENDE KORREKTE EINRÜCKUNG --- + + if article_page: + debug_print(f" -> Artikel gefunden durch Suche: {article_page.url}") + new_wiki_data_extracted = self.wiki_scraper.extract_company_data(article_page.url) + else: + debug_print(f" -> Kein passender Wikipedia Artikel durch Suche gefunden.") + new_wiki_data_extracted = {'url': 'Kein Artikel gefunden', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} + + # --- WICHTIG: Überschreibe wiki_data mit den NEUEN Ergebnissen --- if new_wiki_data_extracted: - wiki_data = new_wiki_data_extracted; wiki_data_updated_in_this_run = True - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki URL"] + 1)}{row_num_in_sheet}', 'values': [[wiki_data.get('url', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Absatz"] + 1)}{row_num_in_sheet}', 'values': [[wiki_data.get('first_paragraph', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Branche"] + 1)}{row_num_in_sheet}', 'values': [[wiki_data.get('branche', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[wiki_data.get('umsatz', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[wiki_data.get('mitarbeiter', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Kategorien"] + 1)}{row_num_in_sheet}', 'values': [[wiki_data.get('categories', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wikipedia Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) + wiki_data = new_wiki_data_extracted # <-- Hier werden die Daten für den Branch-Teil aktualisiert! + wiki_data_updated_in_this_run = True # Setze Flag + # Füge Updates für M-R und AN hinzu + updates.append({'range': f'M{row_num_in_sheet}', 'values': [[wiki_data.get('url', 'k.A.')]]}) + updates.append({'range': f'N{row_num_in_sheet}', 'values': [[wiki_data.get('first_paragraph', 'k.A.')]]}) + # ... (Updates für O, P, Q, R) ... + updates.append({'range': f'O{row_num_in_sheet}', 'values': [[wiki_data.get('branche', 'k.A.')]]}) + updates.append({'range': f'P{row_num_in_sheet}', 'values': [[wiki_data.get('umsatz', 'k.A.')]]}) + updates.append({'range': f'Q{row_num_in_sheet}', 'values': [[wiki_data.get('mitarbeiter', 'k.A.')]]}) + updates.append({'range': f'R{row_num_in_sheet}', 'values': [[wiki_data.get('categories', 'k.A.')]]}) + updates.append({'range': f'AN{row_num_in_sheet}', 'values': [[now_timestamp]]}) # Setze AN neu + + # Wenn der Trigger "X (URL Copied)" war, setze S zurück if status_s_indicates_reparse: s_idx = COLUMN_MAP.get("Chat Wiki Konsistenzprüfung") - if s_idx is not None: s_let = self.sheet_handler._get_col_letter(s_idx + 1); updates.append({'range': f'{s_let}{row_num_in_sheet}', 'values': [["?"]]}); debug_print(f" -> Status S -> '?' für Re-Verifikation.") - else: debug_print(f" -> FEHLER: Keine neuen Wiki-Daten extrahiert.") - elif process_wiki: pass # debug_print(f"Z{row_num_in_sheet}: Skip Wikipedia (AN vorhanden, S != 'X Copied').") + if s_idx is not None: + s_let = self.sheet_handler._get_col_letter(s_idx + 1) + updates.append({'range': f'{s_let}{row_num_in_sheet}', 'values': [["?"]]}) + debug_print(f" -> Status S zurückgesetzt auf '?' für erneute Verifikation.") + else: + debug_print(f" -> FEHLER: Keine neuen Wiki-Daten extrahiert.") + # wiki_data behält die alten/default Werte - # 3. ChatGPT Eval (AO oder Wiki neu) + elif process_wiki: + debug_print(f"Zeile {row_num_in_sheet}: Überspringe Wikipedia Verarbeitung (AN vorhanden UND S != 'X (URL Copied)').") + + # --- 3. ChatGPT Evaluationen (Branch etc.) --- + # Trigger: AO fehlt ODER Wiki wurde in DIESEM Lauf neu geparsed chat_ts_ao_missing = not get_cell_value("Timestamp letzte Prüfung").strip() - run_chat_eval = process_chatgpt and (chat_ts_ao_missing or wiki_data_updated_in_this_run) + run_chat_eval = process_chatgpt and (chat_ts_ao_missing or wiki_data_updated_in_this_run) # <-- Nutze das neue Flag + if run_chat_eval: - debug_print(f"Z{row_num_in_sheet}: ChatGPT Eval (AO fehlt? {chat_ts_ao_missing}, Wiki neu? {wiki_data_updated_in_this_run})...") + debug_print(f"Zeile {row_num_in_sheet}: Starte ChatGPT Evaluationen (Grund: AO fehlt? {chat_ts_ao_missing}, Wiki gerade aktualisiert? {wiki_data_updated_in_this_run})...") any_processing_done = True - # 3.1 Branch Eval - branch_result = evaluate_branche_chatgpt(crm_branche, crm_beschreibung, wiki_data.get('branche', 'k.A.'), wiki_data.get('categories', 'k.A.'), website_summary) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('branch', 'Fehler')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenz Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('consistency', 'Fehler')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begründung Abweichung Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('justification', 'Fehler')]]}) - # ... (Weitere ChatGPT Evals hier)... - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Prüfung"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) - elif process_chatgpt: pass # debug_print(f"Z{row_num_in_sheet}: Skip ChatGPT Eval (AO vorhanden, Wiki nicht neu).") - # 4. Abschluss - if any_processing_done: updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Version"] + 1)}{row_num_in_sheet}', 'values': [[Config.VERSION]]}) + # 3.1 Branchenevaluierung (Nutzt IMMER die aktuelle 'wiki_data' Variable) + branch_result = evaluate_branche_chatgpt( + crm_branche, crm_beschreibung, + wiki_data.get('branche', 'k.A.'), # Nimmt die potenziell neuen Daten + wiki_data.get('categories', 'k.A.'),# Nimmt die potenziell neuen Daten + website_summary + ) + updates.append({'range': f'W{row_num_in_sheet}', 'values': [[branch_result.get('branch', 'Fehler')]]}) + updates.append({'range': f'X{row_num_in_sheet}', 'values': [[branch_result.get('consistency', 'Fehler')]]}) + updates.append({'range': f'Y{row_num_in_sheet}', 'values': [[branch_result.get('justification', 'Fehler')]]}) - # 5. Batch Update + # ... (Hier weitere ChatGPT Evaluationen) ... + + # Setze Timestamp letzte Prüfung (AO) + updates.append({'range': f'AO{row_num_in_sheet}', 'values': [[now_timestamp]]}) + + elif process_chatgpt: + debug_print(f"Zeile {row_num_in_sheet}: Überspringe ChatGPT Evaluationen (AO vorhanden UND Wiki nicht gerade aktualisiert).") + + # --- 4. Abschließende Updates --- + if any_processing_done: + updates.append({'range': f'AP{row_num_in_sheet}', 'values': [[Config.VERSION]]}) + + # --- 5. Batch Update --- if updates: success = self.sheet_handler.batch_update_cells(updates) - if success: debug_print(f"Z{row_num_in_sheet}: Batch-Update OK ({len(updates)} Zellen/Bereiche).") - else: debug_print(f"Z{row_num_in_sheet}: FEHLER Batch-Update.") - # else: debug_print(f"Z{row_num_in_sheet}: Keine Updates.") - debug_print(f"--- Verarbeitung Zeile {row_num_in_sheet} abgeschlossen ---") - time.sleep(0.05) # Minimale Pause + if success: debug_print(f"Zeile {row_num_in_sheet}: Batch-Update erfolgreich ({len(updates)} Zellen/Bereiche).") + else: debug_print(f"Zeile {row_num_in_sheet}: FEHLER beim Batch-Update.") + else: + debug_print(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben.") + + debug_print(f"--- Verarbeitung für Zeile {row_num_in_sheet} abgeschlossen ---") + time.sleep(max(0.1, Config.RETRY_DELAY / 25)) # Noch kürzere Pause hier + + + def process_rows_sequentially(self, start_row_index, num_rows_to_process, process_wiki=True, process_chatgpt=True, process_website=True): + """ Verarbeitet Zeilen sequentiell ab einem Startindex. """ + data_rows = self.sheet_handler.get_data() # Daten ohne Header + header_rows = 5 + + if start_row_index >= len(data_rows): + debug_print("Startindex liegt hinter der letzten Datenzeile. Keine Verarbeitung.") + return + + # Berechne den Endindex sicher + end_row_index = min(start_row_index + num_rows_to_process, len(data_rows)) + actual_rows_to_process = end_row_index - start_row_index + + if actual_rows_to_process <= 0: + debug_print("Keine Zeilen zur sequenziellen Verarbeitung übrig.") + return + + debug_print(f"Verarbeite {actual_rows_to_process} Zeilen sequenziell (Daten-Index {start_row_index} bis {end_row_index - 1})...") - def process_rows_sequentially(self, start_row_index, num_rows_to_process, process_wiki=True, process_chatgpt=True, process_website=True): # unverändert - data_rows = self.sheet_handler.get_data(); header_rows = Config.HEADER_ROWS - if start_row_index >= len(data_rows): debug_print("Startindex hinter Datenende."); return - end_row_index = min(start_row_index + num_rows_to_process, len(data_rows)); actual_rows_to_process = end_row_index - start_row_index - if actual_rows_to_process <= 0: debug_print("Keine Zeilen sequenziell zu verarbeiten."); return - debug_print(f"Verarbeite {actual_rows_to_process} Zeilen sequenziell (Daten-Idx {start_row_index} bis {end_row_index - 1})...") for i in range(start_row_index, end_row_index): - if i >= len(data_rows): debug_print(f"WARNUNG: Index {i} > Datenlänge ({len(data_rows)})."); break - row_data = data_rows[i]; row_num_in_sheet = i + header_rows + 1 - try: # Add try-except around single row processing - self._process_single_row(row_num_in_sheet, row_data, process_wiki, process_chatgpt, process_website) - except Exception as e: - debug_print(f"!! FEHLER in _process_single_row für Zeile {row_num_in_sheet}: {e}") - debug_print(traceback.format_exc()) # Print traceback for detailed error info + if i >= len(data_rows): # Zusätzliche Sicherheitsprüfung + debug_print(f"WARNUNG: Index {i} überschreitet Datenlänge ({len(data_rows)}). Breche Schleife ab.") + break + row_data = data_rows[i] + row_num_in_sheet = i + header_rows + 1 # 1-basierter Sheet-Index - def process_reevaluation_rows(self, row_limit=None, clear_flag=True): # unverändert - debug_print(f"Starte Re-Eval Modus (A = 'x'). Max: {row_limit if row_limit is not None else 'Alle'}") + # Rufe die detaillierte Verarbeitungsmethode auf + self._process_single_row(row_num_in_sheet, row_data, process_wiki, process_chatgpt, process_website) + + + def process_reevaluation_rows(self, row_limit=None, clear_flag=True): + """ + Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind. + Ruft _process_single_row für jede dieser Zeilen auf. + Verarbeitet maximal row_limit Zeilen. + Löscht optional das 'x'-Flag nach erfolgreicher Verarbeitung. + """ + debug_print(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}") + + # Lade Daten frisch, um aktuelle Flags zu sehen if not self.sheet_handler.load_data(): return all_data = self.sheet_handler.get_all_data_with_headers() - if not all_data or len(all_data) <= Config.HEADER_ROWS: return - header_rows = Config.HEADER_ROWS; data_rows = all_data[header_rows:] + if not all_data or len(all_data) <= 5: return + header_rows = 5 + data_rows = all_data[header_rows:] + reeval_col_idx = COLUMN_MAP.get("ReEval Flag") if reeval_col_idx is None: return debug_print("FEHLER: 'ReEval Flag' nicht in COLUMN_MAP.") + rows_to_process = [] + # Finde zuerst alle Kandidaten for idx, row in enumerate(data_rows): if len(row) > reeval_col_idx and row[reeval_col_idx].strip().lower() == "x": - rows_to_process.append({'row_num': idx + header_rows + 1, 'data': row}) - debug_print(f"{len(rows_to_process)} Zeilen mit ReEval-Flag gefunden.") - processed_count = 0; updates_clear_flag = [] + row_num_in_sheet = idx + header_rows + 1 + rows_to_process.append({'row_num': row_num_in_sheet, 'data': row}) + + debug_print(f"{len(rows_to_process)} Zeilen mit ReEval-Flag 'x' gefunden.") + + processed_count = 0 + updates_clear_flag = [] + + # Verarbeite die gefundenen Kandidaten bis zum Limit for task in rows_to_process: - if row_limit is not None and processed_count >= row_limit: debug_print(f"Limit ({row_limit}) erreicht."); break - row_num = task['row_num']; row_data = task['data']; debug_print(f"--- Re-Evaluiere Z{row_num} ---") + if row_limit is not None and processed_count >= row_limit: + debug_print(f"Zeilenlimit ({row_limit}) erreicht. Breche Re-Evaluation ab.") + break + + row_num = task['row_num'] + row_data = task['data'] + debug_print(f"--- Re-Evaluiere Zeile {row_num} ---") try: - # Ensure all processes run for re-evaluation + # Führe volle Verarbeitung für diese Zeile durch + # _process_single_row prüft intern Timestamps AN, AT, AO self._process_single_row(row_num, row_data, process_wiki=True, process_chatgpt=True, process_website=True) processed_count += 1 + + # Optional: Flag nach Verarbeitung löschen if clear_flag: flag_col_letter = self.sheet_handler._get_col_letter(reeval_col_idx + 1) updates_clear_flag.append({'range': f'{flag_col_letter}{row_num}', 'values': [['']]}) + except Exception as e_proc: - debug_print(f"FEHLER Re-Eval Z{row_num}: {e_proc}") - debug_print(traceback.format_exc()) # Print traceback - # Do not clear flag on error to allow retry + debug_print(f"FEHLER bei Re-Evaluation von Zeile {row_num}: {e_proc}") + # Flag hier nicht löschen, damit es beim nächsten Mal versucht wird + # Lösche Flags am Ende gebündelt if clear_flag and updates_clear_flag: - debug_print(f"Lösche ReEval-Flags für {len(updates_clear_flag)} erfolgreich verarbeitete Zeilen...") + debug_print(f"Lösche ReEval-Flags für {len(updates_clear_flag)} verarbeitete Zeilen...") success = self.sheet_handler.batch_update_cells(updates_clear_flag) - if not success: debug_print("FEHLER Löschen ReEval-Flags.") - debug_print(f"Re-Eval beendet. {processed_count} verarbeitet (Limit: {row_limit}).") + if not success: debug_print("FEHLER beim Löschen der ReEval-Flags.") - def process_website_details_for_marked_rows(self): # unverändert - debug_print("Starte Modus 23: Website Detail Extraction (A='x').") - data_rows = self.sheet_handler.get_data(); header_rows = Config.HEADER_ROWS; rows_processed = 0 - reeval_col_idx = COLUMN_MAP.get("ReEval Flag"); website_col_idx = COLUMN_MAP.get("CRM Website") - # Decide where to write details. AR (43) is Rohtext. Maybe new column needed? Using AR for now. - details_col_key = "Website Rohtext" - details_col_idx = COLUMN_MAP.get(details_col_key) + debug_print(f"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Limit: {row_limit}).") - if reeval_col_idx is None or website_col_idx is None or details_col_idx is None: - debug_print(f"FEHLER: Benötigte Spalten für Modus 23 nicht in COLUMN_MAP gefunden (ReEval, CRM Website, {details_col_key}).") - return - details_col_letter = self.sheet_handler._get_col_letter(details_col_idx + 1) + def process_website_details_for_marked_rows(self): + """ Neuer Modus 23: Extrahiert Website-Details für markierte Zeilen. """ + debug_print("Starte Modus 23: Website Detail Extraction für Zeilen mit 'x' in Spalte A.") + data_rows = self.sheet_handler.get_data() + header_rows = 5 + rows_processed = 0 + reeval_col_idx = COLUMN_MAP.get("ReEval Flag") + website_col_idx = COLUMN_MAP.get("CRM Website") + details_col = f"AR" # Spalte AR für Details? War vorher Rohtext. Ggf. neue Spalte? + + if reeval_col_idx is None or website_col_idx is None: + debug_print("FEHLER: Benötigte Spalten für Modus 23 nicht in COLUMN_MAP gefunden.") + return for i, row in enumerate(data_rows): row_num_in_sheet = i + header_rows + 1 if len(row) > reeval_col_idx and row[reeval_col_idx].strip().lower() == "x": website_url = row[website_col_idx] if len(row) > website_col_idx else "" - if not website_url or website_url.strip().lower() == "k.a.": debug_print(f"Z{row_num_in_sheet}: Keine Website (D), skip."); continue - debug_print(f"Z{row_num_in_sheet}: Extrahiere Details von {website_url}...") - # Define or import scrape_website_details function - # def scrape_website_details(url): return f"Details placeholder for {url}" - try: - # Placeholder for the actual detail scraping function - details = f"Details placeholder for {website_url}" # scrape_website_details(website_url) - except Exception as e_detail: - debug_print(f"Fehler beim Extrahieren der Details für {website_url}: {e_detail}") - details = "k.A. (Detail Extraktion Fehler)" + if not website_url or website_url.strip().lower() == "k.a.": + debug_print(f"Zeile {row_num_in_sheet}: Keine gültige Website in Spalte D vorhanden, überspringe.") + continue - update_data = [{'range': f'{details_col_letter}{row_num_in_sheet}', 'values': [[details]]}] - # Optionally set a timestamp (e.g., in AT if it's related) - # ts_col_letter = self.sheet_handler._get_col_letter(COLUMN_MAP["Website Scrape Timestamp"] + 1) - # update_data.append({'range': f'{ts_col_letter}{row_num_in_sheet}', 'values': [[datetime.now().strftime("%Y-%m-%d %H:%M:%S")]]}) + debug_print(f"Zeile {row_num_in_sheet}: Extrahiere Website Details von {website_url}...") + details = scrape_website_details(website_url) # Annahme: Diese Funktion existiert + + # Speichere das Detail-Ergebnis in Spalte AR (Index 43) + update_data = [{'range': f'{details_col}{row_num_in_sheet}', 'values': [[details]]}] + # Optional: Timestamp setzen? In AT? + # update_data.append({'range': f'AT{row_num_in_sheet}', 'values': [[datetime.now().strftime("%Y-%m-%d %H:%M:%S")]]}) self.sheet_handler.batch_update_cells(update_data) - debug_print(f"Z{row_num_in_sheet}: Details in {details_col_letter} geschrieben.") - rows_processed += 1; time.sleep(Config.RETRY_DELAY) # Pause between detail scrapes if needed - debug_print(f"Modus 23 beendet. {rows_processed} verarbeitet.") + debug_print(f"Zeile {row_num_in_sheet}: Website Detail Extraction abgeschlossen, Ergebnis in Spalte {details_col} geschrieben.") + rows_processed += 1 + time.sleep(Config.RETRY_DELAY) - def process_serp_website_lookup_for_empty(self): # unverändert - debug_print("Starte Modus 22: SERP Website Lookup (D leer).") - # Ensure data is loaded before accessing get_data - if not self.sheet_handler.load_data(): - debug_print("FEHLER: Laden der Daten für Modus 22 fehlgeschlagen.") + debug_print(f"Modus 23 abgeschlossen. {rows_processed} Zeilen verarbeitet.") + + + def process_serp_website_lookup_for_empty(self): + """ Neuer Modus 22: Füllt fehlende Websites via SERP API. """ + debug_print("Starte Modus 22: SERP API Website Lookup für leere Zellen in Spalte D.") + data_rows = self.sheet_handler.get_data() + header_rows = 5 + rows_processed = 0 + website_col_idx = COLUMN_MAP.get("CRM Website") + name_col_idx = COLUMN_MAP.get("CRM Name") + + if website_col_idx is None or name_col_idx is None: + debug_print("FEHLER: Benötigte Spalten für Modus 22 nicht in COLUMN_MAP gefunden.") return - data_rows = self.sheet_handler.get_data(); header_rows = Config.HEADER_ROWS; rows_processed = 0 - website_col_idx = COLUMN_MAP.get("CRM Website"); name_col_idx = COLUMN_MAP.get("CRM Name") - if website_col_idx is None or name_col_idx is None: debug_print("FEHLER: Spalten Modus 22 fehlen."); return - website_col_letter = self.sheet_handler._get_col_letter(website_col_idx + 1) # Get column letter - for i, row in enumerate(data_rows): row_num_in_sheet = i + header_rows + 1 - current_website = "" - # Check if row has enough columns before accessing index - if len(row) > website_col_idx: - current_website = row[website_col_idx] + current_website = row[website_col_idx] if len(row) > website_col_idx else "" if not current_website or current_website.strip().lower() == "k.a.": - company_name = "" - if len(row) > name_col_idx: - company_name = row[name_col_idx] + company_name = row[name_col_idx] if len(row) > name_col_idx else "" + if not company_name: + debug_print(f"Zeile {row_num_in_sheet}: Übersprungen (kein Firmenname für Lookup).") + continue - if not company_name: debug_print(f"Z{row_num_in_sheet}: Skip (kein Firmenname)."); continue - - debug_print(f"Z{row_num_in_sheet}: Suche Website für '{company_name}'...") - new_website = serp_website_lookup(company_name) # Assumes serp_website_lookup is defined and works + debug_print(f"Zeile {row_num_in_sheet}: Suche Website für '{company_name}'...") + new_website = serp_website_lookup(company_name) # Annahme: Diese Funktion existiert if new_website != "k.A.": - update_data = [{'range': f'{website_col_letter}{row_num_in_sheet}', 'values': [[new_website]]}] + update_data = [{'range': f'D{row_num_in_sheet}', 'values': [[new_website]]}] + # Optional: Timestamp setzen? Wo? AT? self.sheet_handler.batch_update_cells(update_data) - debug_print(f"Z{row_num_in_sheet}: Website '{new_website}' in {website_col_letter} eingetragen.") + debug_print(f"Zeile {row_num_in_sheet}: Neue Website '{new_website}' gefunden und in Spalte D eingetragen.") rows_processed += 1 else: - debug_print(f"Z{row_num_in_sheet}: Keine Website gefunden.") - # Optional: Mark failure explicitly - # update_data = [{'range': f'{website_col_letter}{row_num_in_sheet}', 'values': [['k.A. (SERP failed)']]] - # self.sheet_handler.batch_update_cells(update_data) + debug_print(f"Zeile {row_num_in_sheet}: Keine Website gefunden.") - time.sleep(Config.RETRY_DELAY) # Pause between SERP API Calls - - debug_print(f"Modus 22 beendet. {rows_processed} Websites ergänzt.") + time.sleep(Config.RETRY_DELAY) + debug_print(f"Modus 22 abgeschlossen. {rows_processed} Websites ergänzt.") # --- NEU: Datenvorbereitung als Methode der Klasse --- def prepare_data_for_modeling(self): """ Lädt Daten aus dem Google Sheet über den sheet_handler, - bereitet sie für das Decision Tree Modell vor. (Implementierung aus v1.6.5) + bereitet sie für das Decision Tree Modell vor. (Implementierung siehe vorherige Antwort) """ debug_print("Starte Datenvorbereitung für Modellierung...") + try: # --- 1. Daten laden & Spalten auswählen --- if not self.sheet_handler or not self.sheet_handler.sheet_values: - # Attempt to load data if not already loaded - if not self.sheet_handler.load_data(): - debug_print("Fehler: Sheet Handler nicht initialisiert oder Daten konnten nicht geladen werden.") - return None - # Check again after loading - if not self.sheet_handler.sheet_values: - debug_print("Fehler: Keine Daten nach erneutem Laden.") - return None + debug_print("Fehler: Sheet Handler nicht initialisiert oder keine Daten geladen.") + return None - - all_data = self.sheet_handler.sheet_values # Use the loaded data - if len(all_data) <= Config.HEADER_ROWS: # Verwende Config.HEADER_ROWS - debug_print(f"Fehler: Nicht genügend Datenzeilen ({len(all_data)}) im Sheet gefunden (benötigt > {Config.HEADER_ROWS}).") + all_data = self.sheet_handler.sheet_values + if len(all_data) <= 5: + debug_print("Fehler: Nicht genügend Datenzeilen im Sheet gefunden.") return None headers = all_data[0] - data_rows = all_data[Config.HEADER_ROWS:] # Verwende Config.HEADER_ROWS - - # Check if headers is a list and not empty - if not isinstance(headers, list) or not headers: - debug_print("FEHLER: Header-Zeile ist ungültig oder leer.") - return None + data_rows = all_data[5:] df = pd.DataFrame(data_rows, columns=headers) debug_print(f"DataFrame erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.") @@ -1869,123 +3871,77 @@ class DataProcessor: # Finde die tatsächlichen Spaltennamen anhand der COLUMN_MAP col_indices = {} tech_col_key = "CRM Anzahl Techniker" # <- ANPASSEN, FALLS NÖTIG - required_map_keys = ["CRM Name", "CRM Branche", "CRM Umsatz", "Wiki Umsatz", - "CRM Anzahl Mitarbeiter", "Wiki Mitarbeiter", tech_col_key] - actual_col_names = {} - missing_keys = [] - - for key in required_map_keys: - col_idx = COLUMN_MAP.get(key) - if col_idx is None: - missing_keys.append(key) - continue - try: - # Get actual column name from header row using the index - actual_name = headers[col_idx] - actual_col_names[key] = actual_name - except IndexError: - debug_print(f"FEHLER: Index {col_idx} für Key '{key}' ist außerhalb der Header-Grenzen (Länge {len(headers)}).") - missing_keys.append(f"{key} (Index Error)") - - if missing_keys: - debug_print(f"FEHLER: Folgende Keys/Spalten fehlen in COLUMN_MAP oder Header: {missing_keys}") - debug_print(f"Verfügbare Header: {headers}") + try: + col_indices = { + "name": all_data[0][COLUMN_MAP["CRM Name"]], + "branche": all_data[0][COLUMN_MAP["CRM Branche"]], + "umsatz_crm": all_data[0][COLUMN_MAP["CRM Umsatz"]], + "umsatz_wiki": all_data[0][COLUMN_MAP["Wiki Umsatz"]], + "ma_crm": all_data[0][COLUMN_MAP["CRM Anzahl Mitarbeiter"]], + "ma_wiki": all_data[0][COLUMN_MAP["Wiki Mitarbeiter"]], + "techniker": all_data[0][COLUMN_MAP[tech_col_key]] + } + cols_to_select = list(col_indices.values()) + except KeyError as e: + debug_print(f"FEHLER: Konnte Mapping für Schlüssel '{e}' in COLUMN_MAP nicht finden oder Spalte nicht im Header.") + return None + except IndexError as e: + debug_print(f"FEHLER: Spaltenindex aus COLUMN_MAP ist außerhalb der Grenzen der Header-Zeile: {e}") return None - # Select using actual column names - cols_to_select = list(actual_col_names.values()) df_subset = df[cols_to_select].copy() - - # Rename columns to shorter keys for easier access - rename_map_inv = {v: k for k, v in actual_col_names.items()} # Map actual name back to key - df_subset.rename(columns=rename_map_inv, inplace=True) + rename_map = {v: k for k, v in col_indices.items()} + df_subset.rename(columns=rename_map, inplace=True) debug_print(f"Benötigte Spalten ausgewählt und umbenannt: {list(df_subset.columns)}") - # --- 2. Features konsolidieren --- - def get_valid_numeric_ml(value_str, final_col): # Separate helper for ML prep - if pd.isna(value_str) or value_str == '': return np.nan - text = str(value_str).strip() - text = re.sub(r'(?i)^(ca\.?|circa|über|unter|rund|etwa|mehr als|weniger als|bis zu)\s*', '', text) - text = re.sub(r'[€$£¥]', '', text).strip() - if '.' in text and ',' in text: text = text.replace('.', '').replace(',', '.') - elif ',' in text: text = text.replace(',', '.') - if '.' in text and text.count('.') > 1: text = text.replace('.', '') - - multiplier = 1.0; text_lower = text.lower(); num_part = text - if "mrd" in text_lower or "milliarden" in text_lower or "billion" in text_lower: - multiplier = 1000.0; num_part = re.sub(r'(?i)\s*(mrd\.?|milliarden|billion)\b.*', '', text).strip() - elif "mio" in text_lower or "millionen" in text_lower or "mill\." in text_lower: - multiplier = 1.0; num_part = re.sub(r'(?i)\s*(mio\.?|millionen|mill\.?)\b.*', '', text).strip() - elif "tsd" in text_lower or "tausend" in text_lower: - # Determine if Umsatz or Mitarbeiter based on final_col name - is_umsatz_target = 'Umsatz' in final_col - multiplier = 0.001 if is_umsatz_target else 1000.0 - num_part = re.sub(r'(?i)\s*(tsd\.?|tausend)\b.*', '', text).strip() - - # Match numeric part more robustly - num_part_match = re.search(r'([\d.,]+)', num_part) # Find first number group - if not num_part_match: return np.nan - num_part_str = num_part_match.group(1) - # Clean again after potential suffix removal - if '.' in num_part_str and ',' in num_part_str: num_part_str = num_part_str.replace('.', '').replace(',', '.') - elif ',' in num_part_str: num_part_str = num_part_str.replace(',', '.') - if '.' in num_part_str and num_part_str.count('.') > 1: num_part_str = num_part_str.replace('.', '') - - + def get_valid_numeric(value_str): + # (Implementierung wie in vorheriger Antwort) + if value_str is None or pd.isna(value_str) or value_str == '': return np.nan try: - val = float(num_part_str) * multiplier - # Allow 0? For modeling maybe not useful, filter later if needed. - # Keep 0 for now, filter >0 for target variable later. - return val if not pd.isna(val) else np.nan # Return NaN if calculation results in NaN - except ValueError: return np.nan - + val = float(str(value_str).replace(',', '.')) + return val if val > 0 else np.nan + except (ValueError, TypeError): + cleaned_str = re.sub(r'[^\d.]', '', str(value_str)) + if not cleaned_str: return np.nan + try: + val = float(cleaned_str) + return val if val > 0 else np.nan + except ValueError: return np.nan cols_to_process = { - 'Umsatz': ('Wiki Umsatz', 'CRM Umsatz', 'Finaler_Umsatz'), - 'Mitarbeiter': ('Wiki Mitarbeiter', 'CRM Anzahl Mitarbeiter', 'Finaler_Mitarbeiter') + 'Umsatz': ('umsatz_wiki', 'umsatz_crm', 'Finaler_Umsatz'), + 'Mitarbeiter': ('ma_wiki', 'ma_crm', 'Finaler_Mitarbeiter') } - for base_name, (wiki_key, crm_key, final_col) in cols_to_process.items(): + for base_name, (wiki_col, crm_col, final_col) in cols_to_process.items(): debug_print(f"Verarbeite '{base_name}'...") - # Use the renamed short keys - wiki_col_short = wiki_key # Already renamed via rename_map_inv - crm_col_short = crm_key # Already renamed via rename_map_inv - - if wiki_col_short not in df_subset.columns: df_subset[wiki_col_short] = np.nan - if crm_col_short not in df_subset.columns: df_subset[crm_col_short] = np.nan - - # Pass final_col name to helper function - wiki_numeric = df_subset[wiki_col_short].apply(lambda x: get_valid_numeric_ml(x, final_col)) - crm_numeric = df_subset[crm_col_short].apply(lambda x: get_valid_numeric_ml(x, final_col)) - - # Prioritize Wiki > CRM > NaN + if wiki_col not in df_subset.columns: df_subset[wiki_col] = np.nan + if crm_col not in df_subset.columns: df_subset[crm_col] = np.nan + wiki_numeric = df_subset[wiki_col].apply(get_valid_numeric) + crm_numeric = df_subset[crm_col].apply(get_valid_numeric) df_subset[final_col] = np.where( wiki_numeric.notna(), wiki_numeric, np.where(crm_numeric.notna(), crm_numeric, np.nan) ) debug_print(f" -> {df_subset[final_col].notna().sum()} gültige '{final_col}' Werte erstellt.") - # --- 3. Zielvariable vorbereiten --- - techniker_col_short = tech_col_key # Use the short key from rename_map_inv - debug_print(f"Verarbeite Zielvariable '{techniker_col_short}' (Original: '{tech_col_key}')...") - df_subset['Anzahl_Servicetechniker_Numeric'] = pd.to_numeric(df_subset[techniker_col_short], errors='coerce') + techniker_col = "techniker" + debug_print(f"Verarbeite Zielvariable '{techniker_col}'...") + df_subset['Anzahl_Servicetechniker_Numeric'] = pd.to_numeric(df_subset[techniker_col], errors='coerce') initial_rows = len(df_subset) - # Filter: Target variable must be > 0 for modeling df_filtered = df_subset[ df_subset['Anzahl_Servicetechniker_Numeric'].notna() & (df_subset['Anzahl_Servicetechniker_Numeric'] > 0) ].copy() filtered_rows = len(df_filtered) - debug_print(f"{initial_rows - filtered_rows} Zeilen entfernt (fehlende/ungültige/<=0 Technikerzahl).") + debug_print(f"{initial_rows - filtered_rows} Zeilen entfernt (fehlende/ungültige Technikerzahl).") debug_print(f"Verbleibende Zeilen für Modellierung: {filtered_rows}") if filtered_rows == 0: return None - # --- 4. Techniker-Buckets erstellen --- - # Use labels compatible with file names and variable names - bins = [-1, 0, 19, 49, 99, 249, 499, float('inf')] # -1 to include 0 if needed, but we filter >0 above - labels = ['B1_0', 'B2_1_19', 'B3_20_49', 'B4_50_99', 'B5_100_249', 'B6_250_499', 'B7_500plus'] + bins = [-1, 0, 19, 49, 99, 249, 499, float('inf')] + labels = ['Bucket_1_(0)', 'Bucket_2_(<20)', 'Bucket_3_(<50)', 'Bucket_4_(<100)', 'Bucket_5_(<250)', 'Bucket_6_(<500)', 'Bucket_7_(>499)'] df_filtered['Techniker_Bucket'] = pd.cut( df_filtered['Anzahl_Servicetechniker_Numeric'], bins=bins, labels=labels, right=True @@ -1993,39 +3949,21 @@ class DataProcessor: debug_print("Techniker-Buckets erstellt.") debug_print(f"Verteilung der Buckets:\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).round(3)}") - # --- 5. Kategoriale Features vorbereiten (Branche) --- - branche_col_short = "CRM Branche" # Use the short key - debug_print(f"Verarbeite kategoriales Feature '{branche_col_short}'...") - df_filtered[branche_col_short] = df_filtered[branche_col_short].astype(str).fillna('Unbekannt').str.strip() - # Remove prefix if present (e.g., "Hersteller / Produzenten > Maschinenbau" -> "Maschinenbau") - df_filtered[branche_col_short] = df_filtered[branche_col_short].apply(lambda x: x.split(' > ')[-1] if ' > ' in x else x) - # Sanitize branch names for column headers (replace spaces, special chars) - df_filtered['Branche_Cleaned'] = df_filtered[branche_col_short].str.replace(r'\s+', '_', regex=True).str.replace(r'[^\w-]', '', regex=True) - - # Perform One-Hot Encoding on the cleaned branch names - df_encoded = pd.get_dummies(df_filtered, columns=['Branche_Cleaned'], prefix='Branche', dummy_na=False) # Use the cleaned column + branche_col = "branche" + debug_print(f"Verarbeite kategoriales Feature '{branche_col}'...") + df_filtered[branche_col] = df_filtered[branche_col].astype(str).fillna('Unbekannt').str.strip() + df_encoded = pd.get_dummies(df_filtered, columns=[branche_col], prefix='Branche', dummy_na=False) debug_print(f"One-Hot Encoding für Branche durchgeführt.") - # --- 6. Finale Auswahl --- - # Features: Alle 'Branche_' Spalten plus die numerischen feature_columns = [col for col in df_encoded.columns if col.startswith('Branche_')] feature_columns.extend(['Finaler_Umsatz', 'Finaler_Mitarbeiter']) target_column = 'Techniker_Bucket' - - # Keep original data columns for reference/analysis if needed (optional) - original_data_cols = ['CRM Name', 'Anzahl_Servicetechniker_Numeric', 'CRM Branche'] # Keep original CRM Name and Branch - # Ensure only required columns are in the final dataframe for modeling - final_cols_for_model = feature_columns + [target_column] - - # Select the final columns needed for modeling + original data cols for reference - df_model_ready = df_encoded[final_cols_for_model + original_data_cols].copy() - - # Convert numeric features again just to be safe (should already be float/NaN) + original_data_cols = ['name', 'Anzahl_Servicetechniker_Numeric'] + df_model_ready = df_encoded[original_data_cols + feature_columns + [target_column]].copy() for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter']: df_model_ready[col] = pd.to_numeric(df_model_ready[col], errors='coerce') - df_model_ready = df_model_ready.reset_index(drop=True) debug_print("Datenvorbereitung abgeschlossen.") nan_counts = df_model_ready[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].isna().sum() @@ -2046,6 +3984,7 @@ def main(): # --- Initialisierung --- parser = argparse.ArgumentParser(description="Firmen-Datenanreicherungs-Skript") + # NEU: 'update_wiki' hinzugefügt valid_modes = ["combined", "wiki", "website", "branch", "summarize", "reeval", "website_lookup", "website_details", "contacts", "full_run", "alignment", "train_technician_model", "update_wiki"] @@ -2063,12 +4002,12 @@ def main(): if args.mode and args.mode.lower() in valid_modes: mode = args.mode.lower(); print(f"Betriebsmodus (aus Kommandozeile): {mode}") else: # Interaktive Abfrage print("Bitte wählen Sie den Betriebsmodus:") - print(" combined: Wiki(AX), Website-Scrape(AR), Summarize(AS), Branch(AO) (Batch, Start bei leerem AO, Branch Forced)") + print(" combined: Wiki(AX), Website-Scrape(AR), Summarize(AS), Branch(AO) (Batch, Start bei leerem AO)") print(" wiki: Nur Wikipedia-Verifizierung (AX) (Batch, Start bei leerem AX)") print(" website: Nur Website-Scraping Rohtext (AR) (Batch, Start bei leerem AR)") print(" summarize: Nur Website-Zusammenfassung (AS) (Batch, Start bei leerem AS)") - print(" branch: Nur Branchen-Einschätzung (AO) (Batch, Start bei leerem AO, mit TS Check)") - print(" update_wiki: Wiki-URL aus Spalte U übernehmen, löscht TS für Reeval") + print(" branch: Nur Branchen-Einschätzung (AO) (Batch, Start bei leerem AO)") + print(" update_wiki: Wiki-URL aus Spalte T übernehmen & Reparse/Re-Branch") # NEU print(" reeval: Verarbeitet Zeilen mit 'x' (volle Verarbeitung, alle TS prüfen)") print(" website_lookup: Sucht fehlende Websites (D)") print(" website_details:Extrahiert Details für Zeilen mit 'x' (AR)") @@ -2087,7 +4026,7 @@ def main(): if args.limit is not None: if args.limit >= 0: row_limit = args.limit; print(f"Zeilenlimit (aus Kommandozeile): {row_limit}") else: print("Warnung: Negatives Limit ignoriert."); row_limit = None - elif mode in ["combined", "wiki", "website", "branch", "summarize", "full_run", "reeval", "update_wiki"]: # Limit für relevante Modi fragen + elif mode in ["combined", "wiki", "website", "branch", "summarize", "full_run"]: # Nur für relevante Modi fragen try: limit_input = input("Max Zeilen? (Enter=alle): "); if limit_input.strip(): @@ -2099,12 +4038,13 @@ def main(): else: row_limit = None; print("Kein Zeilenlimit.") except Exception as e: print(f"Fehler Limit-Eingabe ({e}) -> Kein Limit"); row_limit = None + # Logfile initialisieren LOG_FILE = create_log_filename(mode) debug_print(f"===== Skript gestartet ====="); debug_print(f"Version: {Config.VERSION}") debug_print(f"Betriebsmodus: {mode}"); limit_log_text = str(row_limit) if row_limit is not None else 'N/A für diesen Modus' - if mode in ["combined", "wiki", "website", "branch", "summarize", "full_run", "reeval", "update_wiki"]: + if mode in ["combined", "wiki", "website", "branch", "summarize", "full_run"]: limit_log_text = str(row_limit) if row_limit is not None else 'Unbegrenzt' if row_limit == 0: limit_log_text = '0 (Keine Verarbeitung geplant)' debug_print(f"Zeilenlimit: {limit_log_text}") @@ -2125,7 +4065,7 @@ def main(): if row_limit == 0: debug_print("Limit 0 -> Skip Dispatcher.") else: run_dispatcher(mode, sheet_handler, row_limit) # Einzelne Zeilen Modi (kein Batch-Dispatcher) - elif mode == "reeval": data_processor.process_reevaluation_rows(row_limit=row_limit) # Limit übergeben + elif mode == "reeval": data_processor.process_reevaluation_rows() elif mode == "website_lookup": data_processor.process_serp_website_lookup_for_empty() elif mode == "website_details": data_processor.process_website_details_for_marked_rows() elif mode == "contacts": process_contact_research(sheet_handler) @@ -2135,17 +4075,14 @@ def main(): start_index = sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Prüfung") if start_index != -1 and start_index < len(sheet_handler.get_data()): num_available = len(sheet_handler.get_data()) - start_index - # Use row_limit if set and positive, otherwise process all available - num_to_process = num_available - if row_limit is not None and row_limit >= 0: - num_to_process = min(row_limit, num_available) - + num_to_process = min(row_limit, num_available) if row_limit is not None and row_limit >= 0 else num_available if num_to_process > 0: + # Übergebe Flags an process_rows_sequentially data_processor.process_rows_sequentially(start_index, num_to_process, process_wiki=True, process_chatgpt=True, process_website=True) - else: debug_print("Keine Zeilen für 'full_run' zu verarbeiten (Limit/Startindex).") - else: debug_print(f"Startindex {start_index} für 'full_run' ungültig oder alle Zeilen bereits verarbeitet.") + else: debug_print("Keine Zeilen für 'full_run' zu verarbeiten.") + else: debug_print(f"Startindex {start_index} für 'full_run' ungültig.") elif mode == "alignment": - print("\nACHTUNG: Überschreibt A1:AX5!"); + print("\nACHTUNG: Überschreibt A1:AX5!"); # AX statt AS try: confirm = input("Fortfahren? (j/N): ").strip().lower() except Exception as e_input: print(f"Input-Fehler: {e_input}"); confirm = 'n' if confirm == 'j': alignment_demo(sheet_handler.sheet) @@ -2153,106 +4090,59 @@ def main(): # --- NEU: Wiki Update Modus --- elif mode == "update_wiki": - # process_wiki_updates_from_chatgpt verwendet das row_limit - process_wiki_updates_from_chatgpt(sheet_handler, data_processor, row_limit=row_limit) + process_wiki_updates_from_chatgpt(sheet_handler, data_processor) # --- Ende Wiki Update Modus --- - # Block für Modelltraining (unverändert von v1.6.5) + # Block für Modelltraining (wie von dir bereitgestellt) elif mode == "train_technician_model": debug_print(f"Starte Modus: {mode}") - # Nutze die Methode aus dem DataProcessor prepared_df = data_processor.prepare_data_for_modeling() if prepared_df is not None and not prepared_df.empty: debug_print("Aufteilen der Daten...") try: - # Features: Alle 'Branche_' Spalten plus die numerischen - feature_columns = [col for col in prepared_df.columns if col.startswith('Branche_')] - feature_columns.extend(['Finaler_Umsatz', 'Finaler_Mitarbeiter']) - X = prepared_df[feature_columns] + X = prepared_df.drop(columns=['Techniker_Bucket', 'name', 'Anzahl_Servicetechniker_Numeric']) y = prepared_df['Techniker_Bucket'] - # Behalte Originaldaten für spätere Referenz oder Analyse falls nötig - original_data_cols = ['CRM Name', 'Anzahl_Servicetechniker_Numeric', 'CRM Branche'] # Use correct keys - original_data = prepared_df[original_data_cols] - - X_train, X_test, y_train, y_test, orig_train, orig_test = train_test_split( - X, y, original_data, test_size=0.25, random_state=42, stratify=y - ) - debug_print(f"Trainingsdaten: {X_train.shape[0]} Zeilen, Testdaten: {X_test.shape[0]} Zeilen.") + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y) split_successful = True - except Exception as e: debug_print(f"FEHLER Split: {e}"); split_successful = False; debug_print(traceback.format_exc()) - - + except Exception as e: debug_print(f"FEHLER Split: {e}"); split_successful = False if split_successful: debug_print("Imputation...") numeric_features = ['Finaler_Umsatz', 'Finaler_Mitarbeiter'] try: imputer = SimpleImputer(strategy='median') - # Wichtig: Imputer nur auf Trainingsdaten fitten! - # Use .loc to avoid SettingWithCopyWarning - X_train.loc[:, numeric_features] = imputer.fit_transform(X_train[numeric_features]) - # Testdaten nur transformieren - X_test.loc[:, numeric_features] = imputer.transform(X_test[numeric_features]) + X_train[numeric_features] = imputer.fit_transform(X_train[numeric_features]) + X_test[numeric_features] = imputer.transform(X_test[numeric_features]) imputer_filename = args.imputer_out; pickle.dump(imputer, open(imputer_filename, 'wb')) debug_print(f"Imputer gespeichert: '{imputer_filename}'.") imputation_successful = True - except Exception as e: debug_print(f"FEHLER Imputation: {e}"); imputation_successful = False; debug_print(traceback.format_exc()) - - + except Exception as e: debug_print(f"FEHLER Imputation: {e}"); imputation_successful = False if imputation_successful: debug_print("Starte Training/GridSearchCV...") - param_grid = { - 'criterion': ['gini', 'entropy'], - 'max_depth': [6, 8, 10, 12, 15], - 'min_samples_split': [20, 40, 60], # Erhöhte Werte gegen Overfitting - 'min_samples_leaf': [10, 20, 30], # Erhöhte Werte gegen Overfitting - 'ccp_alpha': [0.0, 0.001, 0.005, 0.01] # Pruning Parameter - } - # class_weight='balanced' ist wichtig bei ungleichen Klassengrößen + param_grid = { 'criterion': ['gini', 'entropy'], 'max_depth': [6, 8, 10, 12, 15], 'min_samples_split': [20, 40, 60], 'min_samples_leaf': [10, 20, 30], 'ccp_alpha': [0.0, 0.001, 0.005]} dtree = DecisionTreeClassifier(random_state=42, class_weight='balanced') - # Verwende f1_weighted, da Klassen ungleich sein könnten grid_search = GridSearchCV(estimator=dtree, param_grid=param_grid, cv=5, scoring='f1_weighted', n_jobs=-1, verbose=1) try: grid_search.fit(X_train, y_train) best_estimator = grid_search.best_estimator_ - debug_print(f"GridSearchCV fertig. Beste Params: {grid_search.best_params_}, Bester F1-Weighted Score (CV): {grid_search.best_score_:.4f}") + debug_print(f"GridSearchCV fertig. Beste Params: {grid_search.best_params_}, Score: {grid_search.best_score_:.4f}") model_filename = args.model_out; pickle.dump(best_estimator, open(model_filename, 'wb')) - debug_print(f"Bestes Modell gespeichert: '{model_filename}'.") + debug_print(f"Modell gespeichert: '{model_filename}'.") training_successful = True except Exception as e_train: debug_print(f"FEHLER Training: {e_train}"); training_successful = False; import traceback; debug_print(traceback.format_exc()) - if training_successful: - debug_print("Evaluiere bestes Modell auf Test-Set..."); - y_pred = best_estimator.predict(X_test) + debug_print("Evaluiere Test-Set..."); y_pred = best_estimator.predict(X_test) test_accuracy = accuracy_score(y_test, y_pred) - # Stelle sicher, dass Klassen im Report und Matrix korrekt sind - class_labels = best_estimator.classes_ # Oder y.unique() sortiert? - report = classification_report(y_test, y_pred, zero_division=0, labels=class_labels, target_names=[str(c) for c in class_labels]) - conf_matrix = confusion_matrix(y_test, y_pred, labels=class_labels) - conf_matrix_df = pd.DataFrame(conf_matrix, index=[f"Wahr:{c}" for c in class_labels], columns=[f"Vorh:{c}" for c in class_labels]) - debug_print(f"\n--- Evaluation Test-Set ---\nGenauigkeit: {test_accuracy:.4f}\nClassification Report:\n{report}\nConfusion Matrix:\n{conf_matrix_df}"); - print(f"\nModell Genauigkeit (Test): {test_accuracy:.4f}") - + report = classification_report(y_test, y_pred, zero_division=0, labels=best_estimator.classes_, target_names=best_estimator.classes_) + conf_matrix = confusion_matrix(y_test, y_pred, labels=best_estimator.classes_) + conf_matrix_df = pd.DataFrame(conf_matrix, index=best_estimator.classes_, columns=best_estimator.classes_) + debug_print(f"\n--- Evaluation Test-Set ---\nGenauigkeit: {test_accuracy:.4f}\nBericht:\n{report}\nMatrix:\n{conf_matrix_df}"); print(f"\nModell Genauigkeit (Test): {test_accuracy:.4f}") debug_print("\nExtrahiere Regeln..."); try: - feature_names = list(X_train.columns); - class_names = [str(c) for c in best_estimator.classes_] # Sicherstellen, dass es Strings sind - rules_text = export_text(best_estimator, feature_names=feature_names, class_names=class_names, show_weights=True, spacing=3, decimals=2) - patterns_filename_txt = args.patterns_out; - with open(patterns_filename_txt, 'w', encoding='utf-8') as f: f.write(rules_text) - debug_print(f"Regeln gespeichert als Text: '{patterns_filename_txt}'.") - - # Optional: Regeln als JSON speichern (strukturierter) - # patterns_filename_json = PATTERNS_FILE_JSON - # try: - # # Hier müsste eine Funktion implementiert werden, die 'rules_text' in JSON umwandelt - # # rules_json = parse_rules_to_json(rules_text) # Hypothetische Funktion - # # with open(patterns_filename_json, 'w', encoding='utf-8') as f: - # # json.dump(rules_json, f, indent=2, ensure_ascii=False) - # # debug_print(f"Regeln gespeichert als JSON: '{patterns_filename_json}'.") - # pass - # except Exception as e_json: - # debug_print(f"Fehler beim Speichern der Regeln als JSON: {e_json}") - + feature_names = list(X_train.columns); class_names = best_estimator.classes_ + rules_text = export_text(best_estimator, feature_names=feature_names, class_names=class_names, show_weights=True, spacing=3) + patterns_filename = args.patterns_out; + with open(patterns_filename, 'w', encoding='utf-8') as f: f.write(rules_text) + debug_print(f"Regeln gespeichert: '{patterns_filename}'.") except Exception as e_export: debug_print(f"Fehler Export Regeln: {e_export}") else: debug_print("Datenvorbereitung fehlgeschlagen -> Abbruch ML Training.") @@ -2270,11 +4160,15 @@ def main(): debug_print(f"===== Skript beendet =====") if LOG_FILE: try: - with open(LOG_FILE, "a", encoding="utf-8") as f: # Korrigiert + # 'with' startet in der nächsten Zeile + with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ===== Skript wirklich beendet =====\n") except Exception as e: + # Optional: Warnung ausgeben, wenn das finale Schreiben fehlschlägt print(f"[WARNUNG] Konnte letzte Log-Nachricht nicht schreiben: {e}") - pass + pass # Programm soll trotzdem normal beenden + # --- ENDE KORRIGIERTER BLOCK --- + print(f"Verarbeitung abgeschlossen. Logfile: {LOG_FILE}")