From 1134e8167d84d10030a7f0c39a14d8fc338e04c9 Mon Sep 17 00:00:00 2001 From: Floke Date: Thu, 8 May 2025 08:03:00 +0000 Subject: [PATCH] =?UTF-8?q?v.=201.7.1:=20Gro=C3=9Fe=20=C3=9Cberarbeitung?= =?UTF-8?q?=20neuer=20Chat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- brancheneinstufung.py | 3341 +++++++++-------------------------------- 1 file changed, 694 insertions(+), 2647 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index c4cc5898..9f57bbaa 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -8,7 +8,7 @@ von Unternehmensdaten, primär aus einem Google Sheet, ergänzt durch Web Scrapi Wikipedia, OpenAI (ChatGPT) und SerpAPI (Google Search, LinkedIn). Autor: [Ihr Name/Pseudonym] -Version: v1.7.0 +Version: v1.7.1 Hinweis zur Struktur: Dieser Code wird in logischen Bloecken uebermittelt. Fuegen Sie die Bloecke @@ -107,7 +107,7 @@ PATTERNS_FILE_JSON = "technician_patterns.json" # Neu (Empfohlen) # --- Globale Konfiguration Klasse --- class Config: """Zentrale Konfigurationseinstellungen.""" - VERSION = "v1.7.0" + VERSION = "v1.7.1" LANG = "de" # Sprache fuer Wikipedia etc. # ACHTUNG: SHEET_URL ist hier ein Platzhalter. Ersetzen Sie ihn durch Ihre tatsaechliche URL. SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" # <<< ERSETZEN SIE DIES! @@ -2422,15 +2422,15 @@ class GoogleSheetHandler: Initialisiert den Handler, stellt die Verbindung her und laedt die Daten. """ # Holen Sie eine Logger-Instanz fuer diese Klasse - self.logger = logging.getLogger(__name__ + ".GoogleSheetHandler") + self.logger = logging.getLogger(__name__ + ".GoogleSheetHandler") # <<< HINZUGEFÜGT # Initialisieren Sie die Attribute self.sheet = None # Daten werden hier als Instanzvariable gespeichert, um nicht bei jedem Zugriff neu laden zu muessen - self.sheet_values = [] # <<< DIESE ZEILE HINZUFÜGEN + self.sheet_values = [] # header_rows sind fix, aber wir koennen sie hier zur Klarheit definieren - self._header_rows = 5 # <<< DIESE ZEILE HINZUFÜGEN (Annahme: Die ersten 5 Zeilen sind Header) + self._header_rows = 5 # Annahme: Die ersten 5 Zeilen sind Header - self.logger.info("Initialisiere GoogleSheetHandler...") + self.logger.info("Initialisiere GoogleSheetHandler...") # <<< GEÄNDERT try: # Verbindung wird bei der Initialisierung aufgebaut self._connect() @@ -2439,7 +2439,7 @@ class GoogleSheetHandler: self.load_data() # Erste Datenladung nach erfolgreicher Verbindung else: # Wenn die Verbindung fehlschlug (sheet ist None), aber keine Exception geworfen wurde (sollte nicht passieren) - self.logger.critical( + self.logger.critical( # <<< GEÄNDERT "GoogleSheetHandler Init FEHLER: Verbindung konnte nicht hergestellt werden (sheet ist None)." ) raise ConnectionError( @@ -2447,17 +2447,17 @@ class GoogleSheetHandler: ) except Exception as e: # Fehler bei der Initialisierung (entweder von _connect oder load_data nach Retries) - self.logger.critical( + self.logger.critical( # <<< GEÄNDERT f"FATAL: Fehler bei Initialisierung von GoogleSheetHandler: {type(e).__name__} - {e}" ) - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT raise ConnectionError(f"Google Sheet Handler Init failed: {e}") @retry_on_failure def _connect(self): """Stellt Verbindung zum Google Sheet her.""" self.sheet = None - self.logger.info("Versuche Verbindung mit Google Sheets herstellen...") + self.logger.info("Versuche Verbindung mit Google Sheets herstellen...") # <<< GEÄNDERT try: if not os.path.exists(CREDENTIALS_FILE): raise FileNotFoundError(f"Credential-Datei nicht gefunden: {CREDENTIALS_FILE}") @@ -2467,12 +2467,12 @@ class GoogleSheetHandler: gc = gspread.authorize(creds) sh = gc.open_by_url(Config.SHEET_URL) self.sheet = sh.sheet1 - self.logger.info("Verbindung zu Google Sheets erfolgreich.") + self.logger.info("Verbindung zu Google Sheets erfolgreich.") # <<< GEÄNDERT except (gspread.exceptions.APIError, requests.exceptions.RequestException, FileNotFoundError) as e: raise e except Exception as e: - self.logger.error(f"FEHLER bei der Google Sheets Verbindung: {type(e).__name__} - {e}") - self.logger.debug(traceback.format_exc()) + self.logger.error(f"FEHLER bei der Google Sheets Verbindung: {type(e).__name__} - {e}") # <<< GEÄNDERT + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT raise e @retry_on_failure @@ -2481,16 +2481,16 @@ class GoogleSheetHandler: Laedt alle Daten aus dem Sheet und aktualisiert self.sheet_values. """ if not self.sheet: - self.logger.error("Fehler: Keine Sheet-Verbindung zum Laden der Daten.") + self.logger.error("Fehler: Keine Sheet-Verbindung zum Laden der Daten.") # <<< GEÄNDERT self.sheet_values = [] return False - self.logger.info("Lade Daten aus Google Sheet...") + self.logger.info("Lade Daten aus Google Sheet...") # <<< GEÄNDERT try: self.sheet_values = self.sheet.get_all_values() if not self.sheet_values: - self.logger.warning( + self.logger.warning( # <<< GEÄNDERT "Google Sheet scheint leer zu sein oder get_all_values() lieferte keine Daten." ) self.headers = [] @@ -2498,21 +2498,21 @@ class GoogleSheetHandler: num_rows = len(self.sheet_values) num_cols = len(self.sheet_values[0]) if num_rows > 0 else 0 - self.logger.info(f"Daten neu geladen: {num_rows} Zeilen, {num_cols} Spalten.") + self.logger.info(f"Daten neu geladen: {num_rows} Zeilen, {num_cols} Spalten.") # <<< GEÄNDERT try: max_col_idx_in_map = max(COLUMN_MAP.values()) if num_cols <= max_col_idx_in_map: - self.logger.warning( + self.logger.warning( # <<< GEÄNDERT f"Geladenes Sheet hat {num_cols} Spalten, erwartet werden aber mindestens " f"{max_col_idx_in_map + 1} basierend auf COLUMN_MAP." ) except ValueError: - self.logger.warning( + self.logger.warning( # <<< GEÄNDERT "COLUMN_MAP scheint leer zu sein oder enthaelt keine Werte. Kann Spaltenanzahl nicht pruefen." ) except Exception as e: - self.logger.error(f"Fehler bei der Pruefung der Spaltenanzahl gegen COLUMN_MAP: {e}") + self.logger.error(f"Fehler bei der Pruefung der Spaltenanzahl gegen COLUMN_MAP: {e}") # <<< GEÄNDERT if num_rows > 0: self.headers = self.sheet_values[0] @@ -2523,10 +2523,10 @@ class GoogleSheetHandler: except (gspread.exceptions.APIError, requests.exceptions.RequestException) as e: raise e except Exception as e: - self.logger.error( + self.logger.error( # <<< GEÄNDERT f"Allgemeiner Fehler beim Laden der Google Sheet Daten: {type(e).__name__} - {e}" ) - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT raise e def get_data(self): @@ -2535,7 +2535,7 @@ class GoogleSheetHandler: (ohne die ersten N Header-Zeilen). """ if not self.sheet_values or len(self.sheet_values) <= self._header_rows: - self.logger.debug( + self.logger.debug( # <<< GEÄNDERT f"get_data: Keine Datenzeilen verfuegbar " f"(geladen: {len(self.sheet_values) if self.sheet_values else 0} Zeilen, " f"{self._header_rows} Header)." @@ -2546,7 +2546,7 @@ class GoogleSheetHandler: def get_all_data_with_headers(self): """Gibt alle aktuell im Handler gespeicherten Daten inklusive Header zurueck.""" if not self.sheet_values: - self.logger.debug("get_all_data_with_headers: Keine Daten im Handler gespeichert.") + self.logger.debug("get_all_data_with_headers: Keine Daten im Handler gespeichert.") # <<< GEÄNDERT return [] return self.sheet_values.copy() @@ -2556,7 +2556,7 @@ class GoogleSheetHandler: Google Sheets Spaltenbuchstaben (A, B, ..., Z, AA, ...). """ if not isinstance(col_idx_1_based, int) or col_idx_1_based < 1: - self.logger.error( + self.logger.error( # <<< GEÄNDERT f"Ungueltiger Spaltenindex ({col_idx_1_based}) fuer _get_col_letter erhalten." ) return None @@ -2587,38 +2587,38 @@ class GoogleSheetHandler: Gibt die Laenge der Datenliste zurueck, wenn keine leere Zelle im Suchbereich gefunden wurde. """ if not self.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer get_start_row_index.") + self.logger.error("Fehler beim Laden der Daten fuer get_start_row_index.") # <<< GEÄNDERT return -1 data_rows = self.get_data() if not data_rows: - self.logger.info("Keine Datenzeilen im Sheet gefunden. Startindex fuer leere Zelle ist 0.") + self.logger.info("Keine Datenzeilen im Sheet gefunden. Startindex fuer leere Zelle ist 0.") # <<< GEÄNDERT return 0 check_column_index = COLUMN_MAP.get(check_column_key) if check_column_index is None: - self.logger.critical( + self.logger.critical( # <<< GEÄNDERT f"FEHLER: Schluessel '{check_column_key}' nicht in COLUMN_MAP gefunden fuer get_start_row_index!" ) return -1 actual_col_letter = self._get_col_letter(check_column_index + 1) if actual_col_letter is None: - self.logger.error( + self.logger.error( # <<< GEÄNDERT f"FEHLER: Konnte Spaltenbuchstaben fuer Index {check_column_index + 1} nicht ermitteln." ) actual_col_letter = f"Index_{check_column_index + 1}" search_start_index_in_data = max(0, (min_sheet_row - 1) - self._header_rows) - self.logger.info( + self.logger.info( # <<< GEÄNDERT f"get_start_row_index: Suche ab Daten-Index {search_start_index_in_data} " f"(Sheet-Zeile {search_start_index_in_data + self._header_rows + 1}) " f"nach EXAKT LEEREM Wert (=='') in Spalte '{check_column_key}' ({actual_col_letter})..." ) if search_start_index_in_data >= len(data_rows): - self.logger.warning( + self.logger.warning( # <<< GEÄNDERT f"Start-Suchindex in Daten ({search_start_index_in_data}) liegt hinter der letzten Datenzeile ({len(data_rows)}). Keine leere Zelle gefunden im Suchbereich." ) return len(data_rows) @@ -2638,21 +2638,21 @@ class GoogleSheetHandler: log_debug = (i < search_start_index_in_data + 5) or (i % 1000 == 0) or is_exactly_empty if log_debug: - self.logger.debug( + self.logger.debug( # <<< GEÄNDERT f" -> Pruefe Daten-Index {i} (Sheet {current_sheet_row}): " f"Wert in {actual_col_letter}='{str(cell_value).strip()}' " f"(Roh='{cell_value}' Typ: {type(cell_value)}). Leer? {is_exactly_empty}" ) if is_exactly_empty: - self.logger.info( + self.logger.info( # <<< GEÄNDERT f"Erste Zeile ab Sheet-Zeile {min_sheet_row} mit EXAKT LEEREM Wert in Spalte " f"{actual_col_letter} gefunden: Zeile {current_sheet_row} (Daten-Index {i})" ) return i last_data_index = len(data_rows) - self.logger.info( + self.logger.info( # <<< GEÄNDERT f"Alle Zeilen ab Daten-Index {search_start_index_in_data} im Suchbereich haben einen " f"nicht-leeren Wert in Spalte {actual_col_letter}. Naechster Daten-Index waere {last_data_index}." ) @@ -2673,7 +2673,7 @@ class GoogleSheetHandler: bool: True bei Erfolg (nach allen Retries), False bei endgueltigem Fehler. """ if not self.sheet: - self.logger.error("FEHLER: Keine Sheet-Verbindung fuer Batch-Update.") + self.logger.error("FEHLER: Keine Sheet-Verbindung fuer Batch-Update.") # <<< GEÄNDERT return False if not update_data: @@ -2683,14 +2683,14 @@ class GoogleSheetHandler: total_cells_to_update = sum( len(row) for item in update_data for row in item.get('values', []) ) - self.logger.debug( + self.logger.debug( # <<< GEÄNDERT f" -> Versuche sheet.batch_update mit {len(update_data)} Anfragen " f"({total_cells_to_update} Zellen)..." ) self.sheet.batch_update(update_data, value_input_option='USER_ENTERED') return True except Exception: - self.logger.error( + self.logger.error( # <<< GEÄNDERT f"Endgueltiger Fehler beim Batch-Update nach Retries. Kann {len(update_data)} Operationen nicht durchfuehren." ) return False @@ -2717,8 +2717,8 @@ class WikipediaScraper: Defaults to Config.USER_AGENT. """ # Erhalten Sie eine Logger-Instanz fuer diese Klasse - self.logger = logging.getLogger(__name__ + ".WikipediaScraper") - self.logger.debug("WikipediaScraper initialisiert.") + self.logger = logging.getLogger(__name__ + ".WikipediaScraper") # <<< HINZUGEFÜGT + self.logger.debug("WikipediaScraper initialisiert.") # <<< GEÄNDERT # User-Agent fuer Requests (nutzt Config, Fallback wenn nicht gesetzt) self.user_agent = user_agent or getattr( @@ -2727,7 +2727,7 @@ class WikipediaScraper: ) self.session = requests.Session() self.session.headers.update({'User-Agent': self.user_agent}) - self.logger.debug(f"Requests Session mit User-Agent '{self.user_agent}' initialisiert.") + self.logger.debug(f"Requests Session mit User-Agent '{self.user_agent}' initialisiert.") # <<< GEÄNDERT # Keywords fuer die Infobox-Extraktion self.keywords_map = { @@ -2741,11 +2741,11 @@ class WikipediaScraper: wiki_lang = getattr(Config, 'LANG', 'de') wikipedia.set_lang(wiki_lang) wikipedia.set_rate_limiting(True, min_wait=0.1) - self.logger.info( + self.logger.info( # <<< GEÄNDERT f"Wikipedia library language set to '{wiki_lang}'. Rate limiting enabled (min_wait=0.1)." ) except Exception as e: - self.logger.warning(f"Fehler beim Setzen der Wikipedia-Sprache oder Rate Limiting: {e}") + self.logger.warning(f"Fehler beim Setzen der Wikipedia-Sprache oder Rate Limiting: {e}") # <<< GEÄNDERT def _get_full_domain(self, website): """Extrahiert die normalisierte Domain (ohne www, ohne Pfad) aus einer URL.""" @@ -2778,7 +2778,7 @@ class WikipediaScraper: terms.add(full_domain) final_terms = [term for term in list(terms) if term][:getattr(Config, 'WIKIPEDIA_SEARCH_RESULTS', 5)] - self.logger.debug(f"Generierte Suchbegriffe fuer '{company_name[:100]}...': {final_terms}") + self.logger.debug(f"Generierte Suchbegriffe fuer '{company_name[:100]}...': {final_terms}") # <<< GEÄNDERT return final_terms @retry_on_failure @@ -2787,18 +2787,18 @@ class WikipediaScraper: Holt HTML von einer URL (requests) und gibt ein BeautifulSoup-Objekt zurueck. """ if not url or not isinstance(url, str) or not url.lower().startswith(("http://", "https://")): - self.logger.warning(f"_get_page_soup: Ungueltige URL '{url[:100]}...'.") + self.logger.warning(f"_get_page_soup: Ungueltige URL '{url[:100]}...'.") # <<< GEÄNDERT return None try: - self.logger.debug(f"_get_page_soup: Rufe URL ab: {url[:100]}...") + self.logger.debug(f"_get_page_soup: Rufe URL ab: {url[:100]}...") # <<< GEÄNDERT response = self.session.get(url, timeout=getattr(Config, 'REQUEST_TIMEOUT', 15)) response.raise_for_status() response.encoding = response.apparent_encoding soup = BeautifulSoup(response.text, getattr(Config, 'HTML_PARSER', 'html.parser')) - self.logger.debug(f"_get_page_soup: Parsen von {url[:100]}... erfolgreich.") + self.logger.debug(f"_get_page_soup: Parsen von {url[:100]}... erfolgreich.") # <<< GEÄNDERT return soup except Exception as e: - self.logger.error(f"_get_page_soup: Fehler beim Abrufen oder Parsen von HTML von {url[:100]}...: {type(e).__name__} - {e}") + self.logger.error(f"_get_page_soup: Fehler beim Abrufen oder Parsen von HTML von {url[:100]}...: {type(e).__name__} - {e}") # <<< GEÄNDERT raise e def _validate_article(self, page, company_name, website): @@ -2808,7 +2808,7 @@ class WikipediaScraper: """ if not page or not company_name: return False - self.logger.debug( + self.logger.debug( # <<< GEÄNDERT f"Validiere Artikel '{page.title[:100]}...' (URL: {page.url[:100]}...) " f"fuer Firma '{company_name[:100]}' (Website: {website[:100]})..." ) @@ -2816,12 +2816,12 @@ class WikipediaScraper: normalized_company = normalize_company_name(company_name) normalized_title = normalize_company_name(page.title) if not normalized_company or not normalized_title: - self.logger.warning("Validierung nicht moeglich, da Normalisierung eines Namens fehlschlug.") + self.logger.warning("Validierung nicht moeglich, da Normalisierung eines Namens fehlschlug.") # <<< GEÄNDERT return False standard_threshold = getattr(Config, 'SIMILARITY_THRESHOLD', 0.65) similarity = fuzzy_similarity(normalized_title, normalized_company) - self.logger.debug(f" -> Gesamt-Aehnlichkeit (normalized): {similarity:.2f}") + self.logger.debug(f" -> Gesamt-Aehnlichkeit (normalized): {similarity:.2f}") # <<< GEÄNDERT company_tokens = normalized_company.split() title_tokens = normalized_title.split() @@ -2835,7 +2835,7 @@ class WikipediaScraper: domain_found = False full_domain = self._get_full_domain(website) if full_domain != "k.A.": - self.logger.debug(f" -> Suche nach Domain '{full_domain}' in externen Links des Artikels...") + self.logger.debug(f" -> Suche nach Domain '{full_domain}' in externen Links des Artikels...") # <<< GEÄNDERT try: article_html = page.html() if article_html: @@ -2850,7 +2850,7 @@ class WikipediaScraper: if relevant_links: domain_found = True except Exception as e_link_check: - self.logger.error( + self.logger.error( # <<< GEÄNDERT f"Fehler waehrend der Domain-Link-Pruefung fuer '{page.title[:100]}...': " f"{type(e_link_check).__name__} - {e_link_check}" ) @@ -2878,7 +2878,7 @@ class WikipediaScraper: reason = f"Erstes normalisiertes Wort stimmt ueberein UND Aehnlichkeit >= 0.55 (Sim={similarity:.2f})" log_level = logging.INFO if is_valid else logging.DEBUG - self.logger.log( + self.logger.log( # <<< GEÄNDERT log_level, f" => Artikel '{page.title[:100]}...' " f"{'VALIDIERT' if is_valid else 'NICHT validiert'} " @@ -2922,10 +2922,10 @@ class WikipediaScraper: break if paragraph_text == "k.A.": - self.logger.debug("Kein passender erster Absatz gefunden nach Pruefung der

-Tags.") + self.logger.debug("Kein passender erster Absatz gefunden nach Pruefung der

-Tags.") # <<< GEÄNDERT except Exception as e: - self.logger.error(f"Fehler beim Extrahieren des ersten Absatzes: {type(e).__name__} - {e}") - self.logger.debug(traceback.format_exc()) + self.logger.error(f"Fehler beim Extrahieren des ersten Absatzes: {type(e).__name__} - {e}") # <<< GEÄNDERT + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT return paragraph_text def extract_categories(self, soup): @@ -2945,14 +2945,14 @@ class WikipediaScraper: c for c in cats if c and isinstance(c, str) and c.strip() and "kategorien:" not in c.lower() ] - self.logger.debug(f"Kategorien gefunden: {cats_filtered}") + self.logger.debug(f"Kategorien gefunden: {cats_filtered}") # <<< GEÄNDERT else: - self.logger.debug("Kein 'ul' Tag in 'mw-normal-catlinks' gefunden.") + self.logger.debug("Kein 'ul' Tag in 'mw-normal-catlinks' gefunden.") # <<< GEÄNDERT else: - self.logger.debug("Kein 'div#mw-normal-catlinks' gefunden.") + self.logger.debug("Kein 'div#mw-normal-catlinks' gefunden.") # <<< GEÄNDERT except Exception as e: - self.logger.error(f"Fehler beim Extrahieren der Kategorien: {type(e).__name__} - {e}") - self.logger.debug(traceback.format_exc()) + self.logger.error(f"Fehler beim Extrahieren der Kategorien: {type(e).__name__} - {e}") # <<< GEÄNDERT + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT return ", ".join(cats_filtered) if cats_filtered else "k.A." def _extract_infobox_value(self, soup, target): @@ -2960,20 +2960,20 @@ class WikipediaScraper: Extrahiert gezielt Branche, Umsatz oder Mitarbeiter aus der Infobox eines Wikipedia-Artikels Soup-Objekts. """ - self.logger.debug(f"--- Entering _extract_infobox_value for target '{target}' ---") + self.logger.debug(f"--- Entering _extract_infobox_value for target '{target}' ---") # <<< GEÄNDERT if not soup or target not in self.keywords_map: - self.logger.debug( + self.logger.debug( # <<< GEÄNDERT f"_extract_infobox_value: Ungueltiger Input (Soup: {soup is not None}, Target: {target})" ) return "k.A." keywords = self.keywords_map[target] - self.logger.debug(f"_extract_infobox_value: Suche nach '{target}' mit Keywords: {keywords}") + self.logger.debug(f"_extract_infobox_value: Suche nach '{target}' mit Keywords: {keywords}") # <<< GEÄNDERT infobox = soup.select_one('table[class*="infobox"]') if not infobox: - self.logger.debug(" -> KEINE Infobox via select_one 'table[class*=\"infobox\"]' gefunden.") + self.logger.debug(" -> KEINE Infobox via select_one 'table[class*=\"infobox\"]' gefunden.") # <<< GEÄNDERT return "k.A." - self.logger.debug(" -> Infobox gefunden.") + self.logger.debug(" -> Infobox gefunden.") # <<< GEÄNDERT value_found = "k.A." try: @@ -3019,29 +3019,29 @@ class WikipediaScraper: clean_val = re.sub(r'\s*\([^)]*\)', '', clean_val).strip() clean_val = clean_val.split('\n')[0].strip() value_found = clean_val if clean_val else "k.A." - self.logger.info(f" --> Branche extrahiert: '{value_found}'") + self.logger.info(f" --> Branche extrahiert: '{value_found}'") # <<< GEÄNDERT elif target == 'umsatz': numeric_val_str = extract_numeric_value(raw_value_text, is_umsatz=True) value_found = numeric_val_str - self.logger.info( + self.logger.info( # <<< GEÄNDERT f" --> Umsatz extrahiert (aus '{raw_value_text[:50]}...'): '{value_found}'" ) elif target == 'mitarbeiter': numeric_val_str = extract_numeric_value(raw_value_text, is_umsatz=False) value_found = numeric_val_str - self.logger.info( + self.logger.info( # <<< GEÄNDERT f" --> Mitarbeiter extrahiert (aus '{raw_value_text[:50]}...'): '{value_found}'" ) break if value_found != "k.A.": - self.logger.debug(f" -> Finaler Wert fuer '{target}' gefunden: '{value_found}'") + self.logger.debug(f" -> Finaler Wert fuer '{target}' gefunden: '{value_found}'") # <<< GEÄNDERT else: - self.logger.debug( + self.logger.debug( # <<< GEÄNDERT f" -> Kein passender Eintrag fuer '{target}' in der gesamten Infobox gefunden." ) except Exception as e: - self.logger.exception( + self.logger.exception( # <<< GEÄNDERT f"Fehler beim Durchlaufen der Infobox-Zeilen fuer '{target}': {e}" ) return "k.A. (Fehler Extraktion)" @@ -3056,15 +3056,15 @@ class WikipediaScraper: Artikel gefunden wird. Behandelt explizit Begriffsklaerungsseiten. """ if not company_name or str(company_name).strip() == "": - self.logger.warning("Wikipedia search skipped: No company name provided.") + self.logger.warning("Wikipedia search skipped: No company name provided.") # <<< GEÄNDERT raise ValueError("Kein Firmenname fuer Wikipedia Suche angegeben.") search_terms = self._generate_search_terms(company_name, website) if not search_terms: - self.logger.warning(f"Keine Suchbegriffe fuer '{company_name[:100]}...' generiert.") + self.logger.warning(f"Keine Suchbegriffe fuer '{company_name[:100]}...' generiert.") # <<< GEÄNDERT return None - self.logger.info( + self.logger.info( # <<< GEÄNDERT f"Starte Wikipedia-Suche fuer '{company_name[:100]}...' " f"(Website: {website[:100]}...) mit Begriffen: {search_terms}" ) @@ -3074,20 +3074,20 @@ class WikipediaScraper: if title_to_check in processed_titles: return None processed_titles.add(title_to_check) - self.logger.debug(f" -> Pruefe potenziellen Artikel: '{title_to_check[:100]}...'") + self.logger.debug(f" -> Pruefe potenziellen Artikel: '{title_to_check[:100]}...'") # <<< GEÄNDERT try: page = wikipedia.page(title_to_check, auto_suggest=False, preload=True) if self._validate_article(page, company_name, website): - self.logger.info(f" -> Titel '{page.title[:100]}...' erfolgreich validiert!") + self.logger.info(f" -> Titel '{page.title[:100]}...' erfolgreich validiert!") # <<< GEÄNDERT return page else: - self.logger.debug(f" -> Titel '{title_to_check[:100]}...' nicht validiert.") + self.logger.debug(f" -> Titel '{title_to_check[:100]}...' nicht validiert.") # <<< GEÄNDERT return None except wikipedia.exceptions.PageError: - self.logger.debug(f" -> Seite '{title_to_check[:100]}...' nicht gefunden (PageError).") + self.logger.debug(f" -> Seite '{title_to_check[:100]}...' nicht gefunden (PageError).") # <<< GEÄNDERT return None except wikipedia.exceptions.DisambiguationError as e_inner: - self.logger.info( + self.logger.info( # <<< GEÄNDERT f" -> Begriffsklaerung '{title_to_check[:100]}...' gefunden. " f"Pruefe Optionen: {str(e_inner.options)[:100]}..." ) @@ -3100,56 +3100,56 @@ class WikipediaScraper: continue validated_option_page = check_page(option) if validated_option_page: - self.logger.info( + self.logger.info( # <<< GEÄNDERT f" -> Option '{option[:100]}...' aus Begriffsklaerung erfolgreich validiert!" ) return validated_option_page - self.logger.debug( + self.logger.debug( # <<< GEÄNDERT f" -> Keine passende/validierte Unternehmens-Option in Begriffsklaerung '{title_to_check[:100]}...' gefunden." ) return None except (requests.exceptions.RequestException, wikipedia.exceptions.WikipediaException) as e_req: - self.logger.warning( + self.logger.warning( # <<< GEÄNDERT f" -> Netzwerk/API-Fehler beim Laden/Validieren von '{title_to_check[:100]}...': " f"{type(e_req).__name__} - {e_req}. Ueberspringe diesen Titel." ) return None except Exception as e_page: - self.logger.error( + self.logger.error( # <<< GEÄNDERT f" -> Unerwarteter Fehler bei Verarbeitung von Titel '{title_to_check[:100]}...': " f"{type(e_page).__name__} - {e_page}" ) - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT return None - self.logger.debug(f" -> Versuche direkten Match fuer '{company_name[:100]}...'...") + self.logger.debug(f" -> Versuche direkten Match fuer '{company_name[:100]}...'...") # <<< GEÄNDERT validated_page = check_page(company_name) if validated_page: return validated_page - self.logger.debug( + self.logger.debug( # <<< GEÄNDERT f" -> Kein direkter Treffer/validiert. Starte Suche mit generierten Begriffen: {search_terms}" ) for term in search_terms: try: - self.logger.debug(f" -> Suche mit Begriff: '{term[:100]}...'...") + self.logger.debug(f" -> Suche mit Begriff: '{term[:100]}...'...") # <<< GEÄNDERT search_results = wikipedia.search(term, results=getattr(Config, 'WIKIPEDIA_SEARCH_RESULTS', 5)) - self.logger.debug(f" -> Suchergebnisse fuer '{term[:100]}...': {search_results}") + self.logger.debug(f" -> Suchergebnisse fuer '{term[:100]}...': {search_results}") # <<< GEÄNDERT if not search_results: - self.logger.debug(f" -> Keine Suchergebnisse fuer '{term[:100]}...'.") + self.logger.debug(f" -> Keine Suchergebnisse fuer '{term[:100]}...'.") # <<< GEÄNDERT continue for title in search_results: validated_page = check_page(title) if validated_page: return validated_page except Exception as e_search: - self.logger.error( + self.logger.error( # <<< GEÄNDERT f"Fehler waehrend Wikipedia-Suche fuer '{term[:100]}...': " f"{type(e_search).__name__} - {e_search}" ) raise e_search - self.logger.warning( + self.logger.warning( # <<< GEÄNDERT f"Kein passender & validierter Wikipedia-Artikel fuer '{company_name[:100]}...' gefunden nach Pruefung aller Begriffe und Optionen." ) return None @@ -3170,30 +3170,30 @@ class WikipediaScraper: } if not page_url or not isinstance(page_url, str) or "wikipedia.org/wiki/" not in page_url.lower(): - self.logger.warning( + self.logger.warning( # <<< GEÄNDERT f"extract_company_data: Ungueltige oder keine Wikipedia-URL '{page_url[:100]}...'." ) return default_result - self.logger.info(f"Extrahiere Daten fuer Wiki-URL: {page_url[:100]}...") + self.logger.info(f"Extrahiere Daten fuer Wiki-URL: {page_url[:100]}...") # <<< GEÄNDERT soup = self._get_page_soup(page_url) if not soup: - self.logger.error(f" -> Fehler: Konnte Seite {page_url[:100]}... nicht laden oder parsen.") + self.logger.error(f" -> Fehler: Konnte Seite {page_url[:100]}... nicht laden oder parsen.") # <<< GEÄNDERT return default_result - self.logger.debug(" -> Extrahiere erster Absatz...") + self.logger.debug(" -> Extrahiere erster Absatz...") # <<< GEÄNDERT first_paragraph = self._extract_first_paragraph_from_soup(soup) - self.logger.debug(" -> Extrahiere Kategorien...") + self.logger.debug(" -> Extrahiere Kategorien...") # <<< GEÄNDERT categories_val = self.extract_categories(soup) - self.logger.debug(" -> Extrahiere Branche aus Infobox...") + self.logger.debug(" -> Extrahiere Branche aus Infobox...") # <<< GEÄNDERT branche_val = self._extract_infobox_value(soup, 'branche') - self.logger.debug(" -> Extrahiere Umsatz aus Infobox...") + self.logger.debug(" -> Extrahiere Umsatz aus Infobox...") # <<< GEÄNDERT umsatz_val = self._extract_infobox_value(soup, 'umsatz') - self.logger.debug(" -> Extrahiere Mitarbeiter aus Infobox...") + self.logger.debug(" -> Extrahiere Mitarbeiter aus Infobox...") # <<< GEÄNDERT mitarbeiter_val = self._extract_infobox_value(soup, 'mitarbeiter') result = { @@ -3205,7 +3205,7 @@ class WikipediaScraper: 'categories': categories_val } - self.logger.info( + self.logger.info( # <<< GEÄNDERT f" -> Extrahierte Daten: P='{first_paragraph[:50]}...', " f"B='{branche_val}', U='{umsatz_val}', M='{mitarbeiter_val}', " f"C='{categories_val[:50]}...'" @@ -3238,17 +3238,17 @@ class DataProcessor: # (z.B. OpenAIHandler, SerpAPIHandler), falls diese als eigene Klassen ausgelagert werden. """ # Erhalten Sie eine Logger-Instanz fuer diese Klasse - self.logger = logging.getLogger(__name__ + ".DataProcessor") - self.logger.info("Initialisiere DataProcessor...") + self.logger = logging.getLogger(__name__ + ".DataProcessor") # <<< HINZUGEFÜGT + self.logger.info("Initialisiere DataProcessor...") # <<< GEÄNDERT # Ueberpruefen Sie, ob gueltige Handler-Instanzen uebergeben wurden if not isinstance(sheet_handler, GoogleSheetHandler): # Logge einen kritischen Fehler und werfe eine Exception - self.logger.critical("DataProcessor Init FEHLER: Kein gueltiger GoogleSheetHandler uebergeben!") + self.logger.critical("DataProcessor Init FEHLER: Kein gueltiger GoogleSheetHandler uebergeben!") # <<< GEÄNDERT raise ValueError("DataProcessor benoetigt eine gueltige GoogleSheetHandler Instanz.") if not isinstance(wiki_scraper, WikipediaScraper): # Logge einen kritischen Fehler und werfe eine Exception - self.logger.critical("DataProcessor Init FEHLER: Kein gueltiger WikipediaScraper uebergeben!") + self.logger.critical("DataProcessor Init FEHLER: Kein gueltiger WikipediaScraper uebergeben!") # <<< GEÄNDERT raise ValueError("DataProcessor benoetigt eine gueltige WikipediaScraper Instanz.") # Speichern Sie die Handler-Instanzen als Attribute der Instanz @@ -3263,7 +3263,7 @@ class DataProcessor: self.imputer = None self._expected_features = None # Liste der erwarteten Feature-Spalten fuer Vorhersage - self.logger.info("DataProcessor initialisiert mit Handlern.") + self.logger.info("DataProcessor initialisiert mit Handlern.") # <<< GEÄNDERT # Definieren Sie hier (oder als Klassenattribut) die Zuordnung von Schritt-Typen # zu den relevanten Spaltenschluesseln fuer die Statuspruefung. @@ -3301,7 +3301,7 @@ class DataProcessor: # Pruefen Sie, ob der Schluessel in COLUMN_MAP gefunden wurde if idx is None: # Logge einen Fehler, aber gebe einen leeren String zurueck - self.logger.error(f"_get_cell_value_safe: Schluessel '{column_key}' nicht in COLUMN_MAP gefunden.") + self.logger.error(f"_get_cell_value_safe: Schluessel '{column_key}' nicht in COLUMN_MAP gefunden.") # <<< GEÄNDERT return '' # Gebe leeren String zurueck, wenn Schluessel fehlt # Pruefen Sie, ob die Zeile lang genug ist, um auf diesen Index zuzugreifen @@ -3310,7 +3310,7 @@ class DataProcessor: return row[idx] if row[idx] is not None else '' else: # Logge auf Debug-Level, wenn der Index existiert, aber die Zeile zu kurz ist. - self.logger.debug( + self.logger.debug( # <<< GEÄNDERT f"_get_cell_value_safe: Index {idx} fuer '{column_key}' ist gueltig, " f"aber Zeile ist zu kurz (Laenge {len(row)}). Gebe leeren String zurueck." ) @@ -3531,7 +3531,7 @@ class DataProcessor: # else: self.logger.debug(" -> ML-Schaetzung nicht noetig (AV/AW fehlen)") # Zu viel Laerm im Debug - # Wenn AU gesetzt ist oder ein gueltiger Wert enthaelt, und kein Re-Eval erzwungen wird, ist der Schritt nicht noetig + # Wenn AU gesetzt ist oder einen gueltigen Wert enthaelt, und kein Re-Eval erzwungen wird, ist der Schritt nicht noetig # self.logger.debug(f" -> ML-Schaetzung nicht noetig (AU='{au_value}')") # Zu viel Laerm im Debug return False @@ -3568,7 +3568,7 @@ class DataProcessor: Defaults to False. """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - self.logger.info(f"--- Starte Verarbeitung fuer Zeile {row_num_in_sheet} {'(Re-Eval)' if force_reeval else ''} (Schritte: {', '.join(steps_to_run) if steps_to_run else 'Keine ausgewählt'}) ---") + self.logger.info(f"--- Starte Verarbeitung fuer Zeile {row_num_in_sheet} {'(Re-Eval)' if force_reeval else ''} (Schritte: {', '.join(steps_to_run) if steps_to_run else 'Keine ausgewählt'}) ---") # <<< GEÄNDERT # Liste zur Sammlung von Sheet-Updates fuer diese Zeile # Updates sind Dictionaries: {'range': 'A1', 'values': [['Wert']]} @@ -3633,12 +3633,6 @@ class DataProcessor: # --- Die Logik fuer die einzelnen Verarbeitungsschritte folgt in den naechsten Bloecken --- # Jeder Schritt prueft, ob er in steps_to_run enthalten ist UND (ob er laut Status noetig ist ODER force_reeval True ist). - # Website Handling (Block 20) folgt... - # Wikipedia Handling (Block 21) folgt... - # ChatGPT Evaluationen (Block 22) folgt... - # ML Prediction (Block 23) folgt... - # Finalisierung & Write (Block 23) folgt... - # ====================================================================== # === Verarbeitungsschritte innerhalb von _process_single_row ========== # ====================================================================== @@ -3666,12 +3660,12 @@ class DataProcessor: if not self._get_cell_value_safe(row_data, "Website Scrape Timestamp").strip(): grund_message_parts.append('AT leer') grund_message = ", ".join(grund_message_parts) - self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre WEBSITE Schritte aus (Grund: {grund_message})...") + self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre WEBSITE Schritte aus (Grund: {grund_message})...") # <<< GEÄNDERT # Website Lookup nur, wenn die URL in Spalte D (CRM Website) leer oder "k.A." ist # Nutzt die lokal gespeicherte Kopie der URL, die ggf. im Lookup ueberschrieben wird. if not website_url or website_url.lower() == "k.a.": - self.logger.debug(" -> Website URL (D) leer oder k.A., suche ueber SERP...") + self.logger.debug(" -> Website URL (D) leer oder k.A., suche ueber SERP...") # <<< GEÄNDERT # Annahme: serp_website_lookup global definiert (Block 10) und nutzt logging/retry try: # Der serp_website_lookup Aufruf ist mit retry_on_failure dekoriert. @@ -3683,22 +3677,22 @@ class DataProcessor: website_url = new_website # Fuegen Sie das Update fuer Spalte D zur Liste der Sheet-Updates hinzu updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["CRM Website"] + 1)}{row_num_in_sheet}', 'values': [[website_url]]}) - self.logger.info(f" -> Neue Website gefunden und fuer Update D:{row_num_in_sheet} vorgemerkt: {website_url[:100]}...") # Gekuerzt loggen + self.logger.info(f" -> Neue Website gefunden und fuer Update D:{row_num_in_sheet} vorgemerkt: {website_url[:100]}...") # <<< GEÄNDERT else: # Wenn keine neue Website gefunden wurde - self.logger.warning(f" -> Keine neue Website ueber SERP gefunden fuer '{company_name[:100]}...'.") # Gekuerzt loggen + self.logger.warning(f" -> Keine neue Website ueber SERP gefunden fuer '{company_name[:100]}...'.") # <<< GEÄNDERT # website_url bleibt leer oder k.A. in diesem Fall. except Exception as e_serp_lookup: # Wenn serp_website_lookup eine Exception wirft (nach Retries) - self.logger.error(f"FEHLER bei SERP Website Lookup fuer '{company_name[:100]}...': {e_serp_lookup}") # Gekuerzt loggen + self.logger.error(f"FEHLER bei SERP Website Lookup fuer '{company_name[:100]}...': {e_serp_lookup}") # <<< GEÄNDERT # Bei Fehler bleibt website_url unveraendert (leer oder k.A.). Fahren Sie fort. pass # Fahren Sie fort, falls eine URL im Sheet war oder gefunden wurde # Führen Sie Scraping und Zusammenfassung nur durch, wenn eine gueltige Website URL vorhanden ist (lokale Variable website_url) # Ueberpruefen Sie auf nicht-leere website_url und ungleich "k.A." oder Fehlerwerten. if website_url and website_url.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"]: - self.logger.debug(f" -> Scrape Rohtext von {website_url[:100]}...") # Gekuerzt loggen + self.logger.debug(f" -> Scrape Rohtext von {website_url[:100]}...") # <<< GEÄNDERT # Annahme: get_website_raw global definiert (Block 11) und nutzt logging/retry try: # Der get_website_raw Aufruf ist mit retry_on_failure dekoriert. @@ -3709,7 +3703,7 @@ class DataProcessor: # Zusammenfassung nur, wenn gueltiger Rohtext extrahiert wurde. # Pruefen Sie auf nicht-leeren raw_text und ungleich Standard-Fehlerwerten. if website_raw and str(website_raw).strip().lower() not in ["k.a.", "k.a. (nur cookie-banner erkannt)", "k.a. (fehler)"]: - self.logger.debug(f" -> Fasse Rohtext zusammen (Laenge: {len(str(website_raw))})...") + self.logger.debug(f" -> Fasse Rohtext zusammen (Laenge: {len(str(website_raw))})...") # <<< GEÄNDERT # Annahme: summarize_website_content global definiert (Block 9) und nutzt logging/retry try: # Der summarize_website_content Aufruf ist mit retry_on_failure dekoriert. @@ -3722,16 +3716,16 @@ class DataProcessor: except Exception as e_summary: # Wenn summarize_website_content eine Exception wirft (nach Retries) - self.logger.error(f"FEHLER bei Website Zusammenfassung fuer '{company_name[:100]}...': {e_summary}") # Gekuerzt loggen + self.logger.error(f"FEHLER bei Website Zusammenfassung fuer '{company_name[:100]}...': {e_summary}") # <<< GEÄNDERT # Setze die lokale Variable auf einen Fehlerwert - website_summary = f"k.A. (Fehler Zusammenfassung: {str(e)[:100]}...)" + website_summary = f"k.A. (Fehler Zusammenfassung: {str(e_summary)[:100]}...)" # Korrektur: e statt e_summary # Fuegen Sie ein Update mit dem Fehlerwert fuer Spalte AS hinzu updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [[website_summary]]}) pass # Fahren Sie fort else: # Wenn kein gueltiger Rohtext zum Zusammenfassen vorhanden war - self.logger.debug(" -> Kein gueltiger Rohtext zum Zusammenfassen vorhanden. Zusammenfassung uebersprungen.") + self.logger.debug(" -> Kein gueltiger Rohtext zum Zusammenfassen vorhanden. Zusammenfassung uebersprungen.") # <<< GEÄNDERT # Stellen Sie sicher, dass die lokale Variable korrekt gesetzt ist, falls nicht zusammengefasst website_summary = "k.A." # Fuege 'k.A.' Update fuer AS hinzu (nur wenn es vorher nicht k.A. war?) @@ -3744,9 +3738,9 @@ class DataProcessor: except Exception as e_scrape: # Wenn get_website_raw eine Exception wirft (nach Retries) - self.logger.error(f"FEHLER beim Website Scraping fuer '{company_name[:100]}' ({website_url[:100]}...): {e_scrape}") # Gekuerzt loggen + self.logger.error(f"FEHLER beim Website Scraping fuer '{company_name[:100]}' ({website_url[:100]}...): {e_scrape}") # <<< GEÄNDERT # Setze die lokalen Variablen auf Fehlerwerte - website_raw = f"k.A. (Fehler Scraping: {str(e)[:100]}...)" + website_raw = f"k.A. (Fehler Scraping: {str(e_scrape)[:100]}...)" # Korrektur: e statt e_scrape website_summary = "k.A. (Fehler Zusammenfassung)" # Zusammenfassung fehlschlaegt auch # Fuegen Sie Updates mit Fehlerwerten fuer AR und AS hinzu updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [[website_raw]]}) @@ -3755,7 +3749,7 @@ class DataProcessor: else: # Wenn keine gueltige Website URL vorhanden/gefunden wurde - self.logger.debug(f" -> Keine gueltige Website URL vorhanden/gefunden fuer '{company_name[:100]}...'. Website Verarbeitung uebersprungen.") # Gekuerzt loggen + self.logger.debug(f" -> Keine gueltige Website URL vorhanden/gefunden fuer '{company_name[:100]}...'. Website Verarbeitung uebersprungen.") # <<< GEÄNDERT # Stellen Sie sicher, dass AR und AS auf k.A. gesetzt werden, wenn der Schritt lief, aber keine URL da war. # Die lokalen Variablen behalten ihre initialen Werte (current_...) wenn der Schritt uebersprungen wurde, # aber wenn der Schritt lief, aber keine URL da war, sollten sie auf k.A. gesetzt werden. @@ -3809,7 +3803,7 @@ class DataProcessor: if self._get_cell_value_safe(row_data, "Chat Wiki Konsistenzpruefung").strip().upper() == "X (URL COPIED)": grund_message_parts.append("S='X (URL Copied)'") grund_message = ", ".join(grund_message_parts) - self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre WIKI Suche/Extraktion aus (Grund: {grund_message})...") + self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre WIKI Suche/Extraktion aus (Grund: {grund_message})...") # <<< GEÄNDERT # Holen Sie die aktuelle Wiki URL aus Spalte M (nutzt interne Helfer) url_in_m = self._get_cell_value_safe(row_data, "Wiki URL").strip() @@ -3832,20 +3826,20 @@ class DataProcessor: # Bestimmen Sie, ob eine neue Suche notwendig ist if status_s_indicates_reparse: # Wenn Status S signalisiert, dass eine neu kopierte URL extrahiert werden soll, fuehre immer eine Suche aus. - self.logger.warning(f" -> Status S ist 'X (URL Copied)', ignoriere URL '{url_in_m[:100]}...' in M und starte neue Suche...") # Gekuerzt loggen + self.logger.warning(f" -> Status S ist 'X (URL Copied)', ignoriere URL '{url_in_m[:100]}...' in M und starte neue Suche...") # <<< GEÄNDERT search_was_needed = True # Suche ist noetig elif force_reeval: # Wenn Re-Eval erzwungen wird - self.logger.debug(" -> Re-Eval Modus aktiv fuer Wiki-Schritt.") + self.logger.debug(" -> Re-Eval Modus aktiv fuer Wiki-Schritt.") # <<< GEÄNDERT # Wenn die URL in M existiert und gueltig aussieht if m_url_exists_and_looks_valid: # Im Re-Eval Modus nehmen wir die URL aus M an, OHNE erneute Validierung oder Suche (Vertrauen auf M!). - self.logger.info(f" -> Re-Eval: Nutze vorhandene URL aus Spalte M direkt: {url_in_m[:100]}...") # Gekuerzt loggen + self.logger.info(f" -> Re-Eval: Nutze vorhandene URL aus Spalte M direkt: {url_in_m[:100]}...") # <<< GEÄNDERT url_to_extract = url_in_m # Verwende die URL aus M direkt else: # Wenn M leer/ungueltig ist, auch im Re-Eval Modus neu suchen - self.logger.debug(f" -> Re-Eval: Spalte M ist leer oder ungueltig ('{url_in_m[:100]}...'). Starte neue Suche...") # Gekuerzt loggen + self.logger.debug(f" -> Re-Eval: Spalte M ist leer oder ungueltig ('{url_in_m[:100]}...'). Starte neue Suche...") # <<< GEÄNDERT search_was_needed = True # Suche ist noetig elif not an_value: @@ -3853,7 +3847,7 @@ class DataProcessor: # Wenn die URL in M existiert und gueltig aussieht if m_url_exists_and_looks_valid: # Wenn AN fehlt und M gefuellt ist, pruefen wir die Validitaet der M-URL ueber die wikipedia Bibliothek. - self.logger.debug(f" -> AN fehlt, pruefe Validitaet der URL aus M: {url_in_m[:100]}...") # Gekuerzt loggen + self.logger.debug(f" -> AN fehlt, pruefe Validitaet der URL aus M: {url_in_m[:100]}...") # <<< GEÄNDERT try: # Extrahieren des Titels aus der URL fuer wikipedia.page (nutzt globale Helfer) # Dieser Aufruf kann Exceptions werfen (PageError, DisambiguationError). @@ -3871,35 +3865,35 @@ class DataProcessor: # _validate_article kann interne Fehler haben (z.B. bei HTML Parsing), aber faengt sie. if self.wiki_scraper._validate_article(page_from_m, company_name, website_url): url_to_extract = page_from_m.url # Die URL ist valide und wird verwendet - self.logger.info(f" -> Vorhandene URL aus M '{url_to_extract[:100]}...' ist valide und wird verwendet.") # Gekuerzt loggen + self.logger.info(f" -> Vorhandene URL aus M '{url_to_extract[:100]}...' ist valide und wird verwendet.") # <<< GEÄNDERT else: # Wenn der Artikel aus M nicht validiert wird - self.logger.warning(f" -> Vorhandene URL aus M '{page_from_m.title[:100]}...' ist NICHT valide. Starte neue Suche...") # Gekuerzt loggen + self.logger.warning(f" -> Vorhandene URL aus M '{page_from_m.title[:100]}...' ist NICHT valide. Starte neue Suche...") # <<< GEÄNDERT search_was_needed = True # Suche ist noetig except (wikipedia.exceptions.PageError, wikipedia.exceptions.DisambiguationError) as e_wiki_m: # Wenn die URL in M zu einem nicht existierenden Artikel oder einer Begriffsklaerung fuehrt - self.logger.warning(f" -> Vorhandene URL aus M '{url_in_m[:100]}...' fuehrt zu Fehler ({type(e_wiki_m).__name__}). Starte neue Suche...") # Gekuerzt loggen + self.logger.warning(f" -> Vorhandene URL aus M '{url_in_m[:100]}...' fuehrt zu Fehler ({type(e_wiki_m).__name__}). Starte neue Suche...") # <<< GEÄNDERT # Logge die Disambiguation Optionen auf Debug, falls vorhanden if isinstance(e_wiki_m, wikipedia.exceptions.DisambiguationError): - self.logger.debug(f" -> Disambiguation Optionen: {str(e_wiki_m.options)[:100]}...") # Gekuerzt loggen + self.logger.debug(f" -> Disambiguation Optionen: {str(e_wiki_m.options)[:100]}...") # <<< GEÄNDERT search_was_needed = True # Suche ist noetig pass # Faert fort except Exception as e_val_m: # Fange andere unerwartete Fehler beim Pruefen der URL aus M ab (z.B. URL-Parsing-Fehler vor wikipedia.page) - self.logger.exception(f" -> Unerwarteter Fehler beim Pruefen der URL aus M '{url_in_m[:100]}...': {e_val_m}. Starte neue Suche...") # Gekuerzt loggen + self.logger.exception(f" -> Unerwarteter Fehler beim Pruefen der URL aus M '{url_in_m[:100]}...': {e_val_m}. Starte neue Suche...") # <<< GEÄNDERT search_was_needed = True # Suche ist noetig pass # Faert fort else: # M ist leer/ungueltig und AN fehlt -> Suche starten - self.logger.debug(f" -> AN fehlt und M leer/ungueltig ('{url_in_m[:100]}...'). Starte Wikipedia-Suche fuer '{company_name[:100]}...'...") # Gekuerzt loggen + self.logger.debug(f" -> AN fehlt und M leer/ungueltig ('{url_in_m[:100]}...'). Starte Wikipedia-Suche fuer '{company_name[:100]}...'...") # <<< GEÄNDERT search_was_needed = True # Suche ist noetig # --- Führe die Suche aus, wenn search_was_needed True ist --- if search_was_needed: - self.logger.debug(f" -> Fuehre Wikipedia Suche ueber scraper durch...") + self.logger.debug(f" -> Fuehre Wikipedia Suche ueber scraper durch...") # <<< GEÄNDERT try: # Rufe die search_company_article Methode des Scrapers auf. # search_company_article ist mit retry_on_failure dekoriert und wirft bei endgueltigem Fehler eine Exception. @@ -3909,16 +3903,16 @@ class DataProcessor: if validated_page: # Wenn ein validierter Artikel gefunden wurde, setze die URL, von der extrahiert werden soll. url_to_extract = validated_page.url - self.logger.info(f" -> Suche erfolgreich, validierte URL: {url_to_extract[:100]}...") # Gekuerzt loggen + self.logger.info(f" -> Suche erfolgreich, validierte URL: {url_to_extract[:100]}...") # <<< GEÄNDERT else: # Wenn die Suche keinen validierten Artikel fand - self.logger.debug(f" -> Suche fand keinen validierten Artikel fuer '{company_name[:100]}...'.") # Gekuerzt loggen + self.logger.debug(f" -> Suche fand keinen validierten Artikel fuer '{company_name[:100]}...'.") # <<< GEÄNDERT url_to_extract = 'Kein Artikel gefunden' # Signalisiert kein Artikel gefunden except Exception as e_wiki_search: # Wenn search_company_article eine Exception wirft (nach Retries) # Der Fehler wird bereits vom retry_on_failure Decorator geloggt. - self.logger.error(f"FEHLER bei Wikipedia Suche fuer '{company_name[:100]}...': {e_wiki_search}") # Gekuerzt loggen + self.logger.error(f"FEHLER bei Wikipedia Suche fuer '{company_name[:100]}...': {e_wiki_search}") # <<< GEÄNDERT url_to_extract = f"FEHLER bei Suche: {str(e_wiki_search)[:50]}..." # Signalisiert Fehler bei Suche (gekuerzt) # Pass, faert fort, um zumindest den Status zu setzen. pass @@ -3927,7 +3921,7 @@ class DataProcessor: # --- Datenextraktion, wenn eine URL bestimmt wurde, von der extrahiert werden soll --- # Extrahiere Daten, wenn url_to_extract einen Wert hat, der NICHT "Kein Artikel gefunden" oder ein Fehlerstring ist. if url_to_extract and isinstance(url_to_extract, str) and url_to_extract.lower() not in ['kein artikel gefunden'] and not url_to_extract.startswith("FEHLER"): - self.logger.debug(f" -> Extrahiere Daten von URL: {url_to_extract[:100]}...") # Gekuerzt loggen + self.logger.debug(f" -> Extrahiere Daten von URL: {url_to_extract[:100]}...") # <<< GEÄNDERT try: # Rufe die extract_company_data Methode des Scrapers auf. # extract_company_data ist mit retry_on_failure dekoriert und wirft bei endgueltigem Fehler eine Exception. @@ -3937,28 +3931,28 @@ class DataProcessor: if extracted_data and isinstance(extracted_data, dict) and extracted_data.get('url') != 'k.A.': # Pruefe auf gueltige Extraktion final_wiki_data = extracted_data # Aktualisiere die Arbeitskopie der Wiki-Daten mit den extrahierten Daten. wiki_data_updated_in_this_run = True # Markieren, dass extrahierte Daten da sind (Trigger fuer Chat). - self.logger.info(f" -> Datenextraktion von {url_to_extract[:100]}... erfolgreich.") # Gekuerzt loggen + self.logger.info(f" -> Datenextraktion von {url_to_extract[:100]}... erfolgreich.") # <<< GEÄNDERT else: # Wenn extrahierte Daten leer oder ungueltig sind (z.B. parse Fehler intern) - self.logger.error(f" -> Fehler bei Datenextraktion von {url_to_extract[:100]}... oder Extraktion war leer. Setze Daten auf 'k.A.'") # Gekuerzt loggen + self.logger.error(f" -> Fehler bei Datenextraktion von {url_to_extract[:100]}... oder Extraktion war leer. Setze Daten auf 'k.A.'") # <<< GEÄNDERT # Behalte die URL, aber setze alle anderen Felder auf k.A. oder Fehler. final_wiki_data = {'url': url_to_extract, 'first_paragraph': 'k.A. (Extraktion fehlgeschlagen)', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} wiki_data_updated_in_this_run = True # Markieren, dass die Daten ueberschrieben werden. except Exception as e_wiki_extract: # Wenn extract_company_data eine Exception wirft (nach Retries) - self.logger.error(f"FEHLER bei Wikipedia Datenextraktion von {url_to_extract[:100]}...: {e_wiki_extract}") # Gekuerzt loggen + self.logger.error(f"FEHLER bei Wikipedia Datenextraktion von {url_to_extract[:100]}...: {e_wiki_extract}") # <<< GEÄNDERT # Setze Daten auf k.A., behalte aber die URL, von der extrahiert werden sollte - final_wiki_data = {'url': url_to_extract, 'first_paragraph': f'k.A. (FEHLER Extraktion: {str(e)[:50]}...)', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} + final_wiki_data = {'url': url_to_extract, 'first_paragraph': f'k.A. (FEHLER Extraktion: {str(e_wiki_extract)[:50]}...)', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} # Korrektur: e -> e_wiki_extract wiki_data_updated_in_this_run = True # Markieren, dass die Daten ueberschrieben werden. pass # Faert fort else: # Wenn keine gueltige URL zum Extrahieren bestimmt wurde (z.B. Suche fand nichts oder Fehler bei Suche) - self.logger.debug(f" -> Keine gueltige URL zum Extrahieren bestimmt ('{url_to_extract}'). Wiki-Daten nicht extrahiert.") + self.logger.debug(f" -> Keine gueltige URL zum Extrahieren bestimmt ('{url_to_extract}'). Wiki-Daten nicht extrahiert.") # <<< GEÄNDERT # final_wiki_data behaelt die current_wiki_data Werte (initial geladen) oder wurde oben bei Suche auf "Kein Artikel gefunden"/"FEHLER" gesetzt. # Stelle sicher, dass final_wiki_data die richtige URL enthaelt, auch wenn keine Extraktion stattfand. - if url_to_extract in ['Kein Artikel gefunden', 'FEHLER bei Suche']: + if url_to_extract in ['Kein Artikel gefunden'] or (isinstance(url_to_extract, str) and url_to_extract.startswith("FEHLER")): # Korrektur Pruefung final_wiki_data['url'] = url_to_extract # Update nur die URL im Ergebnis # --- Sheet Updates fuer M-R und AN --- @@ -3985,7 +3979,8 @@ class DataProcessor: status_s_indicates_reparse = self._get_cell_value_safe(row_data, "Chat Wiki Konsistenzpruefung").strip().upper() == "X (URL COPIED)" # Pruefe, ob die FINAL_wiki_data URL (nach Suche/Extraktion) anders ist als die URSPRUENGLICHE URL in M im Sheet. # UND stelle sicher, dass die neue URL eine gueltige URL ist (nicht "k.A." oder Fehlerstring). - url_changed_and_valid = (url_in_m != final_wiki_data.get('url')) and isinstance(final_wiki_data.get('url'), str) and final_wiki_data.get('url').lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche"] + new_wiki_url = final_wiki_data.get('url') + url_changed_and_valid = (url_in_m != new_wiki_url) and isinstance(new_wiki_url, str) and new_wiki_url.lower() not in ["k.a.", "kein artikel gefunden"] and not new_wiki_url.startswith("FEHLER") # Korrektur Pruefung # Bestimme, ob S und AX zurueckgesetzt werden sollen if force_reeval or status_s_indicates_reparse or url_changed_and_valid: @@ -4008,10 +4003,10 @@ class DataProcessor: if url_changed_and_valid: grund_message_parts.append('URL geaendert und gueltig') grund_message_s_reset = ", ".join(grund_message_parts) - self.logger.info(f" -> Status S zurueckgesetzt auf '?' und Timestamp AX geleert fuer erneute Verifikation (Grund: {grund_message_s_reset}).") + self.logger.info(f" -> Status S zurueckgesetzt auf '?' und Timestamp AX geleert fuer erneute Verifikation (Grund: {grund_message_s_reset}).") # <<< GEÄNDERT else: # Logge Fehler, wenn Spaltenindizes fehlen - self.logger.error("FEHLER: Konnte Spaltenbuchstaben fuer S oder AX nicht ermitteln. Zuruecksetzen uebersprungen.") + self.logger.error("FEHLER: Konnte Spaltenbuchstaben fuer S oder AX nicht ermitteln. Zuruecksetzen uebersprungen.") # <<< GEÄNDERT # else if run_wiki_step: @@ -4030,6 +4025,7 @@ class DataProcessor: # Nutzt interne Helfer: _get_cell_value_safe, _needs_chat_evaluations. # Nutzt globale Helfer: COLUMN_MAP, logger, datetime, time, # evaluate_branche_chatgpt (Block 10), + # get_numeric_filter_value (Block 5) <- ERSETZT get_valid_numeric. # (Optional: evaluate_fsm_suitability, evaluate_employee_chatgpt, evaluate_umsatz_chatgpt - muessen implementiert werden). # Nutzt lokale Variablen: crm_branche, crm_beschreibung, final_wiki_data, website_summary, wiki_data_updated_in_this_run. @@ -4056,7 +4052,7 @@ class DataProcessor: if wiki_data_updated_in_this_run: grund_message_parts.append('Wiki Daten gerade aktualisiert') grund_message = ", ".join(grund_message_parts) - self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre CHATGPT Evaluationen aus (Grund: {grund_message})...") + self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre CHATGPT Evaluationen aus (Grund: {grund_message})...") # <<< GEÄNDERT # Hole die notwendigen Daten fuer die ChatGPT-Calls. # Nutzt die initial geladenen CRM-Daten und die finalen Daten aus den vorherigen Schritten (Wiki, Website). @@ -4065,204 +4061,7 @@ class DataProcessor: # website_summary wurde im Website-Schritt (Block 18) aktualisiert oder behaelt alte Werte. # --- 3a. Branchen-Einstufung (W, X, Y) --- - self.logger.debug(" -> Starte Branchen-Einstufung ueber ChatGPT...") - try: - # Annahme: evaluate_branche_chatgpt global definiert (Block 10) und nutzt logging/retry - # evaluate_branche_chatgpt braucht Zugriff auf globale ALLOWED_TARGET_BRANCHES und TARGET_SCHEMA_STRING (Block 7) - # Der Aufruf ist mit retry_on_failure dekoriert und wirft bei endgueltigem Fehler eine Exception. - branch_result = evaluate_branche_chatgpt( - crm_branche, # Nutzt initial geladenen CRM Wert - crm_beschreibung, # Nutzt initial geladenen CRM Wert - final_wiki_data.get('branche', 'k.A.'), # Nutzt ggf. neue Wiki-Branche aus Block 19 - final_wiki_data.get('categories', 'k.A.'), # Nutzt ggf. neue Wiki-Kategorien aus Block 19 - website_summary # Nutzt ggf. neue Website-Zusammenfassung aus Block 18 - ) - # Sammle Updates fuer die Branchen-Spalten (W, X, Y) (nutzt interne Helfer) - # Stellen Sie sicher, dass die Schluessel im Ergebnis-Dict vorhanden sind, Fallback auf Standard-Fehlerwerte. - 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", "error")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get("justification", "Keine Begruendung")]]}) - - except Exception as e_branch_eval: - # Wenn evaluate_branche_chatgpt eine Exception wirft (nach Retries) - # Der Fehler wird bereits vom retry_on_failure Decorator oder evaluate_branche_chatgpt geloggt. - self.logger.error(f"FEHLER bei Branchen-Einstufung ueber ChatGPT fuer Zeile {row_num_in_sheet}: {e_branch_eval}") - # Fuegen Sie Updates mit Fehlerwerten hinzu, um den Fehler im Sheet zu dokumentieren. - error_msg = f"Fehler: {str(e_branch_eval)[:100]}..." # Kuerze Fehlermeldung - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenz Branche"] + 1)}{row_num_in_sheet}', 'values': [['error']]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Branche"] + 1)}{row_num_in_sheet}', 'values': [[error_msg]]}) - pass # Fahren Sie fort mit den naechsten Schritten, auch wenn Branchenevaluation fehlschlug - - - # --- 3b. FSM Relevanz Bewertung (Z, AA) --- - # TODO: Implementieren Sie die Logik und den Aufruf der Funktion evaluate_fsm_suitability - self.logger.debug(" -> Starte FSM Relevanz Bewertung (Platzhalter)...") - # Beispielaufruf (angenommen, evaluate_fsm_suitability existiert global): - # try: - # fsm_result = evaluate_fsm_suitability( - # company_name, # Nutzt initial geladenen CRM Namen - # {'crm_desc': crm_beschreibung, 'wiki_paragraph': final_wiki_data.get('first_paragraph', 'k.A.'), 'web_summary': website_summary} - # ) - # # Sammle Updates fuer FSM Spalten (Z, AA) - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Pruefung FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [[fsm_result.get('suitability', 'k.A.')]]}) - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung fuer FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [[fsm_result.get('justification', 'k.A.')]]}) - # except Exception as e_fsm_eval: - # self.logger.error(f"FEHLER bei FSM Relevanz Bewertung fuer Zeile {row_num_in_sheet}: {e_fsm_eval}") - # # Fuege Fehler-Updates hinzu - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Pruefung FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung fuer FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [[f'Fehler: {str(e_fsm_eval)[:100]}...']]}) - # pass # Faert fort - - - # --- 3c. Mitarbeiterzahl Schaetzung (AB, AC, AD) --- - # TODO: Implementieren Sie die Logik und den Aufruf der Funktion evaluate_employee_chatgpt - self.logger.debug(" -> Starte Mitarbeiterzahl Schaetzung (Platzhalter)...") - # Beispielaufruf (angenommen, evaluate_employee_chatgpt existiert global): - # try: - # emp_estimate_result = evaluate_employee_chatgpt( - # company_name, # Nutzt initial geladenen CRM Namen - # {'crm_emp': self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter"), 'wiki_emp': final_wiki_data.get('mitarbeiter', 'k.A.'), 'wiki_paragraph': final_wiki_data.get('first_paragraph', 'k.A.'), 'web_summary': website_summary} - # ) - # # Sammle Updates fuer Mitarbeiter Schaetzspalten (AB, AC, AD) - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Anzahl Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[emp_estimate_result.get('estimate', 'k.A.')]]}) - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenzpruefung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [[emp_estimate_result.get('consistency', 'k.A.')]]}) - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [[emp_estimate_result.get('justification', 'k.A.')]]}) - # except Exception as e_emp_eval: - # self.logger.error(f"FEHLER bei Mitarbeiterzahl Schaetzung fuer Zeile {row_num_in_sheet}: {e_emp_eval}") - # # Fuege Fehler-Updates hinzu - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Anzahl Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenzpruefung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [['error']]}) - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [[f'Fehler: {str(e_emp_eval)[:100]}...']]}) - # pass # Faert fort - - - # --- 3d. Umsatz Schaetzung (AG, AH) --- - # TODO: Implementieren Sie die Logik und den Aufruf der Funktion evaluate_umsatz_chatgpt - self.logger.debug(" -> Starte Umsatz Schaetzung (Platzhalter)...") - # Beispielaufruf (angenommen, evaluate_umsatz_chatgpt existiert global): - # try: - # umsatz_estimate_result = evaluate_umsatz_chatgpt( - # company_name, # Nutzt initial geladenen CRM Namen - # {'crm_umsatz': self._get_cell_value_safe(row_data, "CRM Umsatz"), 'wiki_umsatz': final_wiki_data.get('umsatz', 'k.A.'), 'wiki_paragraph': final_wiki_data.get('first_paragraph', 'k.A.'), 'web_summary': website_summary} - # ) - # # Sammle Updates fuer Umsatz Schaetzspalten (AG, AH) - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[umsatz_estimate_result.get('estimate', 'k.A.')]]}) - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[umsatz_estimate_result.get('justification', 'k.A.')]]}) - # except Exception as e_umsatz_eval: - # self.logger.error(f"FEHLER bei Umsatz Schaetzung fuer Zeile {row_num_in_sheet}: {e_umsatz_eval}") - # # Fuege Fehler-Updates hinzu - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) - # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[f'Fehler: {str(e_umsatz_eval)[:100]}...']]}) - # pass # Faert fort - - - # --- 3e. Konsolidierung Umsatz/Mitarbeiter (AV, AW) --- - # Diese Logik wurde bisher in prepare_data_for_modeling (Block 31) verwendet, - # kann aber auch hier nach jeder Zeilenverarbeitung durchgefuehrt und - # ins Sheet geschrieben werden, um die konsolidierten Werte aktuell zu halten. - self.logger.debug(" -> Konsolidiere Umsatz (AV) und Mitarbeiter (AW) (Wiki > CRM Logik)...") - try: - # Nutzt globale Funktion get_valid_numeric (Block 5) - # Hole die Werte aus den entsprechenden Spalten (CRM und finale Wiki-Daten) - crm_umsatz_val = self._get_cell_value_safe(row_data, "CRM Umsatz") - wiki_umsatz_val = final_wiki_data.get('umsatz', 'k.A.') # Nutzt finalen Wiki-Wert aus Block 19 - - crm_ma_val = self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter") - wiki_ma_val = final_wiki_data.get('mitarbeiter', 'k.A.') # Nutzt finalen Wiki-Wert aus Block 19 - - # Konvertiere die Werte zu Numerisch (Float/Int oder NaN) mit get_valid_numeric - num_crm_umsatz = get_valid_numeric(crm_umsatz_val) - num_wiki_umsatz = get_valid_numeric(wiki_umsatz_val) - - num_crm_ma = get_valid_numeric(crm_ma_val) - num_wiki_ma = get_valid_numeric(wiki_ma_val) - - # Konsolidierung: Wiki hat Prioritaet vor CRM. Wenn beide NaN sind, Ergebnis NaN. - final_num_umsatz = num_wiki_umsatz if pd.notna(num_wiki_umsatz) else num_crm_umsatz - final_num_ma = num_wiki_ma if pd.notna(num_wiki_ma) else num_crm_ma - - # Konvertiere das finale numerische Ergebnis zurueck zu einem String ("Zahl" oder "k.A.") - # Runden Sie Umsatz auf ganze Millionen und Mitarbeiter auf ganze Zahlen. - final_umsatz_str = str(int(round(final_num_umsatz))) if pd.notna(final_num_umsatz) and final_num_umsatz > 0 else 'k.A.' - final_ma_str = str(int(round(final_num_ma))) if pd.notna(final_num_ma) and final_num_ma > 0 else 'k.A.' - - - # Sammle Updates fuer die Konsolidierungsspalten (AV, AW) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [[final_umsatz_str]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [[final_ma_str]]}) - self.logger.debug(f" -> Konsolidiert: Umsatz={final_umsatz_str}, MA={final_ma_str}") - - - except Exception as e_consolidate: - # Fange Fehler bei der Konsolidierung ab und logge sie - self.logger.error(f"FEHLER bei Konsolidierung Umsatz/Mitarbeiter fuer Zeile {row_num_in_sheet}: {e_consolidate}") - # Logge den Traceback - self.logger.debug(traceback.format_exc()) - # Fuege Fehler-Updates hinzu, um den Fehler im Sheet zu dokumentieren - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) - pass # Faert fort - - - # Setze den Timestamp letzte Pruefung (AO), da die ChatGPT-Evaluationen liefen (auch wenn fehlerhaft) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Pruefung"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) - - - # else if run_chat_step: - # Die Chat-Schritte waren angefordert, aber nicht noetig basierend auf Status/Re-Eval/Wiki-Update. - # Die lokalen Variablen final_wiki_data und website_summary behalten ihre initialen Werte (current_...). - # chat_eval_just_ran bleibt False. - # self.logger.debug(f"Zeile {row_num_in_sheet}: Ueberspringe CHATGPT Evaluationen (AO vorhanden, Wiki nicht aktualisiert und kein Re-Eval).") # Zu viel Laerm im Debug - - - # --- Der Code fuer den naechsten Verarbeitungsschritt (ML Prediction) folgt im naechsten Block --- - # Definition der Methode _process_single_row wird in der naechsten Nachricht fortgesetzt. - - # --- 3. ChatGPT Evaluationen (Branch, FSM, Emp, Umsatz Schaetzungen etc.) --- - # Dieser Schritt wird ausgefuehrt, wenn 'chat' in steps_to_run enthalten ist UND - # (_needs_chat_evaluations True ist ODER force_reeval True ist). - # _needs_chat_evaluations prueft AO oder ob Wiki-Daten in diesem Lauf gerade aktualisiert wurden. - # Nutzt interne Helfer: _get_cell_value_safe, _needs_chat_evaluations. - # Nutzt globale Helfer: COLUMN_MAP, logger, datetime, time, - # evaluate_branche_chatgpt (Block 10), - # get_valid_numeric (Block 5). - # (Optional: evaluate_fsm_suitability, evaluate_employee_chatgpt, evaluate_umsatz_chatgpt - muessen implementiert werden). - # Nutzt lokale Variablen: crm_branche, crm_beschreibung, final_wiki_data, website_summary, wiki_data_updated_in_this_run. - - # Pruefen Sie, ob die Chat-Schritte im aktuellen Lauf angefordert wurden - run_chat_step = 'chat' in steps_to_run - # Pruefen Sie, ob die Chat-Schritte laut Status, Re-Eval oder Wiki-Update noetig sind. - # wiki_data_just_updated_in_this_run ist ein Flag aus dem vorherigen Wiki-Schritt (Block 19). - chat_processing_needed_based_on_status = self._needs_chat_evaluations(row_data, force_reeval, wiki_data_updated_in_this_run) - - - # Wenn die Chat-Schritte angefordert wurden UND laut Status/Re-Eval noetig sind - if run_chat_step and chat_processing_needed_based_on_status: - any_processing_done = True # Markiere, dass in dieser Zeile etwas getan wird - - # Setzen Sie das Flag, dass Chat-Evaluationen liefen (koennte ML ausloesen Block 23) - chat_eval_just_ran = True - - # Bestimme den Grund fuer die Ausfuehrung dieses Schritts fuer das Logging - grund_message_parts = [] - if force_reeval: grund_message_parts.append('Re-Eval') - # Pruefe, ob der Timestamp AO leer ist (nutzt interne Helfer) - if not self._get_cell_value_safe(row_data, "Timestamp letzte Pruefung").strip(): grund_message_parts.append('AO leer') - # Pruefe, ob Wiki-Daten gerade aktualisiert wurden (Flag aus Block 19) - if wiki_data_updated_in_this_run: grund_message_parts.append('Wiki Daten gerade aktualisiert') - grund_message = ", ".join(grund_message_parts) - - self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre CHATGPT Evaluationen aus (Grund: {grund_message})...") - - # Hole die notwendigen Daten fuer die ChatGPT-Calls. - # Nutzt die initial geladenen CRM-Daten und die finalen Daten aus den vorherigen Schritten (Wiki, Website). - # crm_branche, crm_beschreibung wurden initial geladen (Block 17). - # final_wiki_data wurde im Wiki-Schritt (Block 19) aktualisiert oder behaelt alte Werte. - # website_summary wurde im Website-Schritt (Block 18) aktualisiert oder behaelt alte Werte. - - # --- 3a. Branchen-Einstufung (W, X, Y) --- - self.logger.debug(" -> Starte Branchen-Einstufung ueber ChatGPT...") + self.logger.debug(" -> Starte Branchen-Einstufung ueber ChatGPT...") # <<< GEÄNDERT try: # Annahme: evaluate_branche_chatgpt global definiert (Block 10) und nutzt logging/retry # evaluate_branche_chatgpt braucht Zugriff auf globale ALLOWED_TARGET_BRANCHES und TARGET_SCHEMA_STRING (Block 6) @@ -4283,9 +4082,9 @@ class DataProcessor: except Exception as e_branch_eval: # Wenn evaluate_branche_chatgpt eine Exception wirft (nach Retries) # Der Fehler wird bereits vom retry_on_failure Decorator oder evaluate_branche_chatgpt geloggt. - self.logger.error(f"FEHLER bei Branchen-Einstufung ueber ChatGPT fuer Zeile {row_num_in_sheet}: {e_branch_eval}") + self.logger.error(f"FEHLER bei Branchen-Einstufung ueber ChatGPT fuer Zeile {row_num_in_sheet}: {e_branch_eval}") # <<< GEÄNDERT # Logge den Traceback fuer detailliertere Fehlerinformationen - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT # Fuegen Sie Updates mit Fehlerwerten hinzu, um den Fehler im Sheet zu dokumentieren. error_msg = f"Fehler: {str(e_branch_eval)[:100]}..." # Kuerze Fehlermeldung updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) # Block 1 Column Map @@ -4296,7 +4095,7 @@ class DataProcessor: # --- 3b. FSM Relevanz Bewertung (Z, AA) --- # TODO: Implementieren Sie die Logik und den Aufruf der Funktion evaluate_fsm_suitability - self.logger.debug(" -> Starte FSM Relevanz Bewertung (Platzhalter)...") + self.logger.debug(" -> Starte FSM Relevanz Bewertung (Platzhalter)...") # <<< GEÄNDERT # Beispielaufruf (angenommen, evaluate_fsm_suitability existiert global): # try: # fsm_result = evaluate_fsm_suitability( @@ -4307,7 +4106,7 @@ class DataProcessor: # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Pruefung FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [[fsm_result.get('suitability', 'k.A.')]]}) # Block 1 Column Map # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung fuer FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [[fsm_result.get('justification', 'k.A.')]]}) # Block 1 Column Map # except Exception as e_fsm_eval: - # self.logger.error(f"FEHLER bei FSM Relevanz Bewertung fuer Zeile {row_num_in_sheet}: {e_fsm_eval}") + # self.logger.error(f"FEHLER bei FSM Relevanz Bewertung fuer Zeile {row_num_in_sheet}: {e_fsm_eval}") # <<< GEÄNDERT # # Fuege Fehler-Updates hinzu # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Pruefung FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) # Block 1 Column Map # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung fuer FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [[f'Fehler: {str(e_fsm_eval)[:100]}...']]}) # Block 1 Column Map @@ -4316,7 +4115,7 @@ class DataProcessor: # --- 3c. Mitarbeiterzahl Schaetzung (AB, AC, AD) --- # TODO: Implementieren Sie die Logik und den Aufruf der Funktion evaluate_employee_chatgpt - self.logger.debug(" -> Starte Mitarbeiterzahl Schaetzung (Platzhalter)...") + self.logger.debug(" -> Starte Mitarbeiterzahl Schaetzung (Platzhalter)...") # <<< GEÄNDERT # Beispielaufruf (angenommen, evaluate_employee_chatgpt existiert global): # try: # emp_estimate_result = evaluate_employee_chatgpt( @@ -4328,7 +4127,7 @@ class DataProcessor: # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenzpruefung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [[emp_estimate_result.get('consistency', 'k.A.')]]}) # Block 1 Column Map # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [[emp_estimate_result.get('justification', 'k.A.')]]}) # Block 1 Column Map # except Exception as e_emp_eval: - # self.logger.error(f"FEHLER bei Mitarbeiterzahl Schaetzung fuer Zeile {row_num_in_sheet}: {e_emp_eval}") + # self.logger.error(f"FEHLER bei Mitarbeiterzahl Schaetzung fuer Zeile {row_num_in_sheet}: {e_emp_eval}") # <<< GEÄNDERT # # Fuege Fehler-Updates hinzu # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Anzahl Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) # Block 1 Column Map # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenzpruefung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [['error']]}) # Block 1 Column Map @@ -4338,7 +4137,7 @@ class DataProcessor: # --- 3d. Umsatz Schaetzung (AG, AH) --- # TODO: Implementieren Sie die Logik und den Aufruf der Funktion evaluate_umsatz_chatgpt - self.logger.debug(" -> Starte Umsatz Schaetzung (Platzhalter)...") + self.logger.debug(" -> Starte Umsatz Schaetzung (Platzhalter)...") # <<< GEÄNDERT # Beispielaufruf (angenommen, evaluate_umsatz_chatgpt existiert global): # try: # umsatz_estimate_result = evaluate_umsatz_chatgpt( @@ -4349,7 +4148,7 @@ class DataProcessor: # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[umsatz_estimate_result.get('estimate', 'k.A.')]]}) # Block 1 Column Map # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[umsatz_estimate_result.get('justification', 'k.A.')]]}) # Block 1 Column Map # except Exception as e_umsatz_eval: - # self.logger.error(f"FEHLER bei Umsatz Schaetzung fuer Zeile {row_num_in_sheet}: {e_umsatz_eval}") + # self.logger.error(f"FEHLER bei Umsatz Schaetzung fuer Zeile {row_num_in_sheet}: {e_umsatz_eval}") # <<< GEÄNDERT # # Fuege Fehler-Updates hinzu # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) # Block 1 Column Map # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[f'Fehler: {str(e_umsatz_eval)[:100]}...']]}) # Block 1 Column Map @@ -4360,9 +4159,9 @@ class DataProcessor: # Diese Logik wurde bisher in prepare_data_for_modeling (Block 31) verwendet, # kann aber auch hier nach jeder Zeilenverarbeitung durchgefuehrt und # ins Sheet geschrieben werden, um die konsolidierten Werte aktuell zu halten. - self.logger.debug(" -> Konsolidiere Umsatz (AV) und Mitarbeiter (AW) (Wiki > CRM Logik)...") + self.logger.debug(" -> Konsolidiere Umsatz (AV) und Mitarbeiter (AW) (Wiki > CRM Logik)...") # <<< GEÄNDERT try: - # Nutzt globale Funktion get_valid_numeric (Block 5) + # Nutzt globale Funktion get_numeric_filter_value (Block 5) - ERSETZT get_valid_numeric # Hole die Werte aus den entsprechenden Spalten (CRM und finale Wiki-Daten) crm_umsatz_val = self._get_cell_value_safe(row_data, "CRM Umsatz") # Block 1 Column Map wiki_umsatz_val = final_wiki_data.get('umsatz', 'k.A.') # Nutzt finalen Wiki-Wert aus Block 19 @@ -4370,35 +4169,36 @@ class DataProcessor: crm_ma_val = self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter") # Block 1 Column Map wiki_ma_val = final_wiki_data.get('mitarbeiter', 'k.A.') # Nutzt finalen Wiki-Wert aus Block 19 - # Konvertiere die Werte zu Numerisch (Float/Int oder NaN) mit get_valid_numeric - num_crm_umsatz = get_valid_numeric(crm_umsatz_val) - num_wiki_umsatz = get_valid_numeric(wiki_umsatz_val) + # Konvertiere die Werte zu Numerisch (Float/Int oder 0) mit get_numeric_filter_value + num_crm_umsatz = get_numeric_filter_value(crm_umsatz_val, is_umsatz=True) + num_wiki_umsatz = get_numeric_filter_value(wiki_umsatz_val, is_umsatz=True) - num_crm_ma = get_valid_numeric(crm_ma_val) - num_wiki_ma = get_valid_numeric(wiki_ma_val) + num_crm_ma = get_numeric_filter_value(crm_ma_val, is_umsatz=False) + num_wiki_ma = get_numeric_filter_value(wiki_ma_val, is_umsatz=False) - # Konsolidierung: Wiki hat Prioritaet vor CRM. Wenn beide NaN sind, Ergebnis NaN. - final_num_umsatz = num_wiki_umsatz if pd.notna(num_wiki_umsatz) else num_crm_umsatz - final_num_ma = num_wiki_ma if pd.notna(num_wiki_ma) else num_crm_ma + # Konsolidierung: Wiki hat Prioritaet vor CRM. Wenn Wiki ungueltig (0), nehme CRM. + # WICHTIG: 0 ist hier das Kennzeichen fuer ungueltig/nicht parsebar/k.A. in get_numeric_filter_value + final_num_umsatz = num_wiki_umsatz if num_wiki_umsatz > 0 else num_crm_umsatz + final_num_ma = num_wiki_ma if num_wiki_ma > 0 else num_crm_ma # Konvertiere das finale numerische Ergebnis zurueck zu einem String ("Zahl" oder "k.A.") # Runden Sie Umsatz auf ganze Millionen und Mitarbeiter auf ganze Zahlen. # Stellen Sie sicher, dass nur positive Werte als Zahl ausgegeben werden, sonst "k.A.". - final_umsatz_str = str(int(round(final_num_umsatz))) if pd.notna(final_num_umsatz) and final_num_umsatz > 0 else 'k.A.' - final_ma_str = str(int(round(final_num_ma))) if pd.notna(final_num_ma) and final_num_ma > 0 else 'k.A.' + final_umsatz_str = str(int(round(final_num_umsatz))) if final_num_umsatz > 0 else 'k.A.' + final_ma_str = str(int(round(final_num_ma))) if final_num_ma > 0 else 'k.A.' # Sammle Updates fuer die Konsolidierungsspalten (AV, AW) (nutzt interne Helfer) updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [[final_umsatz_str]]}) # Block 1 Column Map updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [[final_ma_str]]}) # Block 1 Column Map - self.logger.debug(f" -> Konsolidiert: Umsatz={final_umsatz_str}, MA={final_ma_str}") + self.logger.debug(f" -> Konsolidiert: Umsatz={final_umsatz_str}, MA={final_ma_str}") # <<< GEÄNDERT except Exception as e_consolidate: # Fange Fehler bei der Konsolidierung ab und logge sie - self.logger.error(f"FEHLER bei Konsolidierung Umsatz/Mitarbeiter fuer Zeile {row_num_in_sheet}: {e_consolidate}") + self.logger.error(f"FEHLER bei Konsolidierung Umsatz/Mitarbeiter fuer Zeile {row_num_in_sheet}: {e_consolidate}") # <<< GEÄNDERT # Logge den Traceback - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT # Fuege Fehler-Updates hinzu, um den Fehler im Sheet zu dokumentieren updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) # Block 1 Column Map updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) # Block 1 Column Map @@ -4443,10 +4243,10 @@ class DataProcessor: if force_reeval: grund_message_parts.append('Re-Eval') # Wenn nicht Re-Eval, dann liegt es an _needs_ml_prediction. Logge den Grund von dort auf Debug. if not force_reeval: - self.logger.debug(" -> ML-Schaetzung noetig (Grund laut _needs_ml_prediction).") + self.logger.debug(" -> ML-Schaetzung noetig (Grund laut _needs_ml_prediction).") # <<< GEÄNDERT pass # Der spezifische Grund wird bereits in _needs_ml_prediction geloggt (auf Debug). - self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre ML-Schaetzung aus...") + self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre ML-Schaetzung aus...") # <<< GEÄNDERT # Die ML-Schaetzung benoetigt die vorbereiteten Daten (konsolidierter Umsatz/Mitarbeiter und Branche). # Diese Werte sind bereits in der Zeile im Sheet verfuegbar (Spalten AV, AW) @@ -4464,20 +4264,20 @@ class DataProcessor: if predicted_bucket and isinstance(predicted_bucket, str) and not predicted_bucket.startswith("FEHLER"): # Sammle Update fuer den AU Bucket (Geschaetzter Techniker Bucket) (nutzt interne Helfer) updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', 'values': [[predicted_bucket]]}) # Block 1 Column Map - self.logger.info(f" -> ML-Schaetzung erfolgreich: Bucket '{predicted_bucket}'.") + self.logger.info(f" -> ML-Schaetzung erfolgreich: Bucket '{predicted_bucket}'.") # <<< GEÄNDERT else: # Wenn die Vorhersage fehlschlug oder kein Ergebnis lieferte - self.logger.warning(f" -> ML-Schaetzung lieferte kein gueltiges Ergebnis: '{predicted_bucket}'.") + self.logger.warning(f" -> ML-Schaetzung lieferte kein gueltiges Ergebnis: '{predicted_bucket}'.") # <<< GEÄNDERT # Setze einen Fehlerwert im Sheet updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', 'values': [['k.A. (Schaetzung fehlgeschlagen)']]}) # Block 1 Column Map except Exception as e_ml: # Wenn _predict_technician_bucket eine Exception wirft - self.logger.error(f"FEHLER bei ML-Schaetzung fuer Zeile {row_num_in_sheet}: {e_ml}") + self.logger.error(f"FEHLER bei ML-Schaetzung fuer Zeile {row_num_in_sheet}: {e_ml}") # <<< GEÄNDERT # Logge den Traceback - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT # Fuege Fehler-Update hinzu, um den Fehler im Sheet zu dokumentieren - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', 'values': [[f'FEHLER Schaetzung: {str(e)[:50]}...']]}) # Block 1 Column Map + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', 'values': [[f'FEHLER Schaetzung: {str(e_ml)[:50]}...']]}) # Block 1 Column Map pass # Faert fort @@ -4500,7 +4300,7 @@ class DataProcessor: # Fuege das Update fuer die Version zur Liste hinzu updates.append({'range': f'{self.sheet_handler._get_col_letter(version_col_idx + 1)}{row_num_in_sheet}', 'values': [[getattr(Config, 'VERSION', 'unknown')]]}) # Block 1 Column Map else: - self.logger.error("FEHLER: Spaltenschluessel 'Version' nicht in COLUMN_MAP gefunden.") + self.logger.error("FEHLER: Spaltenschluessel 'Version' nicht in COLUMN_MAP gefunden.") # <<< GEÄNDERT # Tokens (AQ) - Hier ist die Zaehlung komplex, da mehrere OpenAI-Calls passiert sein koennten. # Eine einfache Loesung ist, die Token-Zahl der letzten relevanten Antwort zu speichern @@ -4533,20 +4333,20 @@ class DataProcessor: # Es wird nur geloescht, wenn die Zeile ansonsten erfolgreich bis hierhin kam und Updates gesammelt wurden. # Wenn eine schwere Exception in _process_single_row auftrat, wird dieser Block nicht erreicht. updates.append({'range': f'{flag_col_letter}{row_num_in_sheet}', 'values': [['']]}) - self.logger.debug(f" -> Update zum Loeschen des ReEval-Flags (A{row_num_in_sheet}) vorgemerkt.") + self.logger.debug(f" -> Update zum Loeschen des ReEval-Flags (A{row_num_in_sheet}) vorgemerkt.") # <<< GEÄNDERT else: # Logge Fehler, wenn Spaltenbuchstaben nicht ermittelt werden konnten - self.logger.error(f"FEHLER: Konnte Spaltenbuchstaben fuer 'ReEval Flag' ({reeval_col_idx+1}) nicht ermitteln. Flag kann nicht geloescht werden.") + self.logger.error(f"FEHLER: Konnte Spaltenbuchstaben fuer 'ReEval Flag' ({reeval_col_idx+1}) nicht ermitteln. Flag kann nicht geloescht werden.") # <<< GEÄNDERT else: # Logge Fehler, wenn Spaltenindex fehlt - self.logger.error("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden. Flag kann nicht geloescht werden.") + self.logger.error("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden. Flag kann nicht geloescht werden.") # <<< GEÄNDERT # --- 6. Batch Update fuer diese Zeile --- # Fuehren Sie das Batch-Update fuer ALLE gesammelten Aenderungen dieser EINEN Zeile durch. if updates: # Info-Log ueber die Anzahl der Updates fuer diese spezifische Zeile - self.logger.info(f"Zeile {row_num_in_sheet}: Sende Batch-Update mit {len(updates)} Operationen fuer diese Zeile...") + self.logger.info(f"Zeile {row_num_in_sheet}: Sende Batch-Update mit {len(updates)} Operationen fuer diese Zeile...") # <<< GEÄNDERT # Rufe die batch_update_cells Methode des Sheet Handlers auf. # batch_update_cells ist mit retry_on_failure dekoriert und loggt intern. success = self.sheet_handler.batch_update_cells(updates) # Nutzt die uebergeordnete Instanz @@ -4554,17 +4354,17 @@ class DataProcessor: # Wenn der Batch-Update fehlschlaegt (nach Retries) if not success: # Logge einen Error - self.logger.error(f"Zeile {row_num_in_sheet}: ENDGUELTIGER FEHLER beim Batch-Update nach Retries.") + self.logger.error(f"Zeile {row_num_in_sheet}: ENDGUELTIGER FEHLER beim Batch-Update nach Retries.") # <<< GEÄNDERT # Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte schreiben # (Dieses Update muesste separat oder im naechsten Lauf behandelt werden) else: # Info-Log, wenn nichts zu tun war in dieser Zeile if not any_processing_done: - self.logger.debug(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben (alle relevanten Schritte uebersprungen oder nicht angefordert).") + self.logger.debug(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben (alle relevanten Schritte uebersprungen oder nicht angefordert).") # <<< GEÄNDERT # else: # Dieser Fall sollte nicht eintreten, wenn updates nicht leer ist, aber any_processing_done True ist. - # self.logger.warning(f"Zeile {row_num_in_sheet}: Updates Liste war leer, aber any_processing_done=True. Pruefen Sie die Logik.") + # self.logger.warning(f"Zeile {row_num_in_sheet}: Updates Liste war leer, aber any_processing_done=True. Pruefen Sie die Logik.") # <<< GEÄNDERT # Kleine Pause nach der Verarbeitung jeder Zeile, um API-Limits zu respektieren @@ -4577,7 +4377,7 @@ class DataProcessor: time.sleep(pause_duration) # Logge den Abschluss der Verarbeitung fuer diese Zeile - self.logger.info(f"--- Verarbeitung fuer Zeile {row_num_in_sheet} abgeschlossen ---") + self.logger.info(f"--- Verarbeitung fuer Zeile {row_num_in_sheet} abgeschlossen ---") # <<< GEÄNDERT # --- Ende der _process_single_row Methode --- @@ -4625,11 +4425,11 @@ class DataProcessor: # Pruefen Sie, ob num_to_process gueltig ist if num_to_process is None or not isinstance(num_to_process, int) or num_to_process <= 0: - self.logger.info("Sequentielle Verarbeitung uebersprungen: num_to_process ist ungueltig oder <= 0.") + self.logger.info("Sequentielle Verarbeitung uebersprungen: num_to_process ist ungueltig oder <= 0.") # <<< GEÄNDERT return # Logge die Konfiguration des sequentiellen Laufs - self.logger.info(f"Starte sequentielle Verarbeitung von {num_to_process} Zeilen ab Sheet-Zeile {start_sheet_row}...") + self.logger.info(f"Starte sequentielle Verarbeitung von {num_to_process} Zeilen ab Sheet-Zeile {start_sheet_row}...") # <<< GEÄNDERT # Logge die ausgewaehlten Schritte fuer diesen Lauf selected_steps_log = [] if process_wiki_steps: selected_steps_log.append("Wiki (wiki)") @@ -4637,11 +4437,11 @@ class DataProcessor: if process_website_steps: selected_steps_log.append("Website (web)") if process_ml_steps: selected_steps_log.append("ML Predict (ml_predict)") # Neues Flag # Fuegen Sie hier weitere Schritte hinzu, wenn neue Flags existieren - self.logger.info(f" Ausgewaehlte Schritte fuer sequentiellen Lauf: {', '.join(selected_steps_log) if selected_steps_log else 'Keine ausgewählt!'}") + self.logger.info(f" Ausgewaehlte Schritte fuer sequentiellen Lauf: {', '.join(selected_steps_log) if selected_steps_log else 'Keine ausgewählt!'}") # <<< GEÄNDERT # Logge, ob force_reeval in _process_single_row gesetzt wird if force_reeval_in_single_row: - self.logger.warning(" !!! force_reeval=True wird fuer alle Zeilen in _process_single_row gesetzt !!!") + self.logger.warning(" !!! force_reeval=True wird fuer alle Zeilen in _process_single_row gesetzt !!!") # <<< GEÄNDERT # Erstelle das Set der Schluessel fuer die Schritte, die an _process_single_row uebergeben werden steps_to_run_set = set() @@ -4654,14 +4454,14 @@ class DataProcessor: # Wenn keine Schritte ausgewaehlt wurden (trotz gueltigem num_to_process) if not steps_to_run_set: - self.logger.warning("Keine Verarbeitungsschritte fuer sequentiellen Lauf ausgewaehlt (steps_to_run_set ist leer). Modus wird uebersprungen.") + self.logger.warning("Keine Verarbeitungsschritte fuer sequentiellen Lauf ausgewaehlt (steps_to_run_set ist leer). Modus wird uebersprungen.") # <<< GEÄNDERT return # Lade Daten einmalig vor der Verarbeitung (nutzt die uebergeordnete Instanz) # Der load_data Aufruf ist mit retry_on_failure dekoriert. if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer sequentielle Verarbeitung.") + self.logger.error("Fehler beim Laden der Daten fuer sequentielle Verarbeitung.") # <<< GEÄNDERT return # Beende die Methode, wenn das Laden fehlschlaegt @@ -4675,11 +4475,11 @@ class DataProcessor: # Pruefen Sie, ob der angegebene Startindex gueltig ist if start_index_in_all_data >= total_sheet_rows: - self.logger.warning(f"Start-Sheet-Zeile {start_sheet_row} (Index {start_index_in_all_data}) liegt ausserhalb der verfuegbaren Daten ({total_sheet_rows} Zeilen insgesamt). Keine Verarbeitung.") + self.logger.warning(f"Start-Sheet-Zeile {start_sheet_row} (Index {start_index_in_all_data}) liegt ausserhalb der verfuegbaren Daten ({total_sheet_rows} Zeilen insgesamt). Keine Verarbeitung.") # <<< GEÄNDERT return # Beende die Methode, wenn der Startindex ungueltig ist if start_index_in_all_data < header_rows: # Wenn der Startindex innerhalb der Header liegt, beginnen Sie nach den Headern. - self.logger.warning(f"Start-Sheet-Zeile {start_sheet_row} liegt innerhalb der Header-Zeilen ({header_rows} Header). Verarbeitung startet ab Sheet-Zeile {header_rows + 1}.") + self.logger.warning(f"Start-Sheet-Zeile {start_sheet_row} liegt innerhalb der Header-Zeilen ({header_rows} Header). Verarbeitung startet ab Sheet-Zeile {header_rows + 1}.") # <<< GEÄNDERT start_index_in_all_data = header_rows # Beginnen Sie direkt nach den Headern @@ -4690,11 +4490,11 @@ class DataProcessor: # Logge den Bereich der tatsaechlich zu verarbeitenden Zeilen - self.logger.info(f"Sequentielle Verarbeitung: Verarbeitungsbereich (0-basiert Index) [{start_index_in_all_data}, {end_index_in_all_data}). Entsprechende Sheet-Zeilen (1-basiert): {start_index_in_all_data + 1} bis {end_index_in_all_data}.") + self.logger.info(f"Sequentielle Verarbeitung: Verarbeitungsbereich (0-basiert Index) [{start_index_in_all_data}, {end_index_in_all_data}). Entsprechende Sheet-Zeilen (1-basiert): {start_index_in_all_data + 1} bis {end_index_in_all_data}.") # <<< GEÄNDERT # Pruefen Sie, ob es ueberhaupt Zeilen im berechneten Bereich gibt if start_index_in_all_data >= end_index_in_all_data: - self.logger.info(f"Berechneter Startindex ({start_index_in_all_data}) liegt bei oder nach dem berechneten Endindex ({end_index_in_all_data}). Keine Zeilen im definierten Bereich zu verarbeiten.") + self.logger.info(f"Berechneter Startindex ({start_index_in_all_data}) liegt bei oder nach dem berechneten Endindex ({end_index_in_all_data}). Keine Zeilen im definierten Bereich zu verarbeiten.") # <<< GEÄNDERT return # Beende die Methode, wenn der Bereich leer ist @@ -4706,7 +4506,7 @@ class DataProcessor: # Ueberspringen Sie Header-Zeilen explizit, falls der Startindex faelschlicherweise <= header_rows war if row_num_in_sheet <= header_rows: - self.logger.debug(f"Ueberspringe Header-Zeile {row_num_in_sheet}.") + self.logger.debug(f"Ueberspringe Header-Zeile {row_num_in_sheet}.") # <<< GEÄNDERT continue # Springe zur naechsten Iteration @@ -4714,7 +4514,7 @@ class DataProcessor: # Nutzt die interne Helferfunktion _get_cell_value_safe implizit durch Iteration oder prueft direkt # Eine einfache Pruefung: Ist irgendeine Zelle in der Zeile nicht leer oder None? if not any(cell and isinstance(cell, str) and cell.strip() for cell in row_data): - self.logger.debug(f"Ueberspringe scheinbar leere Zeile {row_num_in_sheet}.") + self.logger.debug(f"Ueberspringe scheinbar leere Zeile {row_num_in_sheet}.") # <<< GEÄNDERT continue # Springe zur naechsten Iteration @@ -4737,7 +4537,7 @@ class DataProcessor: except Exception as e_proc: # Wenn _process_single_row einen Fehler wirft (nachdem interne Retries aufgaben), # fangen wir ihn hier, loggen ihn und fahren mit der naechsten Zeile fort. - self.logger.exception(f"FEHLER bei sequentieller Verarbeitung von Zeile {row_num_in_sheet}: {e_proc}") + self.logger.exception(f"FEHLER bei sequentieller Verarbeitung von Zeile {row_num_in_sheet}: {e_proc}") # <<< GEÄNDERT # Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte im Sheet schreiben lassen. # Dieses Update muesste dann separat oder im naechsten Lauf behandelt werden. @@ -4747,7 +4547,7 @@ class DataProcessor: # time.sleep(0.1) # Optional: Kurze Pause bei Fehler # Logge den Abschluss der sequentiellen Verarbeitung - self.logger.info(f"Sequentielle Verarbeitung abgeschlossen. {processed_count} Zeilen im Bereich [{start_sheet_row}, {end_index_in_all_data}] bearbeitet.") + self.logger.info(f"Sequentielle Verarbeitung abgeschlossen. {processed_count} Zeilen im Bereich [{start_sheet_row}, {end_index_in_all_data}] bearbeitet.") # <<< GEÄNDERT # ============================================================================== @@ -4776,7 +4576,7 @@ class DataProcessor: Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind. Ruft _process_single_row fuer jede dieser Zeilen auf mit force_reeval=True. Verarbeitet maximal row_limit Zeilen. - Loescht optional das 'x'-Flag nach erfolgreicher Verarbeitung. + Loescht optional das 'x'-Flag nach erfolgreicher Verarbeitung (innerhalb von _process_single_row). Erlaubt die Auswahl spezifischer Verarbeitungsschritte. Args: @@ -4792,7 +4592,7 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Re-Eval Laufs - self.logger.info(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}") + self.logger.info(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}") # <<< GEÄNDERT # Logge die ausgewaehlten Schritte fuer diesen Lauf selected_steps_log = [] if process_wiki_steps: selected_steps_log.append("Wiki (wiki)") @@ -4800,7 +4600,8 @@ class DataProcessor: if process_website_steps: selected_steps_log.append("Website (web)") if process_ml_steps: selected_steps_log.append("ML Predict (ml_predict)") # Neues Flag # Fuegen Sie hier weitere Schritte hinzu, wenn neue Flags existieren - self.logger.info(f"Ausgewaehlte Schritte fuer Re-Eval: {', '.join(selected_steps_log) if selected_steps_log else 'Keine ausgewählt!'}") + self.logger.info(f"Ausgewaehlte Schritte fuer Re-Eval: {', '.join(selected_steps_log) if selected_steps_log else 'Keine ausgewählt!'}") # <<< GEÄNDERT + # Erstelle das Set der Schluessel fuer die Schritte, die an _process_single_row uebergeben werden steps_to_run_set = set() @@ -4813,14 +4614,14 @@ class DataProcessor: # Wenn keine Schritte ausgewaehlt wurden if not steps_to_run_set: - self.logger.warning("Keine Verarbeitungsschritte fuer Re-Eval ausgewaehlt (steps_to_run_set ist leer). Modus wird uebersprungen.") + self.logger.warning("Keine Verarbeitungsschritte fuer Re-Eval ausgewaehlt (steps_to_run_set ist leer). Modus wird uebersprungen.") # <<< GEÄNDERT return # Daten neu laden vor der Verarbeitung (nutzt die uebergeordnete Instanz) # Der load_data Aufruf ist mit retry_on_failure dekoriert. if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer Re-Evaluation.") + self.logger.error("Fehler beim Laden der Daten fuer Re-Evaluation.") # <<< GEÄNDERT return # Beende die Methode, wenn das Laden fehlschlaegt @@ -4830,14 +4631,14 @@ class DataProcessor: header_rows = self.sheet_handler._header_rows # Wenn keine Daten da sind oder nur Header if not all_data or len(all_data) <= header_rows: - self.logger.warning("Keine Datenzeilen fuer Re-Evaluation gefunden.") + self.logger.warning("Keine Datenzeilen fuer Re-Evaluation gefunden.") # <<< GEÄNDERT return # Beende die Methode # Ermitteln Sie den Index der ReEval Flag Spalte aus COLUMN_MAP (Block 1) reeval_col_idx = COLUMN_MAP.get("ReEval Flag") if reeval_col_idx is None: - self.logger.critical("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden. Kann Zeilen mit 'x' nicht finden. Breche ab.") + self.logger.critical("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden. Kann Zeilen mit 'x' nicht finden. Breche ab.") # <<< GEÄNDERT return # Beende die Methode bei kritischem Fehler @@ -4858,38 +4659,38 @@ class DataProcessor: found_count = len(rows_to_process) # Anzahl der gefundenen markierten Zeilen - self.logger.info(f"{found_count} Zeilen mit ReEval-Flag 'x' gefunden.") + self.logger.info(f"{found_count} Zeilen mit ReEval-Flag 'x' gefunden.") # <<< GEÄNDERT # Wenn keine Zeilen zum Verarbeiten markiert sind if found_count == 0: - self.logger.info("Keine Zeilen zur Re-Evaluation markiert.") + self.logger.info("Keine Zeilen zur Re-Evaluation markiert.") # <<< GEÄNDERT return # Beende die Methode # Verarbeitung der markierten Zeilen processed_count = 0 # Zaehlt Zeilen, fuer die _process_single_row aufgerufen wurde (im Rahmen des Limits). - # updates_clear_flag Liste wird NICHT mehr hier gefuellt, da _process_single_row das Update selbst hinzufuegt. - # rows_actually_processed = [] # Diese Liste wird nicht mehr benoetigt, da _process_single_row das Update selbst sendet. + # Die Liste updates_clear_flag wird NICHT mehr hier gefuellt, da _process_single_row das Update selbst hinzufuegt (Block 21). + # Die Liste rows_actually_processed wird nicht mehr benoetigt. # Iteriere ueber die gefundenen markierten Zeilen for task in rows_to_process: # Ueberpruefen Sie das Limit fuer die zu verarbeitenden Zeilen VOR der Verarbeitung if row_limit is not None and isinstance(row_limit, int) and row_limit > 0 and processed_count >= row_limit: # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Zeilenlimit ({row_limit}) fuer Re-Evaluation erreicht. Breche weitere Verarbeitung ab.") + self.logger.info(f"Zeilenlimit ({row_limit}) fuer Re-Evaluation erreicht. Breche weitere Verarbeitung ab.") # <<< GEÄNDERT break # Brich die Schleife ab row_num = task['row_num'] # 1-basierte Zeilennummer row_data = task['data'] # Die Rohdaten fuer diese Zeile - self.logger.info(f"Bearbeite Re-Eval Zeile {row_num}...") + self.logger.info(f"Bearbeite Re-Eval Zeile {row_num}...") # <<< GEÄNDERT try: - # Rufe die Methode zur Verarbeitung einer einzelnen Zeile auf. + # Rufe die Methode zur Verarbeitung einer einzelnen Zeile auf (_process_single_row Block 19). # In diesem Modus setzen wir immer force_reeval=True. # Wir uebergeben die aus CLI/Menue ausgewaehlten Schritte in steps_to_run_set. # Wir uebergeben das clear_flag, damit _process_single_row weiss, ob das 'x' geloescht werden soll. - # _process_single_row (Block 19) loggt intern, fuehrt die Schritte durch, sammelt Updates - # (inkl. 'x'-Flag Update wenn clear_x_flag=True) und sendet das Batch-Update fuer diese Zeile. + # _process_single_row fuehrt die Schritte durch, sammelt Updates (inkl. 'x'-Flag Update wenn clear_x_flag=True) + # und sendet das Batch-Update fuer diese Zeile. self._process_single_row( row_num_in_sheet = row_num, row_data = row_data, @@ -4906,7 +4707,7 @@ class DataProcessor: # Wenn _process_single_row einen Fehler wirft (nachdem interne Retries aufgaben), # fangen wir ihn hier, loggen ihn und fahren mit der naechsten Zeile fort. # Das 'x'-Flag wird in diesem Fall NICHT geloescht, da _process_single_row nicht bis zum Ende kam. - self.logger.exception(f"FEHLER bei Re-Evaluation von Zeile {row_num}: {e_proc}") + self.logger.exception(f"FEHLER bei Re-Evaluation von Zeile {row_num}: {e_proc}") # <<< GEÄNDERT # Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte im Sheet schreiben lassen. # Dieses Update muesste dann separat oder im naechsten Lauf behandelt werden. @@ -4915,10 +4716,11 @@ class DataProcessor: # Wenn _process_single_row eine Exception wirft, kann hier eine kurze Pause sinnvoll sein. # time.sleep(0.1) # Optional: Kurze Pause bei Fehler nach Exception + # Der Codeblock zum Loeschen der gesammelten Updates (updates_clear_flag) am Ende wurde entfernt. # Logge den Abschluss des Re-Eval Modus - self.logger.info(f"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Gefunden: {found_count}, Limit: {row_limit}).") + self.logger.info(f"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Gefunden: {found_count}, Limit: {row_limit}).") # <<< GEÄNDERT # ============================================================================== @@ -4931,8 +4733,8 @@ class DataProcessor: # --- Interne Hilfsfunktion fuer Wiki-Verifizierungs-Batch (OpenAI Call) --- # Diese Funktion verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen ueber OpenAI. - # Sie wird von process_verification_batch aufgerufen. - # Nutzt globale Helfer: call_openai_chat, logger, token_count (optional), retry_on_failure, re. + # Sie wird von process_verification_batch (dieselben Block) aufgerufen. + # Nutzt globale Helfer: call_openai_chat (Block 8), logger, token_count (optional Block 3), retry_on_failure (Block 2), re. @retry_on_failure # Wende den Decorator auf den gesamten Batch-API Call an def _process_verification_openai_batch(self, batch_data): """ @@ -4954,9 +4756,11 @@ class DataProcessor: if not batch_data: return {} # Gebe leeres Dictionary zurueck, wenn keine Tasks da sind - self.logger.debug(f"Sende OpenAI-Batch fuer Wiki-Verifizierung ({len(batch_data)} Zeilen, Start: {batch_data[0]['row_num']})...") + self.logger.debug(f"Sende OpenAI-Batch fuer Wiki-Verifizierung ({len(batch_data)} Zeilen, Start: {batch_data[0]['row_num'] if batch_data else 'N/A'})...") # <<< GEÄNDERT # --- Prompt Erstellung --- + # Verwenden Sie klare Anweisungen und das definierte Antwortformat. + # Vermeiden Sie Umlaute im Prompt, um Encoding-Probleme zu minimieren. aggregated_prompt = ( "Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln fuer Unternehmen. " "Fuer jeden der folgenden Eintraege pruefe, ob der vorhandene Wikipedia-Artikel (URL, Absatz, Kategorien) plausibel zum Firmennamen und zur Beschreibung passt. " @@ -4973,49 +4777,60 @@ class DataProcessor: "--------------------\n" ) - # Fuegen Sie die Daten fuer jeden Eintrag im Batch hinzu + # Fuegen Sie die Daten fuer jeden Eintrag im Batch hinzu. + # Kuerzen Sie die Beschreibungen und Kategorien, um das Prompt-Limit zu reduzieren. + # Stellen Sie sicher, dass die Werte Strings sind und "k.A." richtig behandelt wird. + max_desc_length = 200 # Maximale Laenge fuer Beschreibungsteile im Prompt for item in batch_data: row_num = item['row_num'] - # Kuerze die Beschreibungen und Kategorien, um das Prompt-Limit zu reduzieren. - # Stelle sicher, dass die Werte Strings sind. - crm_desc_short = str(item.get('crm_desc', 'k.A.' or 'k.A.'))[:200] + '...' if len(str(item.get('crm_desc', ''))) > 200 else str(item.get('crm_desc', 'k.A.')) - wiki_paragraph_short = str(item.get('wiki_paragraph', 'k.A.' or 'k.A.'))[:200] + '...' if len(str(item.get('wiki_paragraph', ''))) > 200 else str(item.get('wiki_paragraph', 'k.A.')) - wiki_categories_short = str(item.get('wiki_categories', 'k.A.' or 'k.A.'))[:200] + '...' if len(str(item.get('wiki_categories', ''))) > 200 else str(item.get('wiki_categories', 'k.A.')) + # Holen und Kuerzen Sie die Werte sicher. Ersetzen Sie None durch "k.A.". + company_name = str(item.get('company_name', 'k.A.')) + crm_desc = str(item.get('crm_desc', 'k.A.')) + wiki_url = str(item.get('wiki_url', 'k.A.')) + wiki_paragraph = str(item.get('wiki_paragraph', 'k.A.')) + wiki_categories = str(item.get('wiki_categories', 'k.A.')) + + # Kuerzen Sie die Laengen und fuegen Sie "..." hinzu, wenn gekuerzt wurde. + crm_desc_short = crm_desc[:max_desc_length] + '...' if len(crm_desc) > max_desc_length else crm_desc + wiki_paragraph_short = wiki_paragraph[:max_desc_length] + '...' if len(wiki_paragraph) > max_desc_length else wiki_paragraph + wiki_categories_short = wiki_categories[:max_desc_length] + '...' if len(wiki_categories) > max_desc_length else wiki_categories entry_text = ( f"Eintrag {row_num}:\n" - f" Firmenname: {str(item.get('company_name', 'k.A.'))}\n" + f" Firmenname: {company_name}\n" f" CRM-Beschreibung: {crm_desc_short}\n" - f" Wikipedia-URL: {str(item.get('wiki_url', 'k.A.' or 'k.A.'))}\n" + f" Wikipedia-URL: {wiki_url}\n" f" Wiki-Absatz: {wiki_paragraph_short}\n" f" Wiki-Kategorien: {wiki_categories_short}\n" f"----\n" ) aggregated_prompt += entry_text + + # Fuegen Sie den Abschluss des Prompts hinzu. aggregated_prompt += "--------------------\nBitte nur die 'Eintrag X: Antwort'-Zeilen ausgeben." - # Optional: Token zaehlen fuer den Prompt - # try: prompt_tokens = token_count(aggregated_prompt, model=getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')); logger.debug(f"Geschaetzt Prompt-Tokens fuer Batch: {prompt_tokens}."); - # except Exception as e_tc: logger.debug(f"Fehler beim Token-Zaehlen: {e_tc}"); + # Optional: Token zaehlen fuer den Prompt. + # try: prompt_tokens = token_count(aggregated_prompt, model=getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')); self.logger.debug(f"Geschaetzt Prompt-Tokens fuer Batch: {prompt_tokens}."); + # except Exception as e_tc: self.logger.debug(f"Fehler beim Token-Zaehlen: {e_tc}"); # --- ChatGPT Aufruf --- # call_openai_chat (Block 8) nutzt den retry_on_failure Decorator und wirft bei endgueltigem Fehler eine Exception. - # Der retry_on_failure Decorator auf dieser summarize_batch_openai Funktion (Block 9) faengt die Exception + # Der retry_on_failure Decorator auf dieser Funktion faengt die Exception # von call_openai_chat und fuehrt die Retries fuer die GESAMTE Batch-Funktion durch. chat_response = None try: # Rufe die zentrale OpenAI Chat API Funktion auf (Block 8). - # Standard Temperatur 0.0 fuer Klassifizierung. + # Standard Temperatur 0.0 fuer Klassifizierung/Verifizierung. chat_response = call_openai_chat(aggregated_prompt, temperature=0.0) # Wenn call_openai_chat erfolgreich ist, gibt es den String zurueck. # Exceptions werden nach Retries von call_openai_chat geworfen und vom aeusseren retry_on_failure dieser Funktion gefangen. if not chat_response: # Dieser Fall sollte nach der Aenderung in call_openai_chat (wirft Exception) nicht mehr auftreten. - logger.error("call_openai_chat gab unerwarteterweise None zurueck fuer Wiki-Verifizierungs-Batch.") + self.logger.error("call_openai_chat gab unerwarteterweise None zurueck fuer Wiki-Verifizierungs-Batch.") # <<< GEÄNDERT # Werfen Sie eine spezifische Exception, damit der aeussere Decorator sie faengt. raise openai.error.APIError("Keine Antwort von OpenAI erhalten fuer Wiki-Verifizierungs-Batch.") @@ -5023,15 +4838,17 @@ class DataProcessor: except Exception as e: # Wenn call_openai_chat oder der aeussere retry_on_failure eine Exception wirft (nach Retries) # Die Exception wird hier gefangen, bevor sie an den Aufrufer (process_verification_batch) weitergeleitet wird. - logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Wiki-Verifizierung (innerhalb Batch Decorator): {e}") + self.logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Wiki-Verifizierung (innerhalb Batch Decorator): {e}") # <<< GEÄNDERT # Logge den Traceback - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT # Geben Sie ein Dictionary zurueck, das signalisiert, dass fuer alle Zeilen im Batch ein Fehler aufgetreten ist return {item['row_num']: f"FEHLER API: {str(e)[:100]}" for item in batch_data} # --- Antwort parsen --- answers = {} # Initialisieren Sie das Ergebnis-Dictionary + # Liste der Zeilennummern, die im ursprünglichen Batch angefragt wurden + original_batch_row_nums = {item['row_num'] for item in batch_data} lines = chat_response.strip().split('\n') parsed_count = 0 for line in lines: @@ -5040,23 +4857,23 @@ class DataProcessor: if match: row_num = int(match.group(1)) answer_text = match.group(2).strip() - # Pruefen Sie, ob die Zeilennummer im urspruenglichen Batch enthalten war - if any(item['row_num'] == row_num for item in batch_data): + # Pruefen Sie, ob die Zeilennummer im urspruenglichen Batch angefragt wurde + if row_num in original_batch_row_nums: answers[row_num] = answer_text parsed_count += 1 - # else: logger.debug(f"Warnung: Antwort fuer unerwartete Zeilennummer {row_num} im Batch erhalten: {answer_text[:100]}...") # Zu viel Laerm (gekuerzt loggen) + # else: self.logger.debug(f"Warnung: Antwort fuer unerwartete Zeilennummer {row_num} im Batch erhalten: {answer_text[:100]}...") # Zu viel Laerm (gekuerzt loggen) # Logge das Ergebnis des Parsens - self.logger.debug(f"OpenAI-Batch-Antwort geparst: {parsed_count} von {len(batch_data)} Zeilen erfolgreich zugeordnet.") + self.logger.debug(f"OpenAI-Batch-Antwort geparst: {parsed_count} von {len(original_batch_row_nums)} Zeilen erfolgreich zugeordnet.") # <<< GEÄNDERT # Fuegen Sie einen Fehlerwert fuer Zeilen hinzu, die nicht geparst werden konnten (z.B. falsches Antwortformat) - if parsed_count < len(batch_data): - logger.warning(f"Nicht alle Zeilen aus dem Batch ({len(batch_data)}) konnten in der OpenAI-Antwort ({len(lines)} Zeilen) geparst werden.") + if parsed_count < len(original_batch_row_nums): + self.logger.warning(f"Nicht alle Zeilen aus dem Batch ({len(original_batch_row_nums)}) konnten in der OpenAI-Antwort ({len(lines)} Zeilen) geparst werden.") # <<< GEÄNDERT # Logge den Anfang der unvollstaendigen Antwort auf Debug - logger.debug(f"Unerwartete Antwortteile (erste 500 Zeichen): {chat_response[:500]}") - for item in batch_data: - if item['row_num'] not in answers: - answers[item['row_num']] = "FEHLER: Antwort nicht geparst" + self.logger.debug(f"Unerwartete Antwortteile (erste 500 Zeichen): {chat_response[:500]}") # <<< GEÄNDERT + for row_num in original_batch_row_nums: + if row_num not in answers: + answers[row_num] = "FEHLER: Antwort nicht geparst" # Die 'answers' Dictionary enthaelt nun Ergebnisse fuer alle Zeilen, entweder geparst oder mit einem Fehlerstring. @@ -5067,9 +4884,9 @@ class DataProcessor: # Diese Methode koordiniert die Auswahl der Zeilen, die Batch-Verarbeitung durch OpenAI, # und das Schreiben der Ergebnisse (S, T, U, V-Y, AX, AP) ins Sheet. # Basierend auf process_verification_only und _process_batch aus Teil 8. - # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter, _process_verification_openai_batch. - # Nutzt globale Helfer: COLUMN_MAP, logger, Config, datetime, time. - # Nutzt die uebergeordnete sheet_handler Instanz. + # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter, _process_verification_openai_batch (derselben Block). + # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, Config (Block 1), datetime, time. + # Nutzt die uebergeordnete sheet_handler Instanz (Block 14). def process_verification_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None): """ Batch-Prozess nur fuer Wikipedia-Verifizierung (Spalten S-U, V-Y werden geleert). @@ -5085,35 +4902,36 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Batch-Laufs - self.logger.info(f"Starte Wikipedia-Verifizierungsmodus (Batch S-U, AX). Bereich: {start_sheet_row}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") + self.logger.info(f"Starte Wikipedia-Verifizierungsmodus (Batch S-U, AX). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT - # --- Daten laden --- + + # --- Daten laden und Startzeile ermitteln --- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AX...") - # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AT. + self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AX...") # <<< GEÄNDERT + # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AX (Block 1 Column Map). # Standardmaessig ab Zeile 7 - start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wiki Verif. Timestamp") # Block 1 Column Map + start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wiki Verif. Timestamp", min_sheet_row=7) # Wenn get_start_row_index -1 zurueckgibt (Fehler) if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") + self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT return # Beende die Methode - # Berechne die 1-basierte Sheet-Startzeile + # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AX Zelle): {start_sheet_row}") + self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AX Zelle): {start_sheet_row}") # <<< GEÄNDERT else: # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. - # Der load_data Aufruf ist mit retry_on_failure dekoriert. + # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_verification_batch.") + self.logger.error("FEHLER beim Laden der Daten fuer process_verification_batch.") # <<< GEÄNDERT return # Beende die Methode, wenn das Laden fehlschlaegt - # Holen Sie die gesamte Datenliste (inklusive Header) + # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. all_data = self.sheet_handler.get_all_data_with_headers() - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar. + # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). header_rows = self.sheet_handler._header_rows total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet @@ -5122,11 +4940,11 @@ class DataProcessor: end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den verarbeitungsbereich - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") + self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT - # Pruefe, ob der Bereich gueltig ist + # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") + self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT return # Beende die Methode, wenn der Bereich leer ist @@ -5136,8 +4954,9 @@ class DataProcessor: "Wiki Verif. Timestamp", "Wiki URL", "Chat Wiki Konsistenzpruefung", # Pruefkriterien / Timestamp (AX, M, S) "CRM Name", "CRM Beschreibung", "Wiki Absatz", "Wiki Kategorien", # Daten fuer Prompt (B, F, N, R) "Chat Begruendung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", # Ergebnisspalten (T, U) - "Begruendung bei Abweichung", "Wikipedia Timestamp", "Timestamp letzte Pruefung", # Spalten zum Leeren (V, AN, AO) - "Version", "Wiki Verif. Timestamp", "SerpAPI Wiki Search Timestamp" # Weitere Spalten zum Leeren (AP, AX - aber AX wird gesetzt!, AY) + "Begruendung bei Abweichung", "Chat Begruendung Abweichung Branche", # Spalten V-Y zum Leeren + "Wikipedia Timestamp", "Timestamp letzte Pruefung", # Spalten AN, AO zum Leeren + "Version", "SerpAPI Wiki Search Timestamp" # Spalten AP, AY zum Leeren ] # Erstellen Sie ein Dictionary mit Schluesseln und Indizes col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} @@ -5145,11 +4964,11 @@ class DataProcessor: # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_verification_batch: {missing}. Breche ab.") + self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_verification_batch: {missing}. Breche ab.") # <<< GEÄNDERT return # Beende die Methode bei kritischem Fehler - # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt interne Helfer) + # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt interne Helfer _get_col_letter Block 14) ts_ax_letter = self.sheet_handler._get_col_letter(col_indices["Wiki Verif. Timestamp"] + 1) # Timestamp zu setzen (AX) s_letter = self.sheet_handler._get_col_letter(col_indices["Chat Wiki Konsistenzpruefung"] + 1) # Status S t_letter = self.sheet_handler._get_col_letter(col_indices["Chat Begruendung Wiki Inkonsistenz"] + 1) # Begruendung T @@ -5168,8 +4987,9 @@ class DataProcessor: empty_vy_values = [''] * (y_idx - v_idx + 1) # Anzahl der Spalten = Y_Index - V_Index + 1 - # Timestamps AN, AO, AY und Version AP leeren. - # Diese werden von anderen Schritten gesetzt und sollen hier zurueckgesetzt werden. + # Timestamps AN, AO, AP, AY leeren. + # Diese werden von anderen Schritten gesetzt und sollen hier zurueckgesetzt werden, + # um sicherzustellen, dass die Zeile bei Bedarf von diesen anderen Schritten erneut bearbeitet wird. an_letter = self.sheet_handler._get_col_letter(col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS) ao_letter = self.sheet_handler._get_col_letter(col_indices["Timestamp letzte Pruefung"] + 1) # AO (Chat Evaluation TS) ap_letter = self.sheet_handler._get_col_letter(col_indices["Version"] + 1) # AP (Version) @@ -5207,7 +5027,7 @@ class DataProcessor: # Nutzt interne Helfer _get_cell_value_safe company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map if not company_name: - self.logger.debug(f"Zeile {i}: Uebersprungen (Kein Firmenname in Spalte B).") + self.logger.debug(f"Zeile {i}: Uebersprungen (Kein Firmenname in Spalte B).") # <<< GEÄNDERT skipped_count += 1 # Zaehlen als uebersprungen continue # Springe zur naechsten Zeile @@ -5222,13 +5042,14 @@ class DataProcessor: s_value_upper = self._get_cell_value_safe(row, "Chat Wiki Konsistenzpruefung").strip().upper() # Block 1 Column Map # Pruefen Sie, ob die Wiki URL (M) gueltig aussieht - is_wiki_url_valid_looking = m_value and isinstance(m_value, str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu + is_wiki_url_valid_looking = m_value and isinstance(m_value, str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu basierend auf Log # Definieren Sie die Endzustaende von Status S (Grossbuchstaben) s_end_states = ["OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID SUGGESTION)"] # Pruefen Sie, ob Status S in einem Endzustand ist - is_s_in_endstate = s_value_upper in s_end_states + is_s_in_endstate = s_value_upper in s_end_states # Bugfix: Korrekte Zuweisung + # Verarbeitung ist noetig, wenn AX leer UND M gefuellt/gueltig aussieht UND S NICHT im Endzustand ist. processing_needed_for_row = not ax_value and is_wiki_url_valid_looking and not is_s_in_endstate @@ -5237,13 +5058,13 @@ class DataProcessor: # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) if log_check: - self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Verif. Check): AX leer? {not ax_value}, M gueltig? {is_wiki_url_valid_looking}, S ('{s_value_upper}') Endzustand? {is_s_in_endstate}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen + self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Verif. Check): AX leer? {not ax_value}, M gueltig? {is_wiki_url_valid_looking}, S ('{s_value_upper}') Endzustand? {is_s_in_endstate}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist if not processing_needed_for_row: skipped_count += 1 # Zaehlen als uebersprungene Zeile - # Zaehlen Sie separat, wenn die Zeile wegen fehlender M-URL uebersprungen wurde + # Zaehlen Sie separat, wenn die Zeile speziell wegen fehlender M-URL uebersprungen wurde if not is_wiki_url_valid_looking: skipped_no_wiki_url += 1 continue # Springe zur naechsten Zeile @@ -5254,7 +5075,7 @@ class DataProcessor: # Pruefe das Limit fuer verarbeitete Zeilen if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_verification_batch erreicht. Breche weitere Zeilenpruefung ab.") + self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_verification_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT break # Brich die Schleife ab @@ -5285,7 +5106,7 @@ class DataProcessor: # Logge den Start der Batch-Verarbeitung batch_start_row = current_openai_batch_data[0]['row_num'] batch_end_row = current_openai_batch_data[-1]['row_num'] - self.logger.debug(f"\n--- Starte Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") + self.logger.debug(f"\n--- Starte Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT # Rufe die interne Methode auf, die den OpenAI Call fuer den Batch macht. @@ -5376,12 +5197,12 @@ class DataProcessor: # Sende die gesammelten Updates fuer DIESEN Batch sofort. if batch_sheet_updates: - self.logger.debug(f" Sende Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...") + self.logger.debug(f" Sende Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. # Wenn es fehlschlaegt, wird es intern geloggt. success = self.sheet_handler.batch_update_cells(batch_sheet_updates) if success: - self.logger.info(f" Sheet-Update fuer Wiki-Verifizierungs-Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.") + self.logger.info(f" Sheet-Update fuer Wiki-Verifizierungs-Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Setze Batch-Listen zurueck fuer die naechste Iteration @@ -5392,7 +5213,7 @@ class DataProcessor: # Dies ist wichtig, um Rate Limits zu vermeiden. # Nutze Config.RETRY_DELAY, ggf. kuerzer, da es ein Batch war pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.5 # 50% der Retry-Wartezeit - self.logger.debug(f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---") + self.logger.debug(f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---") # <<< GEÄNDERT time.sleep(pause_duration) @@ -5402,7 +5223,7 @@ class DataProcessor: # Logge den Start des finalen Batches batch_start_row = current_openai_batch_data[0]['row_num'] batch_end_row = current_openai_batch_data[-1]['row_num'] - self.logger.debug(f"\n--- Starte FINALEN Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") + self.logger.debug(f"\n--- Starte FINALEN Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT # Rufe die interne Methode auf, die den OpenAI Call macht batch_results = self._process_verification_openai_batch(current_openai_batch_data) @@ -5445,1315 +5266,85 @@ class DataProcessor: # Sende die gesammelten Updates fuer DIESEN finalen Batch. if batch_sheet_updates: - self.logger.debug(f" Sende FINALES Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen)...") + self.logger.debug(f" Sende FINALES Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. success = self.sheet_handler.batch_update_cells(batch_sheet_updates) if success: - self.logger.info(f" FINALES Sheet-Update fuer Wiki-Verifizierungs-Batch erfolgreich.") + self.logger.info(f" FINALES Sheet-Update fuer Wiki-Verifizierungs-Batch erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Logge den Abschluss des Modus - self.logger.info(f"Wikipedia-Verifizierungs-Batch abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen ({skipped_no_wiki_url} wegen fehlender M-URL).") + self.logger.info(f"Wikipedia-Verifizierungs-Batch abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen ({skipped_no_wiki_url} wegen fehlender M-URL).") # <<< GEÄNDERT # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. # ============================================================================== # Ende DataProcessor Klasse Batch: Wiki Verification Block -# ============================================================================== - - # ========================================================================== - # === Prozess Methoden (Re-Evaluation) ===================================== - # ========================================================================== - - # --- Methode fuer den Re-Eval Modus (Spalte A = 'x') --- - # Diese Methode verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind. - # Sie ruft _process_single_row fuer jede dieser Zeilen auf mit force_reeval=True - # und uebergibt die Auswahl der Schritte und das Flag zum Loeschen des 'x'-Flags. - # Nutzt interne Helfer: _process_single_row, _get_cell_value_safe. - # Nutzt globale Helfer: COLUMN_MAP, logger. - # Nutzt die uebergeordnete sheet_handler Instanz. - def process_reevaluation_rows(self, row_limit=None, clear_flag=True, - process_wiki_steps=True, - process_chatgpt_steps=True, - process_website_steps=True, - process_ml_steps=True # Neues Flag fuer ML-Schritt - # Fuegen Sie hier ggf. weitere boolsche Flags fuer andere Schrittgruppen hinzu - ): - """ - Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind. - Ruft _process_single_row fuer jede dieser Zeilen auf mit force_reeval=True. - Verarbeitet maximal row_limit Zeilen. - Loescht optional das 'x'-Flag nach erfolgreicher Verarbeitung (innerhalb von _process_single_row). - Erlaubt die Auswahl spezifischer Verarbeitungsschritte. - - Args: - row_limit (int, optional): Maximale Anzahl zu verarbeitender Zeilen. Defaults to None (Unbegrenzt). - clear_flag (bool, optional): Wenn True, wird das Flag 'x' in Spalte A - nach erfolgreicher Verarbeitung durch _process_single_row geloescht. - Defaults to True. - process_wiki_steps (bool, optional): Soll der Wiki-Schritt in _process_single_row ausgefuehrt werden?. Defaults to True. - process_chatgpt_steps (bool, optional): Sollen ChatGPT-Schritte in _process_single_row ausgefuehrt werden?. Defaults to True. - process_website_steps (bool, optional): Soll der Website-Schritt in _process_single_row ausgefuehrt werden?. Defaults to True. - process_ml_steps (bool, optional): Soll der ML-Schritt in _process_single_row ausgefuehrt werden?. Defaults to True. # Neues Flag - # Fuegen Sie hier ggf. weitere boolsche Flags fuer andere Schrittgruppen hinzu. - """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - # Logge die Konfiguration des Re-Eval Laufs - self.logger.info(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}") - # Logge die ausgewaehlten Schritte fuer diesen Lauf - selected_steps_log = [] - if process_wiki_steps: selected_steps_log.append("Wiki (wiki)") - if process_chatgpt_steps: selected_steps_log.append("ChatGPT (chat)") - if process_website_steps: selected_steps_log.append("Website (web)") - if process_ml_steps: selected_steps_log.append("ML Predict (ml_predict)") # Neues Flag - # Fuegen Sie hier weitere Schritte hinzu, wenn neue Flags existieren - self.logger.info(f" Ausgewaehlte Schritte fuer Re-Eval: {', '.join(selected_steps_log) if selected_steps_log else 'Keine ausgewählt!'}") - - - # Erstelle das Set der Schluessel fuer die Schritte, die an _process_single_row uebergeben werden - steps_to_run_set = set() - if process_wiki_steps: steps_to_run_set.add('wiki') - if process_chatgpt_steps: steps_to_run_set.add('chat') # Annahme: 'chat' triggert alle ChatGPT Schritte in _process_single_row (Block 20) - if process_website_steps: steps_to_run_set.add('web') - if process_ml_steps: steps_to_run_set.add('ml_predict') # Neues Flag - # Fuegen Sie hier weitere Schluessel hinzu, wenn neue Flags verwendet werden - - - # Wenn keine Schritte ausgewaehlt wurden - if not steps_to_run_set: - self.logger.warning("Keine Verarbeitungsschritte fuer Re-Eval ausgewaehlt (steps_to_run_set ist leer). Modus wird uebersprungen.") - return - - - # Daten neu laden vor der Verarbeitung (nutzt die uebergeordnete Instanz) - # Der load_data Aufruf ist mit retry_on_failure dekoriert. - if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer Re-Evaluation.") - return # Beende die Methode, wenn das Laden fehlschlaegt - - - # Holen Sie die gesamte Datenliste (inklusive Header) - all_data = self.sheet_handler.get_all_data_with_headers() - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar. - header_rows = self.sheet_handler._header_rows - # Wenn keine Daten da sind oder nur Header - if not all_data or len(all_data) <= header_rows: - self.logger.warning("Keine Datenzeilen fuer Re-Evaluation gefunden.") - return # Beende die Methode - - - # Ermitteln Sie den Index der ReEval Flag Spalte aus COLUMN_MAP (Block 1) - reeval_col_idx = COLUMN_MAP.get("ReEval Flag") - if reeval_col_idx is None: - self.logger.critical("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden. Kann Zeilen mit 'x' nicht finden. Breche ab.") - return # Beende die Methode bei kritischem Fehler - - - # Sammeln Sie die Zeilen, die in Spalte A mit 'x' markiert sind. - rows_to_process = [] # Liste von Dictionaries {'row_num': ..., 'data': ...} - # Iteriere ueber die Datenzeilen (ab der ersten Datenzeile) - for idx_in_list in range(header_rows, len(all_data)): - row_data = all_data[idx_in_list] # Die Rohdaten fuer diese Zeile (0-basierter Index in all_data) - row_num_in_sheet = idx_in_list + 1 # 1-basierte Zeilennummer im Sheet - - # Pruefen Sie sicher auf den Wert 'x' in Spalte A (nutzt interne Helfer) - cell_a_value = self._get_cell_value_safe(row_data, "ReEval Flag").strip().lower() - - # Wenn die Zelle in Spalte A "x" ist - if cell_a_value == "x": - # Fuegen Sie die Zeilendaten zur Liste der zu verarbeitenden Zeilen hinzu - rows_to_process.append({'row_num': row_num_in_sheet, 'data': row_data}) - - - found_count = len(rows_to_process) # Anzahl der gefundenen markierten Zeilen - self.logger.info(f"{found_count} Zeilen mit ReEval-Flag 'x' gefunden.") - # Wenn keine Zeilen zum Verarbeiten markiert sind - if found_count == 0: - self.logger.info("Keine Zeilen zur Re-Evaluation markiert.") - return # Beende die Methode - - - # Verarbeitung der markierten Zeilen - processed_count = 0 # Zaehlt Zeilen, fuer die _process_single_row aufgerufen wurde (im Rahmen des Limits). - # Die Liste updates_clear_flag wird NICHT mehr hier gefuellt, da _process_single_row das Update selbst hinzufuegt (Block 21). - # Die Liste rows_actually_processed wird nicht mehr benoetigt. - - # Iteriere ueber die gefundenen markierten Zeilen - for task in rows_to_process: - # Ueberpruefen Sie das Limit fuer die zu verarbeitenden Zeilen VOR der Verarbeitung - if row_limit is not None and isinstance(row_limit, int) and row_limit > 0 and processed_count >= row_limit: - # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Zeilenlimit ({row_limit}) fuer Re-Evaluation erreicht. Breche weitere Verarbeitung ab.") - break # Brich die Schleife ab - - - row_num = task['row_num'] # 1-basierte Zeilennummer - row_data = task['data'] # Die Rohdaten fuer diese Zeile - - self.logger.info(f"Bearbeite Re-Eval Zeile {row_num}...") - try: - # Rufe die Methode zur Verarbeitung einer einzelnen Zeile auf (_process_single_row Block 19). - # In diesem Modus setzen wir immer force_reeval=True. - # Wir uebergeben die aus CLI/Menue ausgewaehlten Schritte in steps_to_run_set. - # Wir uebergeben das clear_flag, damit _process_single_row weiss, ob das 'x' geloescht werden soll. - # _process_single_row fuehrt die Schritte durch, sammelt Updates (inkl. 'x'-Flag Update wenn clear_x_flag=True) - # und sendet das Batch-Update fuer diese Zeile. - self._process_single_row( - row_num_in_sheet = row_num, - row_data = row_data, - steps_to_run = steps_to_run_set, # <-- Uebergibt die aus CLI/Menue ausgewaehlten Schritte - force_reeval = True, # <-- Erzwingt Re-Evaluation unabhaengig von Timestamps fuer die ausgewaehlten Schritte - clear_x_flag = clear_flag # <-- Uebergibt, ob das 'x'-Flag von _process_single_row geloescht werden soll - ) - - # Zaehlen, wenn _process_single_row erfolgreich aufgerufen wurde (unabhaengig von internen Ueberspringungen in _process_single_row). - processed_count += 1 - # Die Liste rows_actually_processed wird nicht mehr benoetigt. - - except Exception as e_proc: - # Wenn _process_single_row einen Fehler wirft (nachdem interne Retries aufgaben), - # fangen wir ihn hier, loggen ihn und fahren mit der naechsten Zeile fort. - # Das 'x'-Flag wird in diesem Fall NICHT geloescht, da _process_single_row nicht bis zum Ende kam. - self.logger.exception(f"FEHLER bei Re-Evaluation von Zeile {row_num}: {e_proc}") - # Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte im Sheet schreiben lassen. - # Dieses Update muesste dann separat oder im naechsten Lauf behandelt werden. - - # _process_single_row beinhaltet bereits eine kleine Pause am Ende. - # Hier ist keine zusaetzliche Pause noetig nach der Zeilenverarbeitung. - # Wenn _process_single_row eine Exception wirft, kann hier eine kurze Pause sinnvoll sein. - # time.sleep(0.1) # Optional: Kurze Pause bei Fehler nach Exception - - - # Der Codeblock zum Loeschen der gesammelten Updates (updates_clear_flag) am Ende wurde entfernt. - - # Logge den Abschluss des Re-Eval Modus - self.logger.info(f"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Gefunden: {found_count}, Limit: {row_limit}).") - - -# ============================================================================== -# Ende DataProcessor Klasse Prozess: Re-Evaluation Block # ============================================================================== # ========================================================================== # === Batch Processing Methods ============================================= # ========================================================================== - # --- Interne Hilfsfunktion fuer Wiki-Verifizierungs-Batch (OpenAI Call) --- - # Diese Funktion verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen ueber OpenAI. - # Sie wird von process_verification_batch (dieselben Block) aufgerufen. - # Nutzt globale Helfer: call_openai_chat (Block 8), logger, token_count (optional Block 3), retry_on_failure (Block 2), re. - @retry_on_failure # Wende den Decorator auf den gesamten Batch-API Call an - def _process_verification_openai_batch(self, batch_data): + # --- Worker Funktion für paralleles Website Scraping (intern) --- + # Wird von process_website_scraping_batch aufgerufen + def _scrape_raw_text_task(self, task_info, get_website_raw_func): """ - Verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen ueber OpenAI. - Sammelt die Ergebnisse und gibt sie zurueck. Aktualisiert NICHT das Sheet direkt. + Scrapt den Rohtext einer Website in einem separaten Thread. + Wird vom ThreadPoolExecutor in process_website_scraping_batch aufgerufen. + Nutzt die uebergebene Funktion zum Abrufen des Rohtexts. Args: - batch_data (list): Liste von Dictionaries, jedes enthaelt: - {'row_num': int, 'company_name': str, 'crm_desc': str, - 'wiki_url': str, 'wiki_paragraph': str, 'wiki_categories': str} + task_info (dict): Enthält {'row_num': int, 'url': str}. + get_website_raw_func (function): Die Funktion zum Abrufen des Website-Rohtexts (sollte die globale get_website_raw sein). Returns: - dict: Ein Dictionary, das Zeilennummern auf die rohe ChatGPT-Antwort mappt. - z.B. {2122: "OK", 2123: "X | ..."} - Bei Fehlern oder fehlenden Antworten wird ein Fehlerstring verwendet. - Wirft Exception bei endgueltigen API-Fehlern nach Retries. + dict: Enthält {'row_num': int, 'raw_text': str, 'error': str}. """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - if not batch_data: - return {} # Gebe leeres Dictionary zurueck, wenn keine Tasks da sind + # Logger für diese Funktion holen (da sie in einem Thread läuft) + logger = logging.getLogger(__name__ + ".scrape_worker") - self.logger.debug(f"Sende OpenAI-Batch fuer Wiki-Verifizierung ({len(batch_data)} Zeilen, Start: {batch_data[0]['row_num'] if batch_data else 'N/A'})...") # Sichere Indexierung + row_num = task_info['row_num'] + url = task_info['url'] + raw_text = "k.A." + error = None - # --- Prompt Erstellung --- - # Verwenden Sie klare Anweisungen und das definierte Antwortformat. - # Vermeiden Sie Umlaute im Prompt, um Encoding-Probleme zu minimieren. - aggregated_prompt = ( - "Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln fuer Unternehmen. " - "Fuer jeden der folgenden Eintraege pruefe, ob der vorhandene Wikipedia-Artikel (URL, Absatz, Kategorien) plausibel zum Firmennamen und zur Beschreibung passt. " - "Gib das Ergebnis fuer jeden Eintrag ausschliesslich im folgenden Format auf einer neuen Zeile aus:\n" - "Eintrag : \n\n" - "Moegliche Antworten:\n" - "- 'OK' (wenn der Artikel gut passt)\n" - "- 'X | Alternativer Artikel: | Begruendung: ' (wenn der Artikel nicht passt, aber ein besserer gefunden wurde)\n" - "- 'X | Kein passender Artikel gefunden | Begruendung: ' (wenn der Artikel nicht passt und kein besserer gefunden wurde)\n" - # Der Fall "Kein Wikipedia-Eintrag vorhanden" wird vom Skript VOR diesem Call behandelt - # und sollte hier nicht vom KI-Modell generiert werden. - "Stelle sicher, dass du nur EINE Zeile pro Eintrag im Format 'Eintrag X: Antwort' ausgibst.\n\n" - "Eintraege zur Pruefung:\n" - "--------------------\n" - ) - - # Fuegen Sie die Daten fuer jeden Eintrag im Batch hinzu. - # Kuerzen Sie die Beschreibungen und Kategorien, um das Prompt-Limit zu reduzieren. - # Stellen Sie sicher, dass die Werte Strings sind und "k.A." richtig behandelt wird. - max_desc_length = 200 # Maximale Laenge fuer Beschreibungsteile im Prompt - for item in batch_data: - row_num = item['row_num'] - # Holen und Kuerzen Sie die Werte sicher. Ersetzen Sie None durch "k.A.". - company_name = str(item.get('company_name', 'k.A.')) - crm_desc = str(item.get('crm_desc', 'k.A.')) - wiki_url = str(item.get('wiki_url', 'k.A.')) - wiki_paragraph = str(item.get('wiki_paragraph', 'k.A.')) - wiki_categories = str(item.get('wiki_categories', 'k.A.')) - - # Kuerzen Sie die Laengen und fuegen Sie "..." hinzu, wenn gekuerzt wurde. - crm_desc_short = crm_desc[:max_desc_length] + '...' if len(crm_desc) > max_desc_length else crm_desc - wiki_paragraph_short = wiki_paragraph[:max_desc_length] + '...' if len(wiki_paragraph) > max_desc_length else wiki_paragraph - wiki_categories_short = wiki_categories[:max_desc_length] + '...' if len(wiki_categories) > max_desc_length else wiki_categories - - - entry_text = ( - f"Eintrag {row_num}:\n" - f" Firmenname: {company_name}\n" - f" CRM-Beschreibung: {crm_desc_short}\n" - f" Wikipedia-URL: {wiki_url}\n" - f" Wiki-Absatz: {wiki_paragraph_short}\n" - f" Wiki-Kategorien: {wiki_categories_short}\n" - f"----\n" - ) - aggregated_prompt += entry_text - - - # Fuegen Sie den Abschluss des Prompts hinzu. - aggregated_prompt += "--------------------\nBitte nur die 'Eintrag X: Antwort'-Zeilen ausgeben." - - # Optional: Token zaehlen fuer den Prompt. - # try: prompt_tokens = token_count(aggregated_prompt, model=getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')); logger.debug(f"Geschaetzt Prompt-Tokens fuer Batch: {prompt_tokens}."); - # except Exception as e_tc: logger.debug(f"Fehler beim Token-Zaehlen: {e_tc}"); - - - # --- ChatGPT Aufruf --- - # call_openai_chat (Block 8) nutzt den retry_on_failure Decorator und wirft bei endgueltigem Fehler eine Exception. - # Der retry_on_failure Decorator auf dieser summarize_batch_openai Funktion (Block 9) faengt die Exception - # von call_openai_chat und fuehrt die Retries fuer die GESAMTE Batch-Funktion durch. - chat_response = None try: - # Rufe die zentrale OpenAI Chat API Funktion auf (Block 8). - # Standard Temperatur 0.0 fuer Klassifizierung/Verifizierung. - chat_response = call_openai_chat(aggregated_prompt, temperature=0.0) - # Wenn call_openai_chat erfolgreich ist, gibt es den String zurueck. - # Exceptions werden nach Retries von call_openai_chat geworfen und vom aeusseren retry_on_failure dieser Funktion gefangen. + # RUFT die uebergebene Funktion zum Abrufen des Rohtexts auf. + # Der retry_on_failure Decorator auf get_website_raw_func (der hoffentlich get_website_raw ist) + # behandelt Retries und die meisten Fehler. + raw_text = get_website_raw_func(url) # <<< Ruft die uebergebene Funktion auf - if not chat_response: - # Dieser Fall sollte nach der Aenderung in call_openai_chat (wirft Exception) nicht mehr auftreten. - logger.error("call_openai_chat gab unerwarteterweise None zurueck fuer Wiki-Verifizierungs-Batch.") - # Werfen Sie eine spezifische Exception, damit der aeussere Decorator sie faengt. - raise openai.error.APIError("Keine Antwort von OpenAI erhalten fuer Wiki-Verifizierungs-Batch.") + # Wenn die Funktion einen Fehler loggt und einen Fehlerstring im Ergebnis zurueckgibt, + # wird dies hier als Fehler im Task markiert. + if isinstance(raw_text, str) and (raw_text.startswith("k.A. (Fehler") or raw_text.startswith("FEHLER:")): + error = f"Scraping Fehler (Details im Rohtext): {raw_text[:100]}..." + # Der Fehler wurde bereits in get_website_raw geloggt, kein weiteres Logging hier noetig. + # Das raw_text selbst enthaelt den Fehlerstring. + elif not isinstance(raw_text, str) or not raw_text.strip(): + # Wenn die Funktion keinen String oder einen leeren String zurueckgibt + error = "Scraping Task Fehler: Funktion gab keinen gueltigen String zurueck." + raw_text = "k.A. (Extraktion fehlgeschlagen)" # Standard-Fehlerwert except Exception as e: - # Wenn call_openai_chat oder der aeussere retry_on_failure eine Exception wirft (nach Retries) - # Die Exception wird hier gefangen, bevor sie an den Aufrufer (process_verification_batch) weitergeleitet wird. - logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Wiki-Verifizierung (innerhalb Batch Decorator): {e}") - # Logge den Traceback - self.logger.debug(traceback.format_exc()) - # Geben Sie ein Dictionary zurueck, das signalisiert, dass fuer alle Zeilen im Batch ein Fehler aufgetreten ist - return {item['row_num']: f"FEHLER API: {str(e)[:100]}" for item in batch_data} + # Dieser Block sollte jetzt sehr selten erreicht werden, da die uebergegebene Funktion + # mit retry_on_failure die meisten Fehler abfangen sollte. + # Wenn eine Exception hier durchkommt, ist es ein sehr unerwarteter Fehler im Task-Handling selbst. + error = f"Unerwarteter Fehler im Scraping Task Zeile {row_num} ({url[:100]}): {type(e).__name__} - {e}" # Gekuerzt loggen + logger.error(error) # Loggen Sie diesen unerwarteten Fehler + raw_text = "k.A. (Unerwarteter Fehler Task)" # Setze einen spezifischen Fehlerwert - # --- Antwort parsen --- - answers = {} # Initialisieren Sie das Ergebnis-Dictionary - # Liste der Zeilennummern, die im ursprünglichen Batch angefragt wurden - original_batch_row_nums = {item['row_num'] for item in batch_data} - lines = chat_response.strip().split('\n') - parsed_count = 0 - for line in lines: - # Matcht "Eintrag :" und den Rest der Zeile - match = re.match(r"Eintrag (\d+): (.*)", line.strip()) - if match: - row_num = int(match.group(1)) - answer_text = match.group(2).strip() - # Pruefen Sie, ob die Zeilennummer im urspruenglichen Batch angefragt wurde - if row_num in original_batch_row_nums: - answers[row_num] = answer_text - parsed_count += 1 - # else: logger.debug(f"Warnung: Antwort fuer unerwartete Zeilennummer {row_num} im Batch erhalten: {answer_text[:100]}...") # Zu viel Laerm (gekuerzt loggen) - - # Logge das Ergebnis des Parsens - self.logger.debug(f"OpenAI-Batch-Antwort geparst: {parsed_count} von {len(original_batch_row_nums)} Zeilen erfolgreich zugeordnet.") - - # Fuegen Sie einen Fehlerwert fuer Zeilen hinzu, die nicht geparst werden konnten (z.B. falsches Antwortformat) - if parsed_count < len(original_batch_row_nums): - logger.warning(f"Nicht alle Zeilen aus dem Batch ({len(original_batch_row_nums)}) konnten in der OpenAI-Antwort ({len(lines)} Zeilen) geparst werden.") - # Logge den Anfang der unvollstaendigen Antwort auf Debug - logger.debug(f"Unerwartete Antwortteile (erste 500 Zeichen): {chat_response[:500]}") - for row_num in original_batch_row_nums: - if row_num not in answers: - answers[row_num] = "FEHLER: Antwort nicht geparst" - - - # Die 'answers' Dictionary enthaelt nun Ergebnisse fuer alle Zeilen, entweder geparst oder mit einem Fehlerstring. - return answers # Rueckgabe des Dictionarys mit Ergebnissen oder Fehlern - - - # --- Methode fuer den Wiki-Verifizierungs-Batchmodus (AX) --- - # Diese Methode koordiniert die Auswahl der Zeilen, die Batch-Verarbeitung durch OpenAI, - # und das Schreiben der Ergebnisse (S, T, U, V-Y, AX, AP) ins Sheet. - # Basierend auf process_verification_only und _process_batch aus Teil 8. - # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter, _process_verification_openai_batch (derselbe Block). - # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, Config (Block 1), datetime, time. - # Nutzt die uebergeordnete sheet_handler Instanz (Block 14). - def process_verification_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None): - """ - Batch-Prozess nur fuer Wikipedia-Verifizierung (Spalten S-U, V-Y werden geleert). - Laedt Daten neu, prueft fuer jede Zeile im Bereich, ob Timestamp AX (Wiki Verif.) - bereits gesetzt ist, ob eine Wiki URL (M) vorhanden ist und ob Status S - nicht bereits 'OK', 'X (URL Copied)' oder 'X (Invalid Suggestion)' ist. - Setzt AX + AP fuer bearbeitete Zeilen und schreibt S-U in Batches. - - Args: - start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (automatische Ermittlung basierend auf leeren AX). - end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet). - limit (int, optional): Maximale Anzahl ZU VERARBEITENDER (nicht uebersprungener) Zeilen. Defaults to None (Unbegrenzt). - """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - # Logge die Konfiguration des Batch-Laufs - self.logger.info(f"Starte Wikipedia-Verifizierungsmodus (Batch S-U, AX). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") - - - # --- Daten laden und Startzeile ermitteln --- - # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt - if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AX...") - # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AX (Block 1 Column Map). - # Standardmaessig ab Zeile 7 - start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wiki Verif. Timestamp", min_sheet_row=7) - - # Wenn get_start_row_index -1 zurueckgibt (Fehler) - if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") - return # Beende die Methode - - # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index - start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AX Zelle): {start_sheet_row}") - else: - # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. - # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). - if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_verification_batch.") - return # Beende die Methode, wenn das Laden fehlschlaegt - - - # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. - all_data = self.sheet_handler.get_all_data_with_headers() - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). - header_rows = self.sheet_handler._header_rows - total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet - - # Berechne Endzeile, wenn nicht manuell gesetzt - if end_sheet_row is None: - end_sheet_row = total_sheet_rows # Bis zur letzten Zeile - - # Logge den verarbeitungsbereich - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") - - # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) - if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") - return # Beende die Methode, wenn der Bereich leer ist - - - # --- Indizes und Buchstaben --- - # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind - required_keys = [ - "Wiki Verif. Timestamp", "Wiki URL", "Chat Wiki Konsistenzpruefung", # Pruefkriterien / Timestamp (AX, M, S) - "CRM Name", "CRM Beschreibung", "Wiki Absatz", "Wiki Kategorien", # Daten fuer Prompt (B, F, N, R) - "Chat Begruendung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", # Ergebnisspalten (T, U) - "Begruendung bei Abweichung", "Chat Begruendung Abweichung Branche", # Spalten V-Y zum Leeren - "Wikipedia Timestamp", "Timestamp letzte Pruefung", # Spalten AN, AO zum Leeren - "Version", "SerpAPI Wiki Search Timestamp" # Spalten AP, AY zum Leeren - ] - # Erstellen Sie ein Dictionary mit Schluesseln und Indizes aus COLUMN_MAP. - col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} - - # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden - if None in col_indices.values(): - missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_verification_batch: {missing}. Breche ab.") - return # Beende die Methode bei kritischem Fehler - - - # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt interne Helfer _get_col_letter Block 14) - ts_ax_letter = self.sheet_handler._get_col_letter(col_indices["Wiki Verif. Timestamp"] + 1) # Timestamp zu setzen (AX) - s_letter = self.sheet_handler._get_col_letter(col_indices["Chat Wiki Konsistenzpruefung"] + 1) # Status S - t_letter = self.sheet_handler._get_col_letter(col_indices["Chat Begruendung Wiki Inkonsistenz"] + 1) # Begruendung T - u_letter = self.sheet_handler._get_col_letter(col_indices["Chat Vorschlag Wiki Artikel"] + 1) # Vorschlag U - - # Spalten V-Y leeren (werden in diesem Modus nicht neu befuellt). - # V ist Begruendung bei Abweichung (von Wiki-URL Pruefung CRM vs Wiki). - # Y ist Begruendung Abweichung Branche (von Chat). - v_idx = col_indices["Begruendung bei Abweichung"] - y_idx = col_indices["Chat Begruendung Abweichung Branche"] - # Erstellen Sie den Bereichsnamen (z.B. "V:Y") - v_letter = self.sheet_handler._get_col_letter(v_idx + 1) - y_letter = self.sheet_handler._get_col_letter(y_idx + 1) - v_y_range_letter = f'{v_letter}:{y_letter}' # z.B. V:Y - # Erstellen Sie eine Liste von leeren Strings fuer diesen Bereich - empty_vy_values = [''] * (y_idx - v_idx + 1) # Anzahl der Spalten = Y_Index - V_Index + 1 - - - # Timestamps AN, AO, AP, AY leeren. - # Diese werden von anderen Schritten gesetzt und sollen hier zurueckgesetzt werden, - # um sicherzustellen, dass die Zeile bei Bedarf von diesen anderen Schritten erneut bearbeitet wird. - an_letter = self.sheet_handler._get_col_letter(col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS) - ao_letter = self.sheet_handler._get_col_letter(col_indices["Timestamp letzte Pruefung"] + 1) # AO (Chat Evaluation TS) - ap_letter = self.sheet_handler._get_col_letter(col_indices["Version"] + 1) # AP (Version) - ay_letter = self.sheet_handler._get_col_letter(col_indices["SerpAPI Wiki Search Timestamp"] + 1) # AY (SerpAPI Wiki TS) - - - # --- Verarbeitung --- - # Holen Sie die Batch-Groesse fuer OpenAI-Aufrufe aus Config (Block 1) - openai_batch_size = getattr(Config, 'PROCESSING_BATCH_SIZE', 20) # Nutzt dieselbe Batch-Groesse wie Scraping/Summarization - # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1) - update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) - - - current_openai_batch_data = [] # Daten fuer den aktuellen OpenAI Batch (Liste von Dicts) - rows_in_current_openai_batch = [] # 1-basierte Zeilennummern im aktuellen OpenAI Batch - all_sheet_updates = [] # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) - - - processed_count = 0 # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den Batch aufgenommen werden (im Rahmen des Limits). - skipped_count = 0 # Zaehlt Zeilen, die uebersprungen wurden (wegen Status, fehlender Daten etc.). - skipped_no_wiki_url = 0 # Zaehlt Zeilen, die speziell wegen fehlender M-URL uebersprungen wurden. - - - # Iteriere ueber die Sheet-Zeilen im definierten Bereich (1-basierte Sheet-Zeilennummer) - for i in range(start_sheet_row, end_sheet_row + 1): - row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste - # Pruefen Sie, ob das Ende des Sheets erreicht wurde - if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht - - - row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile - - - # Stellen Sie sicher, dass die Zeile nicht leer ist (mindestens Name vorhanden) - # Nutzt interne Helfer _get_cell_value_safe - company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - if not company_name: - self.logger.debug(f"Zeile {i}: Uebersprungen (Kein Firmenname in Spalte B).") - skipped_count += 1 # Zaehlen als uebersprungen - continue # Springe zur naechsten Zeile - - # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist --- - # Kriterium: Wiki Verif. Timestamp (AX) ist leer - # UND Wiki URL (M) ist gefuellt und gueltig aussehend (nicht k.A., Fehler etc.) - # UND Status S ist NICHT bereits in einem Endzustand (OK, X (UPDATED/COPIED/INVALID)). - - # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne Helfer) - ax_value = self._get_cell_value_safe(row, "Wiki Verif. Timestamp").strip() # Block 1 Column Map - m_value = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map - s_value_upper = self._get_cell_value_safe(row, "Chat Wiki Konsistenzpruefung").strip().upper() # Block 1 Column Map - - # Pruefen Sie, ob die Wiki URL (M) gueltig aussieht - is_wiki_url_valid_looking = m_value and isinstance(m_value, str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu basierend auf Log - - - # Definieren Sie die Endzustaende von Status S (Grossbuchstaben) - s_end_states = ["OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID SUGGESTION)"] - # Pruefen Sie, ob Status S in einem Endzustand ist - is_s_in_endstate = is_s_in_endstate = s_value_upper in s_end_states # Bugfix: variable is_s_in_endstate wurde falsch zugewiesen. - - # Verarbeitung ist noetig, wenn AX leer UND M gefuellt/gueltig aussieht UND S NICHT im Endzustand ist. - processing_needed_for_row = not ax_value and is_wiki_url_valid_looking and not is_s_in_endstate - - - # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level - log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) - if log_check: - self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Verif. Check): AX leer? {not ax_value}, M gueltig? {is_wiki_url_valid_looking}, S ('{s_value_upper}') Endzustand? {is_s_in_endstate}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen - - - # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist - if not processing_needed_for_row: - skipped_count += 1 # Zaehlen als uebersprungene Zeile - # Zaehlen Sie separat, wenn die Zeile speziell wegen fehlender M-URL uebersprungen wurde - if not is_wiki_url_valid_looking: skipped_no_wiki_url += 1 - continue # Springe zur naechsten Zeile - - - # --- Wenn Verarbeitung noetig: Fuege zur Batch-Liste fuer OpenAI hinzu --- - processed_count += 1 # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im Rahmen des Limits zaehlen) - - # Pruefe das Limit fuer verarbeitete Zeilen - if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: - # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_verification_batch erreicht. Breche weitere Zeilenpruefung ab.") - break # Brich die Schleife ab - - - # Sammle die benoetigten Daten fuer den OpenAI Prompt (_process_verification_openai_batch Block 26). - # Diese Daten werden in einem Dictionary fuer den Batch gesammelt. - crm_desc = self._get_cell_value_safe(row, "CRM Beschreibung") # Block 1 Column Map - wiki_paragraph = self._get_cell_value_safe(row, "Wiki Absatz") # Block 1 Column Map - wiki_categories = self._get_cell_value_safe(row, "Wiki Kategorien") # Block 1 Column Map - - - # Fuege die Daten dieser Zeile zur aktuellen Batch-Liste fuer OpenAI hinzu - current_openai_batch_data.append({ - 'row_num': i, # Die 1-basierte Sheet-Zeilennummer - 'company_name': company_name, # Nutzt den initial geladenen Namen - 'crm_desc': crm_desc, - 'wiki_url': m_value, # Nutzt die M-URL aus dem Sheet - 'wiki_paragraph': wiki_paragraph, - 'wiki_categories': wiki_categories - }) - # Fuege die Zeilennummer zur Liste der Zeilennummern im Batch hinzu - rows_in_current_openai_batch.append(i) - - - # --- Verarbeite den Batch, wenn voll --- - # Pruefe, ob die aktuelle Batch-Liste die definierte Groesse erreicht hat. - # openai_batch_size wird aus Config geholt (Block 1). - if len(current_openai_batch_data) >= openai_batch_size: - # Logge den Start der Batch-Verarbeitung - batch_start_row = current_openai_batch_data[0]['row_num'] - batch_end_row = current_openai_batch_data[-1]['row_num'] - self.logger.debug(f"\n--- Starte Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") - - - # Rufe die interne Methode auf, die den OpenAI Call fuer den Batch macht. - # _process_verification_openai_batch (derselbe Block) ist mit retry_on_failure dekoriert. - # Wenn _process_verification_openai_batch eine Exception wirft (nach Retries), wird diese hier gefangen. - batch_results = self._process_verification_openai_batch(current_openai_batch_data) - # Ergebnisse sollten ein Dictionary {row_num: raw_chatgpt_answer} sein, auch bei Fehlern. - - - # Sammle Sheet Updates basierend auf den Batch-Ergebnissen. - # Setze immer den Timestamp AX und die Werte in S, T, U und V-Y. - # Der aktuelle Zeitstempel fuer den Batch - current_batch_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen - - - # Iteriere ueber die Zeilennummern, die in DIESEM OpenAI Batch waren - for row_num in rows_in_current_openai_batch: - # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary. - # Fallback auf einen Fehlerstring, wenn das Ergebnis fehlt (sollte nicht passieren, wenn _process_verification_openai_batch korrekt ist). - answer = batch_results.get(row_num, "FEHLER: Batch-Ergebnis fehlt") - # self.logger.debug(f"Zeile {row_num} Verifizierungsantwort: '{answer[:100]}...'") # Zu viel Laerm (gekuerzt) - - - # Logik zur Bestimmung der Werte fuer S, T, U basierend auf 'answer' (aehnlich wie in altem _process_batch) - wiki_confirm, alt_article, wiki_explanation = "", "", "" # Initialisiere mit leeren Strings - - # Pruefe auf Standard-Antworten und Fehler-Antworten - if isinstance(answer, str) and answer.upper() == "OK": - wiki_confirm = "OK" - wiki_explanation = "Passt laut KI zur Firma." # Standard Begruendung bei OK - elif isinstance(answer, str) and answer.startswith("X |"): - # Parse die Antwort im Format "X | | " - parts = answer.split("|", 2) # Teile maximal in 3 Teile - wiki_confirm = "X" # Status ist X - if len(parts) > 1: - detail = parts[1].strip() # Zweiter Teil ist Detail (Alternative URL oder "Kein passender Artikel gefunden") - if detail.lower().startswith("alternativer artikel:"): - alt_article = detail.split(":", 1)[1].strip() # Extrahiere URL - elif detail.lower() == "kein passender artikel gefunden": - alt_article = detail # Text "Kein passender Artikel gefunden" - else: - alt_article = detail # Unbekanntes Detail - - if len(parts) > 2: - reason_part = parts[2].strip() # Dritter Teil ist Begruendung - if reason_part.lower().startswith("begruendung:"): - wiki_explanation = reason_part.split(":", 1)[1].strip() # Extrahiere Begruendungstext - else: - wiki_explanation = reason_part # Unbekannte Begruendung - - # Fuege ggf. den rohen Antworttext zur Begruendung hinzu, wenn Parsing unvollstaendig war - if not alt_article or not wiki_explanation: - wiki_explanation += f" (Rohantwort: {answer[:100]}...)" - - - elif isinstance(answer, str) and answer.startswith("FEHLER"): - # Wenn die Batch-Verarbeitung einen Fehler zurueckgegeben hat - wiki_confirm = "FEHLER" - wiki_explanation = answer # Fehlermeldung in Begruendung schreiben - alt_article = "Siehe Begruendung" # Verweis auf Begruendung - - else: # Unerwartetes Format der Antwort (weder OK noch X | noch FEHLER) - wiki_confirm = "?" # Setze Status auf unbekannt - wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..." # Speichere Anfang der Antwort in Begruendung (gekuerzt) - alt_article = "Siehe Begruendung" # Verweis auf Begruendung - - # Spalten V-Y (Begruendung bei Abweichung etc.) werden in diesem Modus geleert - # Fuer jede Zeile im Batch fuegen wir das Update hinzu. - # empty_vy_values wurde oben vorbereitet. - v_y_values = empty_vy_values # Liste von leeren Strings - # Fuege Update zum Leeren von V-Y hinzu, falls Index gefunden wurde - if v_y_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte - batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [v_y_values]}) # Block 1 Column Map, interne Helfer - - - # Fuege Updates fuer S, T, U und AX hinzu (nutzt interne Helfer) - batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [[wiki_confirm]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [[alt_article]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [[wiki_explanation]]}) # Block 1 Column Map - # Setze AX Timestamp fuer diese Zeile - batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [[current_batch_timestamp]]}) # Block 1 Column Map - - - # --- Sende gesammelte Updates fuer diesen Batch --- - # Sammle die Updates fuer diesen Batch in der globalen Liste. - # all_sheet_updates.extend(batch_sheet_updates) # Nicht hier sammeln, sondern direkt senden - - # Sende die gesammelten Updates fuer DIESEN Batch sofort. - if batch_sheet_updates: - self.logger.debug(f" Sende Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...") - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. - # Wenn es fehlschlaegt, wird es intern geloggt. - success = self.sheet_handler.batch_update_cells(batch_sheet_updates) - if success: - self.logger.info(f" Sheet-Update fuer Wiki-Verifizierungs-Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.") - # Der Fehlerfall wird von batch_update_cells geloggt - - # Setze Batch-Listen zurueck fuer die naechste Iteration - current_openai_batch_data = [] - rows_in_current_openai_batch = [] - - # Pause nach jedem OpenAI Batch (nutzt Config Block 1). - # Dies ist wichtig, um Rate Limits zu vermeiden. - # Nutze Config.RETRY_DELAY, ggf. kuerzer, da es ein Batch war - pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.5 # 50% der Retry-Wartezeit - self.logger.debug(f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---") - time.sleep(pause_duration) - - - # --- Verarbeitung des letzten unvollstaendigen Batches nach der Schleife --- - # Wenn nach der Hauptschleife noch Tasks in der Batch-Liste sind - if current_openai_batch_data: - # Logge den Start des finalen Batches - batch_start_row = current_openai_batch_data[0]['row_num'] - batch_end_row = current_openai_batch_data[-1]['row_num'] - self.logger.debug(f"\n--- Starte FINALEN Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") - - # Rufe die interne Methode auf, die den OpenAI Call macht - batch_results = self._process_verification_openai_batch(current_openai_batch_data) - # Ergebnisse sollten ein Dictionary {row_num: raw_chatgpt_answer} sein, auch bei Fehlern. - - # Sammle Sheet Updates (S, T, U, V-Y, AX) fuer diesen finalen Batch - current_batch_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen - - # Iteriere ueber die Zeilennummern, die in DIESEM finalen OpenAI Batch waren - for row_num in rows_in_current_openai_batch: - # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary. - answer = batch_results.get(row_num, "FEHLER: Batch-Ergebnis fehlt") # Fallback - - # Logik zur Bestimmung der Werte fuer S, T, U basierend auf 'answer' - wiki_confirm, alt_article, wiki_explanation = "", "", "" - # Leere V-Y Spalten - v_y_values = empty_vy_values # Liste von leeren Strings - if isinstance(answer, str) and answer.upper() == "OK": wiki_confirm = "OK"; wiki_explanation = "Passt laut KI zur Firma." - elif isinstance(answer, str) and answer.startswith("X |"): - parts = answer.split("|", 2); wiki_confirm = "X" - if len(parts) > 1: detail = parts[1].strip(); alt_article = detail.split(":", 1)[1].strip() if detail.lower().startswith("alternativer artikel:") else detail - if len(parts) > 2: reason_part = parts[2].strip(); wiki_explanation = reason_part.split(":", 1)[1].strip() if reason_part.lower().startswith("begruendung:") else reason_part - if not alt_article or not wiki_explanation: wiki_explanation += f" (Rohantwort: {answer[:100]}...)" - elif isinstance(answer, str) and answer.startswith("FEHLER"): wiki_confirm = "FEHLER"; wiki_explanation = answer; alt_article = "Siehe Begruendung" - else: wiki_confirm = "?"; wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..."; alt_article = "Siehe Begruendung" - - - # Fuege Updates fuer S, T, U und AX hinzu - batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [[wiki_confirm]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [[alt_article]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [[wiki_explanation]]}) # Block 1 Column Map - # Setze AX Timestamp - batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [[current_batch_timestamp]]}) # Block 1 Column Map - - # Fuege Update zum Leeren von V-Y hinzu, falls Index gefunden wurde - if v_y_range_letter: - batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [v_y_values]}) # Block 1 Column Map, interne Helfer - - - # Sende die gesammelten Updates fuer DIESEN finalen Batch. - if batch_sheet_updates: - self.logger.debug(f" Sende FINALES Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen)...") - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. - success = self.sheet_handler.batch_update_cells(batch_sheet_updates) - if success: - self.logger.info(f" FINALES Sheet-Update fuer Wiki-Verifizierungs-Batch erfolgreich.") - # Der Fehlerfall wird von batch_update_cells geloggt - - - # Logge den Abschluss des Modus - self.logger.info(f"Wikipedia-Verifizierungs-Batch abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen ({skipped_no_wiki_url} wegen fehlender M-URL).") - # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. - - -# ============================================================================== -# Ende DataProcessor Klasse Batch: Wiki Verification Block -# ============================================================================== - - # ========================================================================== - # === Batch Processing Methods ============================================= - # ========================================================================== - - # --- Interne Hilfsfunktion fuer Wiki-Verifizierungs-Batch (OpenAI Call) --- - # Diese Funktion verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen ueber OpenAI. - # Sie wird von process_verification_batch (derselben Block) aufgerufen. - # Nutzt globale Helfer: call_openai_chat (Block 8), logger, token_count (optional Block 3), retry_on_failure (Block 2), re. - @retry_on_failure # Wende den Decorator auf den gesamten Batch-API Call an - def _process_verification_openai_batch(self, batch_data): - """ - Verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen ueber OpenAI. - Sammelt die Ergebnisse und gibt sie zurueck. Aktualisiert NICHT das Sheet direkt. - - Args: - batch_data (list): Liste von Dictionaries, jedes enthaelt: - {'row_num': int, 'company_name': str, 'crm_desc': str, - 'wiki_url': str, 'wiki_paragraph': str, 'wiki_categories': str} - - Returns: - dict: Ein Dictionary, das Zeilennummern auf ihre rohe ChatGPT-Antwort mappt. - z.B. {2122: "OK", 2123: "X | ..."} - Bei Fehlern oder fehlenden Antworten wird ein Fehlerstring verwendet. - Wirft Exception bei endgueltigen API-Fehlern nach Retries. - """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - if not batch_data: - return {} # Gebe leeres Dictionary zurueck, wenn keine Tasks da sind - - self.logger.debug(f"Sende OpenAI-Batch fuer Wiki-Verifizierung ({len(batch_data)} Zeilen, Start: {batch_data[0]['row_num'] if batch_data else 'N/A'})...") # Sichere Indexierung - - # --- Prompt Erstellung --- - # Verwenden Sie klare Anweisungen und das definierte Antwortformat. - # Vermeiden Sie Umlaute im Prompt, um Encoding-Probleme zu minimieren. - aggregated_prompt = ( - "Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln fuer Unternehmen. " - "Fuer jeden der folgenden Eintraege pruefe, ob der vorhandene Wikipedia-Artikel (URL, Absatz, Kategorien) plausibel zum Firmennamen und zur Beschreibung passt. " - "Gib das Ergebnis fuer jeden Eintrag ausschliesslich im folgenden Format auf einer neuen Zeile aus:\n" - "Eintrag : \n\n" - "Moegliche Antworten:\n" - "- 'OK' (wenn der Artikel gut passt)\n" - "- 'X | Alternativer Artikel: | Begruendung: ' (wenn der Artikel nicht passt, aber ein besserer gefunden wurde)\n" - "- 'X | Kein passender Artikel gefunden | Begruendung: ' (wenn der Artikel nicht passt und kein besserer gefunden wurde)\n" - # Der Fall "Kein Wikipedia-Eintrag vorhanden" wird vom Skript VOR diesem Call behandelt - # und sollte hier nicht vom KI-Modell generiert werden. - "Stelle sicher, dass du nur EINE Zeile pro Eintrag im Format 'Eintrag X: Antwort' ausgibst.\n\n" - "Eintraege zur Pruefung:\n" - "--------------------\n" - ) - - # Fuegen Sie die Daten fuer jeden Eintrag im Batch hinzu. - # Kuerzen Sie die Beschreibungen und Kategorien, um das Prompt-Limit zu reduzieren. - # Stellen Sie sicher, dass die Werte Strings sind und "k.A." richtig behandelt wird. - max_desc_length = 200 # Maximale Laenge fuer Beschreibungsteile im Prompt - for item in batch_data: - row_num = item['row_num'] - # Holen und Kuerzen Sie die Werte sicher. Ersetzen Sie None durch "k.A.". - company_name = str(item.get('company_name', 'k.A.')) - crm_desc = str(item.get('crm_desc', 'k.A.')) - wiki_url = str(item.get('wiki_url', 'k.A.')) - wiki_paragraph = str(item.get('wiki_paragraph', 'k.A.')) - wiki_categories = str(item.get('wiki_categories', 'k.A.')) - - # Kuerzen Sie die Laengen und fuegen Sie "..." hinzu, wenn gekuerzt wurde. - crm_desc_short = crm_desc[:max_desc_length] + '...' if len(crm_desc) > max_desc_length else crm_desc - wiki_paragraph_short = wiki_paragraph[:max_desc_length] + '...' if len(wiki_paragraph) > max_desc_length else wiki_paragraph - wiki_categories_short = wiki_categories[:max_desc_length] + '...' if len(wiki_categories) > max_desc_length else wiki_categories - - - entry_text = ( - f"Eintrag {row_num}:\n" - f" Firmenname: {company_name}\n" - f" CRM-Beschreibung: {crm_desc_short}\n" - f" Wikipedia-URL: {wiki_url}\n" - f" Wiki-Absatz: {wiki_paragraph_short}\n" - f" Wiki-Kategorien: {wiki_categories_short}\n" - f"----\n" - ) - aggregated_prompt += entry_text - - - # Fuegen Sie den Abschluss des Prompts hinzu. - aggregated_prompt += "--------------------\nBitte nur die 'Eintrag X: Antwort'-Zeilen ausgeben." - - # Optional: Token zaehlen fuer den Prompt. - # try: prompt_tokens = token_count(aggregated_prompt, model=getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')); logger.debug(f"Geschaetzt Prompt-Tokens fuer Batch: {prompt_tokens}."); - # except Exception as e_tc: logger.debug(f"Fehler beim Token-Zaehlen: {e_tc}"); - - - # --- ChatGPT Aufruf --- - # call_openai_chat (Block 8) nutzt den retry_on_failure Decorator und wirft bei endgueltigem Fehler eine Exception. - # Der retry_on_failure Decorator auf dieser summarize_batch_openai Funktion (Block 9) faengt die Exception - # von call_openai_chat und fuehrt die Retries fuer die GESAMTE Batch-Funktion durch. - chat_response = None - try: - # Rufe die zentrale OpenAI Chat API Funktion auf (Block 8). - # Standard Temperatur 0.0 fuer Klassifizierung/Verifizierung. - chat_response = call_openai_chat(aggregated_prompt, temperature=0.0) - # Wenn call_openai_chat erfolgreich ist, gibt es den String zurueck. - # Exceptions werden nach Retries von call_openai_chat geworfen und vom aeusseren retry_on_failure dieser Funktion gefangen. - - if not chat_response: - # Dieser Fall sollte nach der Aenderung in call_openai_chat (wirft Exception) nicht mehr auftreten. - logger.error("call_openai_chat gab unerwarteterweise None zurueck fuer Wiki-Verifizierungs-Batch.") - # Werfen Sie eine spezifische Exception, damit der aeussere Decorator sie faengt. - raise openai.error.APIError("Keine Antwort von OpenAI erhalten fuer Wiki-Verifizierungs-Batch.") - - - except Exception as e: - # Wenn call_openai_chat oder der aeussere retry_on_failure eine Exception wirft (nach Retries) - # Die Exception wird hier gefangen, bevor sie an den Aufrufer (process_verification_batch) weitergeleitet wird. - logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Wiki-Verifizierung (innerhalb Batch Decorator): {e}") - # Logge den Traceback - self.logger.debug(traceback.format_exc()) - # Geben Sie ein Dictionary zurueck, das signalisiert, dass fuer alle Zeilen im Batch ein Fehler aufgetreten ist - return {item['row_num']: f"FEHLER API: {str(e)[:100]}" for item in batch_data} - - - # --- Antwort parsen --- - answers = {} # Initialisieren Sie das Ergebnis-Dictionary - # Liste der Zeilennummern, die im ursprünglichen Batch angefragt wurden - original_batch_row_nums = {item['row_num'] for item in batch_data} - lines = chat_response.strip().split('\n') - parsed_count = 0 - for line in lines: - # Matcht "Eintrag :" und den Rest der Zeile - match = re.match(r"Eintrag (\d+): (.*)", line.strip()) - if match: - row_num = int(match.group(1)) - answer_text = match.group(2).strip() - # Pruefen Sie, ob die Zeilennummer im urspruenglichen Batch angefragt wurde - if row_num in original_batch_row_nums: - answers[row_num] = answer_text - parsed_count += 1 - # else: logger.debug(f"Warnung: Antwort fuer unerwartete Zeilennummer {row_num} im Batch erhalten: {answer_text[:100]}...") # Zu viel Laerm (gekuerzt loggen) - - # Logge das Ergebnis des Parsens - self.logger.debug(f"OpenAI-Batch-Antwort geparst: {parsed_count} von {len(original_batch_row_nums)} Zeilen erfolgreich zugeordnet.") - - # Fuegen Sie einen Fehlerwert fuer Zeilen hinzu, die nicht geparst werden konnten (z.B. falsches Antwortformat) - if parsed_count < len(original_batch_row_nums): - logger.warning(f"Nicht alle Zeilen aus dem Batch ({len(original_batch_row_nums)}) konnten in der OpenAI-Antwort ({len(lines)} Zeilen) geparst werden.") - # Logge den Anfang der unvollstaendigen Antwort auf Debug - logger.debug(f"Unerwartete Antwortteile (erste 500 Zeichen): {chat_response[:500]}") - for row_num in original_batch_row_nums: - if row_num not in answers: - answers[row_num] = "FEHLER: Antwort nicht geparst" - - - # Die 'answers' Dictionary enthaelt nun Ergebnisse fuer alle Zeilen, entweder geparst oder mit einem Fehlerstring. - return answers # Rueckgabe des Dictionarys mit Ergebnissen oder Fehlern - - - # --- Methode fuer den Wiki-Verifizierungs-Batchmodus (AX) --- - # Diese Methode koordiniert die Auswahl der Zeilen, die Batch-Verarbeitung durch OpenAI, - # und das Schreiben der Ergebnisse (S, T, U, V-Y, AX, AP) ins Sheet. - # Basierend auf process_verification_only und _process_batch aus Teil 8. - # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter, _process_verification_openai_batch (derselben Block). - # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, Config (Block 1), datetime, time. - # Nutzt die uebergeordnete sheet_handler Instanz (Block 14). - def process_verification_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None): - """ - Batch-Prozess nur fuer Wikipedia-Verifizierung (Spalten S-U, V-Y werden geleert). - Laedt Daten neu, prueft fuer jede Zeile im Bereich, ob Timestamp AX (Wiki Verif.) - bereits gesetzt ist, ob eine Wiki URL (M) vorhanden ist und ob Status S - nicht bereits 'OK', 'X (URL Copied)' oder 'X (Invalid Suggestion)' ist. - Setzt AX + AP fuer bearbeitete Zeilen und schreibt S-U in Batches. - - Args: - start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (automatische Ermittlung basierend auf leeren AX). - end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet). - limit (int, optional): Maximale Anzahl ZU VERARBEITENDER (nicht uebersprungener) Zeilen. Defaults to None (Unbegrenzt). - """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - # Logge die Konfiguration des Batch-Laufs - self.logger.info(f"Starte Wikipedia-Verifizierungsmodus (Batch S-U, AX). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") - - - # --- Daten laden und Startzeile ermitteln --- - # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt - if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AX...") - # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AX (Block 1 Column Map). - # Standardmaessig ab Zeile 7 - start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wiki Verif. Timestamp", min_sheet_row=7) - - # Wenn get_start_row_index -1 zurueckgibt (Fehler) - if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") - return # Beende die Methode - - # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index - start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AX Zelle): {start_sheet_row}") - else: - # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. - # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). - if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_verification_batch.") - return # Beende die Methode, wenn das Laden fehlschlaegt - - - # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. - all_data = self.sheet_handler.get_all_data_with_headers(); - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). - header_rows = self.sheet_handler._header_rows; - total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet - - # Berechne Endzeile, wenn nicht manuell gesetzt - if end_sheet_row is None: - end_sheet_row = total_sheet_rows # Bis zur letzten Zeile - - # Logge den verarbeitungsbereich - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") - - # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) - if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") - return # Beende die Methode, wenn der Bereich leer ist - - - # --- Indizes und Buchstaben --- - # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind - required_keys = [ - "Wiki Verif. Timestamp", "Wiki URL", "Chat Wiki Konsistenzpruefung", # Pruefkriterien / Timestamp (AX, M, S) - "CRM Name", "CRM Beschreibung", "Wiki Absatz", "Wiki Kategorien", # Daten fuer Prompt (B, F, N, R) - "Chat Begruendung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", # Ergebnisspalten (T, U) - "Begruendung bei Abweichung", "Chat Begruendung Abweichung Branche", # Spalten V-Y zum Leeren - "Wikipedia Timestamp", "Timestamp letzte Pruefung", # Spalten AN, AO zum Leeren - "Version", "SerpAPI Wiki Search Timestamp" # Spalten AP, AY zum Leeren - ] - # Erstellen Sie ein Dictionary mit Schluesseln und Indizes - col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} - - # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden - if None in col_indices.values(): - missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_verification_batch: {missing}. Breche ab.") - return # Beende die Methode bei kritischem Fehler - - - # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt interne Helfer _get_col_letter Block 14) - ts_ax_letter = self.sheet_handler._get_col_letter(col_indices["Wiki Verif. Timestamp"] + 1) # Timestamp zu setzen (AX) - s_letter = self.sheet_handler._get_col_letter(col_indices["Chat Wiki Konsistenzpruefung"] + 1) # Status S - t_letter = self.sheet_handler._get_col_letter(col_indices["Chat Begruendung Wiki Inkonsistenz"] + 1) # Begruendung T - u_letter = self.sheet_handler._get_col_letter(col_indices["Chat Vorschlag Wiki Artikel"] + 1) # Vorschlag U - - # Spalten V-Y leeren (werden in diesem Modus nicht neu befuellt). - # V ist Begruendung bei Abweichung (von Wiki-URL Pruefung CRM vs Wiki). - # Y ist Begruendung Abweichung Branche (von Chat). - v_idx = col_indices["Begruendung bei Abweichung"] - y_idx = col_indices["Chat Begruendung Abweichung Branche"] # Block 1 Column Map - # Erstellen Sie den Bereichsnamen (z.B. "V:Y") - v_letter = self.sheet_handler._get_col_letter(v_idx + 1) - y_letter = self.sheet_handler._get_col_letter(y_idx + 1) - v_y_range_letter = f'{v_letter}:{y_letter}' # z.B. V:Y - # Erstellen Sie eine Liste von leeren Strings fuer diesen Bereich - empty_vy_values = [''] * (y_idx - v_idx + 1) # Anzahl der Spalten = Y_Index - V_Index + 1 - - - # Timestamps AN, AO, AP, AY leeren. - # Diese werden von anderen Schritten gesetzt und sollen hier zurueckgesetzt werden, - # um sicherzustellen, dass die Zeile bei Bedarf von diesen anderen Schritten erneut bearbeitet wird. - an_letter = self.sheet_handler._get_col_letter(col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS) - ao_letter = self.sheet_handler._get_col_letter(col_indices["Timestamp letzte Pruefung"] + 1) # AO (Chat Evaluation TS) - ap_letter = self.sheet_handler._get_col_letter(col_indices["Version"] + 1) # AP (Version) - ay_letter = self.sheet_handler._get_col_letter(col_indices["SerpAPI Wiki Search Timestamp"] + 1) # AY (SerpAPI Wiki TS) - - - # --- Verarbeitung --- - # Holen Sie die Batch-Groesse fuer OpenAI-Aufrufe aus Config (Block 1) - openai_batch_size = getattr(Config, 'PROCESSING_BATCH_SIZE', 20) # Nutzt dieselbe Batch-Groesse wie Scraping/Summarization - # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1) - update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) - - - current_openai_batch_data = [] # Daten fuer den aktuellen OpenAI Batch (Liste von Dicts) - rows_in_current_openai_batch = [] # 1-basierte Zeilennummern im aktuellen OpenAI Batch - all_sheet_updates = [] # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) - - - processed_count = 0 # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den Batch aufgenommen werden (im Rahmen des Limits). - skipped_count = 0 # Zaehlt Zeilen, die uebersprungen wurden (wegen Status, fehlender Daten etc.). - skipped_no_wiki_url = 0 # Zaehlt Zeilen, die speziell wegen fehlender M-URL uebersprungen wurden. - - - # Iteriere ueber die Sheet-Zeilen im definierten Bereich (1-basierte Sheet-Zeilennummer) - for i in range(start_sheet_row, end_sheet_row + 1): - row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste - # Pruefen Sie, ob das Ende des Sheets erreicht wurde - if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht - - - row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile - - - # Stellen Sie sicher, dass die Zeile nicht leer ist (mindestens Name vorhanden) - # Nutzt interne Helfer _get_cell_value_safe - company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - if not company_name: - self.logger.debug(f"Zeile {i}: Uebersprungen (Kein Firmenname in Spalte B).") - skipped_count += 1 # Zaehlen als uebersprungen - continue # Springe zur naechsten Zeile - - # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist --- - # Kriterium: Wiki Verif. Timestamp (AX) ist leer - # UND Wiki URL (M) ist gefuellt und gueltig aussehend (nicht k.A., Fehler etc.) - # UND Status S ist NICHT bereits in einem Endzustand (OK, X (UPDATED/COPIED/INVALID)). - - # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne Helfer) - ax_value = self._get_cell_value_safe(row, "Wiki Verif. Timestamp").strip() # Block 1 Column Map - m_value = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map - s_value_upper = self._get_cell_value_safe(row, "Chat Wiki Konsistenzpruefung").strip().upper() # Block 1 Column Map - - # Pruefen Sie, ob die Wiki URL (M) gueltig aussieht - is_wiki_url_valid_looking = m_value and isinstance(m_value, str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu basierend auf Log - - - # Definieren Sie die Endzustaende von Status S (Grossbuchstaben) - s_end_states = ["OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID SUGGESTION)"] - # Pruefen Sie, ob Status S in einem Endzustand ist - is_s_in_endstate = is_s_in_endstate = s_value_upper in s_end_states # Bugfix: variable is_s_in_endstate wurde falsch zugewiesen. - - # Verarbeitung ist noetig, wenn AX leer UND M gefuellt/gueltig aussieht UND S NICHT im Endzustand ist. - processing_needed_for_row = not ax_value and is_wiki_url_valid_looking and not is_s_in_endstate - - - # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level - log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) - if log_check: - self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Verif. Check): AX leer? {not ax_value}, M gueltig? {is_wiki_url_valid_looking}, S ('{s_value_upper}') Endzustand? {is_s_in_endstate}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen - - - # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist - if not processing_needed_for_row: - skipped_count += 1 # Zaehlen als uebersprungene Zeile - # Zaehlen Sie separat, wenn die Zeile speziell wegen fehlender M-URL uebersprungen wurde - if not is_wiki_url_valid_looking: skipped_no_wiki_url += 1 - continue # Springe zur naechsten Zeile - - - # --- Wenn Verarbeitung noetig: Fuege zur Batch-Liste fuer OpenAI hinzu --- - processed_count += 1 # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im Rahmen des Limits zaehlen) - - # Pruefe das Limit fuer verarbeitete Zeilen - if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: - # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_verification_batch erreicht. Breche weitere Zeilenpruefung ab.") - break # Brich die Schleife ab - - - # Sammle die benoetigten Daten fuer den OpenAI Prompt (_process_verification_openai_batch Block 26). - # Diese Daten werden in einem Dictionary fuer den Batch gesammelt. - crm_desc = self._get_cell_value_safe(row, "CRM Beschreibung") # Block 1 Column Map - wiki_paragraph = self._get_cell_value_safe(row, "Wiki Absatz") # Block 1 Column Map - wiki_categories = self._get_cell_value_safe(row, "Wiki Kategorien") # Block 1 Column Map - - - # Fuege die Daten dieser Zeile zur aktuellen Batch-Liste fuer OpenAI hinzu - current_openai_batch_data.append({ - 'row_num': i, # Die 1-basierte Sheet-Zeilennummer - 'company_name': company_name, # Nutzt den initial geladenen Namen - 'crm_desc': crm_desc, - 'wiki_url': m_value, # Nutzt die M-URL aus dem Sheet - 'wiki_paragraph': wiki_paragraph, - 'wiki_categories': wiki_categories - }) - # Fuege die Zeilennummer zur Liste der Zeilennummern im Batch hinzu - rows_in_current_openai_batch.append(i) - - - # --- Verarbeite den Batch, wenn voll --- - # Pruefe, ob die aktuelle Batch-Liste die definierte Groesse erreicht hat. - # openai_batch_size wird aus Config geholt (Block 1). - if len(current_openai_batch_data) >= openai_batch_size: - # Logge den Start der Batch-Verarbeitung - batch_start_row = current_openai_batch_data[0]['row_num'] - batch_end_row = current_openai_batch_data[-1]['row_num'] - self.logger.debug(f"\n--- Starte Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") - - - # Rufe die interne Methode auf, die den OpenAI Call fuer den Batch macht. - # _process_verification_openai_batch (derselbe Block) ist mit retry_on_failure dekoriert. - # Wenn _process_verification_openai_batch eine Exception wirft (nach Retries), wird diese hier gefangen. - batch_results = self._process_verification_openai_batch(current_openai_batch_data) - # Ergebnisse sollten ein Dictionary {row_num: raw_chatgpt_answer} sein, auch bei Fehlern. - - - # Sammle Sheet Updates basierend auf den Batch-Ergebnissen. - # Setze immer den Timestamp AX und die Werte in S, T, U und V-Y. - # Der aktuelle Zeitstempel fuer den Batch - current_batch_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen - - - # Iteriere ueber die Zeilennummern, die in DIESEM OpenAI Batch waren - for row_num in rows_in_current_openai_batch: - # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary. - # Fallback auf einen Fehlerstring, wenn das Ergebnis fehlt (sollte nicht passieren, wenn _process_verification_openai_batch korrekt ist). - answer = batch_results.get(row_num, "FEHLER: Batch-Ergebnis fehlt") - # self.logger.debug(f"Zeile {row_num} Verifizierungsantwort: '{answer[:100]}...'") # Zu viel Laerm (gekuerzt) - - - # Logik zur Bestimmung der Werte fuer S, T, U basierend auf 'answer' (aehnlich wie in altem _process_batch) - wiki_confirm, alt_article, wiki_explanation = "", "", "" # Initialisiere mit leeren Strings - - # Pruefe auf Standard-Antworten und Fehler-Antworten - if isinstance(answer, str) and answer.upper() == "OK": - wiki_confirm = "OK" - wiki_explanation = "Passt laut KI zur Firma." # Standard Begruendung bei OK - elif isinstance(answer, str) and answer.startswith("X |"): - # Parse die Antwort im Format "X | | " - parts = answer.split("|", 2) # Teile maximal in 3 Teile - wiki_confirm = "X" # Status ist X - if len(parts) > 1: - detail = parts[1].strip() # Zweiter Teil ist Detail (Alternative URL oder "Kein passender Artikel gefunden") - if detail.lower().startswith("alternativer artikel:"): - alt_article = detail.split(":", 1)[1].strip() # Extrahiere URL - elif detail.lower() == "kein passender artikel gefunden": - alt_article = detail # Text "Kein passender Artikel gefunden" - else: - alt_article = detail # Unbekanntes Detail - - if len(parts) > 2: - reason_part = parts[2].strip() # Dritter Teil ist Begruendung - if reason_part.lower().startswith("begruendung:"): - wiki_explanation = reason_part.split(":", 1)[1].strip() # Extrahiere Begruendungstext - else: - wiki_explanation = reason_part # Unbekannte Begruendung - - # Fuege ggf. den rohen Antworttext zur Begruendung hinzu, wenn Parsing unvollstaendig war - if not alt_article or not wiki_explanation: - wiki_explanation += f" (Rohantwort: {answer[:100]}...)" - - - elif isinstance(answer, str) and answer.startswith("FEHLER"): - # Wenn die Batch-Verarbeitung einen Fehler zurueckgegeben hat - wiki_confirm = "FEHLER" - wiki_explanation = answer # Fehlermeldung in Begruendung schreiben - alt_article = "Siehe Begruendung" # Verweis auf Begruendung - - else: # Unerwartetes Format der Antwort (weder OK noch X | noch FEHLER) - wiki_confirm = "?" # Setze Status auf unbekannt - wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..." # Speichere Anfang der Antwort in Begruendung (gekuerzt) - alt_article = "Siehe Begruendung" # Verweis auf Begruendung - - # Spalten V-Y (Begruendung bei Abweichung etc.) werden in diesem Modus geleert - # Fuer jede Zeile im Batch fuegen wir das Update hinzu. - # empty_vy_values wurde oben vorbereitet. - v_y_values = empty_vy_values # Liste von leeren Strings - # Fuege Update zum Leeren von V-Y hinzu, falls Index gefunden wurde - if v_y_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte - batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [v_y_values]}) # Block 1 Column Map, interne Helfer - - - # Fuege Updates fuer S, T, U und AX hinzu (nutzt interne Helfer) - batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [[wiki_confirm]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [[alt_article]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [[wiki_explanation]]}) # Block 1 Column Map - # Setze AX Timestamp fuer diese Zeile - batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [[current_batch_timestamp]]}) # Block 1 Column Map - - - # --- Sende gesammelte Updates fuer diesen Batch --- - # Sammle die Updates fuer diesen Batch in der globalen Liste. - # all_sheet_updates.extend(batch_sheet_updates) # Nicht hier sammeln, sondern direkt senden - - # Sende die gesammelten Updates fuer DIESEN Batch sofort. - if batch_sheet_updates: - self.logger.debug(f" Sende Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...") - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. - # Wenn es fehlschlaegt, wird es intern geloggt. - success = self.sheet_handler.batch_update_cells(batch_sheet_updates) - if success: - self.logger.info(f" Sheet-Update fuer Wiki-Verifizierungs-Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.") - # Der Fehlerfall wird von batch_update_cells geloggt - - # Setze Batch-Listen zurueck fuer die naechste Iteration - current_openai_batch_data = [] - rows_in_current_openai_batch = [] - - # Pause nach jedem OpenAI Batch (nutzt Config Block 1). - # Dies ist wichtig, um Rate Limits zu vermeiden. - # Nutze Config.RETRY_DELAY, ggf. kuerzer, da es ein Batch war - pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.5 # 50% der Retry-Wartezeit - self.logger.debug(f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---") - time.sleep(pause_duration) - - - # --- Verarbeitung des letzten unvollstaendigen Batches nach der Schleife --- - # Wenn nach der Hauptschleife noch Tasks in der Batch-Liste sind - if current_openai_batch_data: - # Logge den Start des finalen Batches - batch_start_row = current_openai_batch_data[0]['row_num'] - batch_end_row = current_openai_batch_data[-1]['row_num'] - self.logger.debug(f"\n--- Starte FINALEN Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") - - # Rufe die interne Methode auf, die den OpenAI Call macht - batch_results = self._process_verification_openai_batch(current_openai_batch_data) - # Ergebnisse sollten ein Dictionary {row_num: raw_chatgpt_answer} sein, auch bei Fehlern. - - # Sammle Sheet Updates (S, T, U, V-Y, AX) fuer diesen finalen Batch - current_batch_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen - - # Iteriere ueber die Zeilennummern, die in DIESEM finalen OpenAI Batch waren - for row_num in rows_in_current_openai_batch: - # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary. - answer = batch_results.get(row_num, "FEHLER: Batch-Ergebnis fehlt") # Fallback - - # Logik zur Bestimmung der Werte fuer S, T, U basierend auf 'answer' - wiki_confirm, alt_article, wiki_explanation = "", "", "" - # Leere V-Y Spalten - v_y_values = empty_vy_values # Liste von leeren Strings - if isinstance(answer, str) and answer.upper() == "OK": wiki_confirm = "OK"; wiki_explanation = "Passt laut KI zur Firma." - elif isinstance(answer, str) and answer.startswith("X |"): - parts = answer.split("|", 2); wiki_confirm = "X" - if len(parts) > 1: detail = parts[1].strip(); alt_article = detail.split(":", 1)[1].strip() if detail.lower().startswith("alternativer artikel:") else detail - if len(parts) > 2: reason_part = parts[2].strip(); wiki_explanation = reason_part.split(":", 1)[1].strip() if reason_part.lower().startswith("begruendung:") else reason_part - if not alt_article or not wiki_explanation: wiki_explanation += f" (Rohantwort: {answer[:100]}...)" - elif isinstance(answer, str) and answer.startswith("FEHLER"): wiki_confirm = "FEHLER"; wiki_explanation = answer; alt_article = "Siehe Begruendung" - else: wiki_confirm = "?"; wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..."; alt_article = "Siehe Begruendung" - - - # Fuege Updates fuer S, T, U und AX hinzu - batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [[wiki_confirm]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [[alt_article]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [[wiki_explanation]]}) # Block 1 Column Map - # Setze AX Timestamp - batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [[current_batch_timestamp]]}) # Block 1 Column Map - - # Fuege Update zum Leeren von V-Y hinzu, falls Index gefunden wurde - if v_y_range_letter: - batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [v_y_values]}) # Block 1 Column Map, interne Helfer - - - # Sende die gesammelten Updates fuer DIESEN finalen Batch. - if batch_sheet_updates: - self.logger.debug(f" Sende FINALES Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen)...") - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. - success = self.sheet_handler.batch_update_cells(batch_sheet_updates) - if success: - self.logger.info(f" FINALES Sheet-Update fuer Wiki-Verifizierungs-Batch erfolgreich.") - # Der Fehlerfall wird von batch_update_cells geloggt - - - # Logge den Abschluss des Modus - self.logger.info(f"Wikipedia-Verifizierungs-Batch abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen ({skipped_no_wiki_url} wegen fehlender M-URL).") - # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. - - -# ============================================================================== -# Ende DataProcessor Klasse Batch: Wiki Verification Block -# ============================================================================== - - # ========================================================================== - # === Batch Processing Methods ============================================= - # ========================================================================== + # logger.debug(f"Scraping Task Zeile {row_num} abgeschlossen. Textlaenge: {len(str(raw_text))}.") # Zu viel Laerm im Debug + return {"row_num": row_num, "raw_text": raw_text, "error": error} # --- Methode fuer den Website-Scraping-Batchmodus (AR) --- # Diese Methode verarbeitet Zeilen, bei denen AR leer ist, um den Rohtext zu scrapen. - # Sie nutzt einen ThreadPoolExecutor und ruft eine globale Worker-Funktion auf. + # Sie nutzt einen ThreadPoolExecutor und ruft die interne Worker-Funktion auf. # Basierend auf process_website_batch aus Teil 9. - # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter. + # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter, _scrape_raw_text_task. # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, Config (Block 1), datetime, time, # concurrent.futures, get_website_raw (Block 11). # Nutzt die uebergeordnete sheet_handler Instanz (Block 14). @@ -6770,30 +5361,30 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Batch-Laufs - self.logger.info(f"Starte Website-Scraping (Batch AR, AT, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") + self.logger.info(f"Starte Website-Scraping (Batch AR, AT, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden und Startzeile ermitteln --- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AT...") + self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AT...") # <<< GEÄNDERT # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AT (Block 1 Column Map). # Standardmaessig ab Zeile 7 start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Website Scrape Timestamp", min_sheet_row=7) # Wenn get_start_row_index -1 zurueckgibt (Fehler) if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") + self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT return # Beende die Methode # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AT Zelle): {start_sheet_row}") + self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AT Zelle): {start_sheet_row}") # <<< GEÄNDERT else: # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_website_scraping_batch.") + self.logger.error("FEHLER beim Laden der Daten fuer process_website_scraping_batch.") # <<< GEÄNDERT return # Beende die Methode, wenn das Laden fehlschlaegt @@ -6809,11 +5400,11 @@ class DataProcessor: end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den verarbeitungsbereich - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") + self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") + self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT return # Beende die Methode, wenn der Bereich leer ist @@ -6828,7 +5419,7 @@ class DataProcessor: # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_website_scraping_batch: {missing}. Breche ab.") + self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_website_scraping_batch: {missing}. Breche ab.") # <<< GEÄNDERT return # Beende die Methode bei kritischem Fehler # Ermitteln Sie die Indizes und Buchstaben fuer Updates (AR, AT, AP) @@ -6843,59 +5434,6 @@ class DataProcessor: timestamp_col_letter = self.sheet_handler._get_col_letter(timestamp_col_idx + 1) - # --- Worker-Funktion fuer Scraping (Intern in der Methode definiert) --- - # Diese Funktion laeuft in einem separaten Thread fuer parallele Verarbeitung. - # Sie nutzt die globale Funktion get_website_raw und erhaelt sie als Argument uebergeben. - def scrape_raw_text_task(task_info, get_website_raw_func): - """ - Scrapt den Rohtext einer Website in einem separaten Thread. - Wird vom ThreadPoolExecutor in process_website_scraping_batch aufgerufen. - Nutzt die uebergebene Funktion zum Abrufen des Rohtexts. - - Args: - task_info (dict): Enthält {'row_num': int, 'url': str}. - get_website_raw_func (function): Die Funktion zum Abrufen des Website-Rohtexts (sollte die globale get_website_raw sein). - - Returns: - dict: Enthält {'row_num': int, 'raw_text': str, 'error': str}. - """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - row_num = task_info['row_num'] - url = task_info['url'] - raw_text = "k.A." - error = None - - try: - # RUFT die uebergebene Funktion zum Abrufen des Rohtexts auf. - # Der retry_on_failure Decorator auf get_website_raw_func (der hoffentlich get_website_raw ist) - # behandelt Retries und die meisten Fehler. - raw_text = get_website_raw_func(url) # <<< Ruft die uebergebene Funktion auf - - # Wenn die Funktion einen Fehler loggt und einen Fehlerstring im Ergebnis zurueckgibt, - # wird dies hier als Fehler im Task markiert. - if isinstance(raw_text, str) and (raw_text.startswith("k.A. (Fehler") or raw_text.startswith("FEHLER:")): - error = f"Scraping Fehler (Details im Rohtext): {raw_text[:100]}..." - # Der Fehler wurde bereits in get_website_raw geloggt, kein weiteres Logging hier noetig. - # Das raw_text selbst enthaelt den Fehlerstring. - - elif not isinstance(raw_text, str) or not raw_text.strip(): - # Wenn die Funktion keinen String oder einen leeren String zurueckgibt - error = "Scraping Task Fehler: Funktion gab keinen gueltigen String zurueck." - raw_text = "k.A. (Extraktion fehlgeschlagen)" # Standard-Fehlerwert - - except Exception as e: - # Dieser Block sollte jetzt sehr selten erreicht werden, da die uebergegebene Funktion - # mit retry_on_failure die meisten Fehler abfangen sollte. - # Wenn eine Exception hier durchkommt, ist es ein sehr unerwarteter Fehler im Task-Handling selbst. - error = f"Unerwarteter Fehler im Scraping Task Zeile {row_num} ({url[:100]}): {type(e).__name__} - {e}" # Gekuerzt loggen - logger.error(error) # Loggen Sie diesen unerwarteten Fehler - raw_text = "k.A. (Unerwarteter Fehler Task)" # Setze einen spezifischen Fehlerwert - - - # logger.debug(f"Scraping Task Zeile {row_num} abgeschlossen. Textlaenge: {len(str(raw_text))}.") # Zu viel Laerm im Debug - return {"row_num": row_num, "raw_text": raw_text, "error": error} - - # --- Hauptlogik: Iteriere und sammle Batches --- # Holen Sie die Batch-Groesse fuer Verarbeitung (Threading) aus Config (Block 1) processing_batch_size = getattr(Config, 'PROCESSING_BATCH_SIZE', 20) @@ -6956,7 +5494,7 @@ class DataProcessor: log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) if log_check: company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Scraping Check): AR leer/default? {ar_is_empty_or_default}, D gueltig? {website_url_is_valid_looking}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen + self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Scraping Check): AR leer/default? {ar_is_empty_or_default}, D gueltig? {website_url_is_valid_looking}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist @@ -6973,7 +5511,7 @@ class DataProcessor: # Pruefe das Limit fuer verarbeitete Zeilen if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_website_scraping_batch erreicht. Breche weitere Zeilenpruefung ab.") + self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_website_scraping_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT break # Brich die Schleife ab @@ -6990,18 +5528,18 @@ class DataProcessor: # Logge den Start der Batch-Verarbeitung batch_start_row = tasks_for_processing_batch[0]['row_num'] batch_end_row = tasks_for_processing_batch[-1]['row_num'] - self.logger.debug(f"\n--- Starte Website-Scraping Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") + self.logger.debug(f"\n--- Starte Website-Scraping Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT scraping_results = {} # Dictionary zum Speichern der Ergebnisse {row_num: raw_text} batch_error_count = 0 # Fehlerzaehler fuer diesen spezifischen Batch - self.logger.debug(f" Scrape {len(tasks_for_processing_batch)} Websites parallel (max {max_scraping_workers} worker)...") + self.logger.debug(f" Scrape {len(tasks_for_processing_batch)} Websites parallel (max {max_scraping_workers} worker)...") # <<< GEÄNDERT # Nutzt concurrent.futures.ThreadPoolExecutor fuer paralleles Scraping. # max_workers wird aus Config geholt (Block 1). with concurrent.futures.ThreadPoolExecutor(max_workers=max_scraping_workers) as executor: # Map tasks to futures. Ruft die INTERNE Worker-Funktion auf. # Uebergibt das task_info Dictionary und die globale Funktion get_website_raw (Block 11) als Argument. - future_to_task = {executor.submit(scrape_raw_text_task, task, get_website_raw): task for task in tasks_for_processing_batch} + future_to_task = {executor.submit(self._scrape_raw_text_task, task, get_website_raw): task for task in tasks_for_processing_batch} # <<< Korrigiert: interne Methode # Verarbeite die Ergebnisse, sobald sie fertig sind. for future in concurrent.futures.as_completed(future_to_task): @@ -7020,13 +5558,13 @@ class DataProcessor: # Die meisten Fehler sollten von get_website_raws retry/logging behandelt werden. row_num = task['row_num'] # Zeilennummer aus den Task-Daten err_msg = f"Unerwarteter Fehler bei Ergebnisabfrage Scraping Task Zeile {row_num} ({task['url'][:100]}): {type(exc).__name__} - {exc}" # Gekuerzt loggen - logger.error(err_msg) # Logge den Fehler + self.logger.error(err_msg) # <<< GEÄNDERT # Setze einen Standard-Fehlerwert fuer diese Zeile im Ergebnis scraping_results[row_num] = "k.A. (Unerwarteter Fehler Task)" batch_error_count += 1 # Erhoehe den Fehlerzaehler - self.logger.debug(f" Scraping fuer Batch beendet. {len(scraping_results)} Ergebnisse erhalten ({batch_error_count} Fehler in diesem Batch).") + self.logger.debug(f" Scraping fuer Batch beendet. {len(scraping_results)} Ergebnisse erhalten ({batch_error_count} Fehler in diesem Batch).") # <<< GEÄNDERT # Sammle Sheet Updates (AR, AT, AP) fuer diesen Batch. @@ -7065,12 +5603,12 @@ class DataProcessor: rows_in_update_batch = len(all_sheet_updates) // 3 # Ganzzahl-Division if rows_in_update_batch >= update_batch_row_limit: - self.logger.debug(f" Sende gesammelte Sheet-Updates ({rows_in_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") + self.logger.debug(f" Sende gesammelte Sheet-Updates ({rows_in_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. # Wenn es fehlschlaegt, wird es intern geloggt. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f" Sheet-Update fuer {rows_in_update_batch} Zeilen erfolgreich.") + self.logger.info(f" Sheet-Update fuer {rows_in_update_batch} Zeilen erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Leere die gesammelten Updates nach dem Senden. @@ -7089,16 +5627,15 @@ class DataProcessor: # Logge den Start des finalen Batches batch_start_row = tasks_for_processing_batch[0]['row_num'] batch_end_row = tasks_for_processing_batch[-1]['row_num'] - self.logger.debug(f"\n--- Starte FINALEN Website-Scraping Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") + self.logger.debug(f"\n--- Starte FINALEN Website-Scraping Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT scraping_results = {} # Dictionary fuer die Ergebnisse batch_error_count = 0 # Fehlerzaehler - self.logger.debug(f" Scrape {len(tasks_for_processing_batch)} Websites parallel (max {max_scraping_workers} worker)...") + self.logger.debug(f" Scrape {len(tasks_for_processing_batch)} Websites parallel (max {max_scraping_workers} worker)...") # <<< GEÄNDERT with concurrent.futures.ThreadPoolExecutor(max_workers=max_scraping_workers) as executor: - # Map tasks to futures. Ruft die GLOBALE Worker-Funktion auf. - # Uebergibt das task_info Dictionary und die globale Funktion get_website_raw (Block 11) als Argument. - future_to_task = {executor.submit(_scrape_raw_text_task_global, task, get_website_raw): task for task in tasks_for_processing_batch} + # Map tasks to futures. Ruft die INTERNE Worker-Funktion auf. + future_to_task = {executor.submit(self._scrape_raw_text_task, task, get_website_raw): task for task in tasks_for_processing_batch} # <<< Korrigiert: interne Methode # Verarbeite die Ergebnisse for future in concurrent.futures.as_completed(future_to_task): @@ -7112,13 +5649,13 @@ class DataProcessor: # Faengt unerwartete Fehler bei der Ergebnisabfrage ab row_num = task['row_num'] err_msg = f"Unerwarteter Fehler bei Ergebnisabfrage Scraping Task Zeile {row_num} ({task['url'][:100]}): {type(exc).__name__} - {exc}" # Gekuerzt loggen - logger.error(err_msg) # Logge den Fehler + self.logger.error(err_msg) # <<< GEÄNDERT # Setze einen Standard-Fehlerwert scraping_results[row_num] = "k.A. (Unerwarteter Fehler Task)" batch_error_count += 1 - self.logger.debug(f" FINALER Scraping Batch beendet. {len(scraping_results)} Ergebnisse erhalten ({batch_error_count} Fehler).") + self.logger.debug(f" FINALER Scraping Batch beendet. {len(scraping_results)} Ergebnisse erhalten ({batch_error_count} Fehler).") # <<< GEÄNDERT # Sammle Sheet Updates (AR, AT, AP) fuer diesen finalen Batch. if scraping_results: @@ -7139,16 +5676,16 @@ class DataProcessor: # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update. if all_sheet_updates: rows_in_final_update_batch = len(all_sheet_updates) // 3 # Updates pro Zeile ist 3 - self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({rows_in_final_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") + self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({rows_in_final_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f"FINALES Sheet-Update erfolgreich.") + self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Logge den Abschluss des Modus - self.logger.info(f"Website-Scraping (Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") + self.logger.info(f"Website-Scraping (Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. @@ -7181,30 +5718,30 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Batch-Laufs - self.logger.info(f"Starte Website-Zusammenfassung (Batch AS, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") + self.logger.info(f"Starte Website-Zusammenfassung (Batch AS, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden und Startzeile ermitteln --- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AS...") + self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AS...") # <<< GEÄNDERT # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AS (Block 1 Column Map). # Standardmaessig ab Zeile 7 start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Website Zusammenfassung", min_sheet_row=7) # Wenn get_start_row_index -1 zurueckgibt (Fehler) if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") + self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT return # Beende die Methode # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AS Zelle): {start_sheet_row}") + self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AS Zelle): {start_sheet_row}") # <<< GEÄNDERT else: # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_summarization_batch.") + self.logger.error("FEHLER beim Laden der Daten fuer process_summarization_batch.") # <<< GEÄNDERT return # Beende die Methode, wenn das Laden fehlschlaegt @@ -7220,11 +5757,11 @@ class DataProcessor: end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den verarbeitungsbereich - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") + self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") + self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT return # Beende die Methode, wenn der Bereich leer ist @@ -7239,7 +5776,7 @@ class DataProcessor: # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_summarization_batch: {missing}. Breche ab.") + self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_summarization_batch: {missing}. Breche ab.") # <<< GEÄNDERT return # Beende die Methode bei kritischem Fehler # Ermitteln Sie die Indizes und Buchstaben fuer Updates (AS, AP) @@ -7309,7 +5846,7 @@ class DataProcessor: log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) if log_check: company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Summarization Check): AR gueltig? {raw_text_is_valid} (len={len(str(raw_text))}), AS leer/default? {summary_is_empty_or_default}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen + self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Summarization Check): AR gueltig? {raw_text_is_valid} (len={len(str(raw_text))}), AS leer/default? {summary_is_empty_or_default}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist @@ -7324,7 +5861,7 @@ class DataProcessor: # Pruefe das Limit fuer verarbeitete Zeilen if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_summarization_batch erreicht. Breche weitere Zeilenpruefung ab.") + self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_summarization_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT break # Brich die Schleife ab @@ -7341,18 +5878,15 @@ class DataProcessor: # Logge den Start der Batch-Verarbeitung batch_start_row = tasks_for_openai_batch[0]['row_num'] batch_end_row = tasks_for_openai_batch[-1]['row_num'] - self.logger.debug(f"\n--- Starte Website-Summarization Batch ({len(tasks_for_openai_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") + self.logger.debug(f"\n--- Starte Website-Summarization Batch ({len(tasks_for_openai_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT # Rufe die globale Funktion auf, die den OpenAI Call fuer den Batch macht (Block 9). # summarize_batch_openai ist mit retry_on_failure dekoriert (Block 2). # Wenn summarize_batch_openai eine Exception wirft (nach Retries), wird diese hier gefangen. - batch_results = self._process_verification_openai_batch(current_openai_batch_data) # <-- Falsche Methode aufgerufen! MUSS summarize_batch_openai sein. - # TODO: Diesen Aufruf zu summarize_batch_openai aendern! - # !!! KORRIGIERTER AUFRUF !!! try: # Rufen Sie die korrekte globale Funktion auf - batch_results = summarize_batch_openai(current_openai_batch_data) # <<< Korrekter Aufruf Block 9 + batch_results = summarize_batch_openai(tasks_for_openai_batch) # <<< Korrigierter Aufruf (vorher war fälschlicherweise _process_verification_openai_batch) # Ergebnisse sollten ein Dictionary {row_num: summary_text} sein, auch bei Fehlern. # Sammle Sheet Updates (AS, AP) fuer diesen Batch @@ -7383,9 +5917,9 @@ class DataProcessor: except Exception as e_openai_batch: # Wenn summarize_batch_openai eine Exception wirft (nach Retries) # Der Fehler wird bereits vom retry_on_failure Decorator auf summarize_batch_openai geloggt. - self.logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Zusammenfassung: {e_openai_batch}") + self.logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Zusammenfassung: {e_openai_batch}") # <<< GEÄNDERT # Logge den Traceback - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT # Fügen Sie Fehlerwerte für alle Zeilen im Batch hinzu current_version = getattr(Config, 'VERSION', 'unknown') # Block 1 Config Attribut for row_num in rows_in_current_openai_batch: @@ -7405,12 +5939,12 @@ class DataProcessor: rows_in_update_batch = len(all_sheet_updates) // 2 if rows_in_update_batch >= update_batch_row_limit: - self.logger.debug(f" Sende gesammelte Sheet-Updates ({rows_in_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") + self.logger.debug(f" Sende gesammelte Sheet-Updates ({rows_in_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. # Wenn es fehlschlaegt, wird es intern geloggt. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f" Sheet-Update fuer {rows_in_update_batch} Zeilen erfolgreich.") + self.logger.info(f" Sheet-Update fuer {rows_in_update_batch} Zeilen erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Leere die gesammelten Updates nach dem Senden. @@ -7419,24 +5953,24 @@ class DataProcessor: # Kurze Pause nach jedem OpenAI Batch (nutzt Config Block 1). # Dies ist wichtig, um Rate Limits zu vermeiden. pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.5 # 50% der Retry-Wartezeit - self.logger.debug(f"Warte {pause_duration:.2f}s vor naechstem Batch...") + self.logger.debug(f"Warte {pause_duration:.2f}s vor naechstem Batch...") # <<< GEÄNDERT time.sleep(pause_duration) # --- Verarbeitung des letzten unvollstaendigen OpenAI Batches nach der Schleife --- # Wenn nach der Hauptschleife noch Tasks in der Batch-Liste sind. - if current_openai_batch_data: + if tasks_for_openai_batch: # Korrektur: War vorher `current_openai_batch_data` # Logge den Start des finalen Batches - batch_start_row = current_openai_batch_data[0]['row_num'] - batch_end_row = current_openai_batch_data[-1]['row_num'] - self.logger.debug(f"\n--- Starte FINALEN Website-Summarization Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") + batch_start_row = tasks_for_openai_batch[0]['row_num'] # Korrektur: War vorher `current_openai_batch_data` + batch_end_row = tasks_for_openai_batch[-1]['row_num'] # Korrektur: War vorher `current_openai_batch_data` + self.logger.debug(f"\n--- Starte FINALEN Website-Summarization Batch ({len(tasks_for_openai_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT # Rufe die globale Funktion auf, die den OpenAI Call fuer den Batch macht (Block 9). # summarize_batch_openai ist mit retry_on_failure dekoriert (Block 2). # Wenn summarize_batch_openai eine Exception wirft (nach Retries), wird diese hier gefangen. batch_results = None try: - batch_results = summarize_batch_openai(current_openai_batch_data) # <<< Korrekter Aufruf Block 9 + batch_results = summarize_batch_openai(tasks_for_openai_batch) # <<< Korrekter Aufruf Block 9, Korrektur: War vorher `current_openai_batch_data` # Ergebnisse sollten ein Dictionary {row_num: summary_text} sein, auch bei Fehlern. # Sammle Sheet Updates (AS, AP) fuer diesen finalen Batch @@ -7467,9 +6001,9 @@ class DataProcessor: except Exception as e_openai_batch: # Wenn summarize_batch_openai eine Exception wirft (nach Retries) # Der Fehler wird bereits vom retry_on_failure Decorator auf summarize_batch_openai geloggt. - self.logger.error(f"Endgueltiger FEHLER beim FINALEN OpenAI-Batch-Aufruf fuer Zusammenfassung: {e_openai_batch}") + self.logger.error(f"Endgueltiger FEHLER beim FINALEN OpenAI-Batch-Aufruf fuer Zusammenfassung: {e_openai_batch}") # <<< GEÄNDERT # Logge den Traceback - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT # Fügen Sie Fehlerwerte für alle Zeilen im Batch hinzu current_version = getattr(Config, 'VERSION', 'unknown') # Block 1 Config Attribut for row_num in rows_in_current_openai_batch: @@ -7483,16 +6017,16 @@ class DataProcessor: # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update. if all_sheet_updates: rows_in_final_update_batch = len(all_sheet_updates) // 2 # Updates pro Zeile ist 2 - self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({rows_in_final_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") + self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({rows_in_final_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f"FINALES Sheet-Update erfolgreich.") + self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Logge den Abschluss des Modus - self.logger.info(f"Website-Zusammenfassung (Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") + self.logger.info(f"Website-Zusammenfassung (Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. # ============================================================================== @@ -7522,6 +6056,7 @@ class DataProcessor: dict: Ergebnis von evaluate_branche_chatgpt (Block 10) plus row_num und error. """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist + logger = logging.getLogger(__name__ + ".evaluate_branch_task") # Eigener Logger für den Task row_num = task_data['row_num'] # Initialisiere Ergebnis mit Fehlerwerten, falls der Task fehlschlaegt result = {"branch": "k.A. (Fehler Task)", "consistency": "error", "justification": "Fehler in Worker-Task"} @@ -7547,9 +6082,9 @@ class DataProcessor: # Wenn evaluate_branche_chatgpt eine Exception wirft (nach Retries) # Der Fehler wird bereits vom retry_on_failure Decorator oder evaluate_branche_chatgpt geloggt. error = f"Fehler bei Branchenevaluation Zeile {row_num}: {type(e).__name__} - {e}" - self.logger.error(error) # Logge den Fehler + logger.error(error) # Logge den Fehler # Logge den Traceback - self.logger.debug(traceback.format_exc()) + logger.debug(traceback.format_exc()) # Stellen Sie sicher, dass das Ergebnis-Dict im Fehlerfall spezifische Fehlerwerte enthaelt result = {"branch": "FEHLER", "consistency": "error_task", "justification": error[:500]} # Kuerze Begruendung @@ -7579,30 +6114,30 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Batch-Laufs - self.logger.info(f"Starte Brancheneinschaetzung (Parallel Batch W-Y, AO, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") + self.logger.info(f"Starte Brancheneinschaetzung (Parallel Batch W-Y, AO, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden und Startzeile ermitteln --- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AO...") + self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AO...") # <<< GEÄNDERT # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AO (Block 1 Column Map). # Standardmaessig ab Zeile 7 start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Pruefung", min_sheet_row=7) # Wenn get_start_row_index -1 zurueckgibt (Fehler) if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") + self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT return # Beende die Methode # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AO Zelle): {start_sheet_row}") + self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AO Zelle): {start_sheet_row}") # <<< GEÄNDERT else: # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_branch_batch.") + self.logger.error("FEHLER beim Laden der Daten fuer process_branch_batch.") # <<< GEÄNDERT return # Beende die Methode, wenn das Laden fehlschlaegt @@ -7618,11 +6153,11 @@ class DataProcessor: end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den verarbeitungsbereich - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") + self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") + self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT return # Beende die Methode, wenn der Bereich leer ist @@ -7640,7 +6175,7 @@ class DataProcessor: # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_branch_batch: {missing}. Breche ab.") + self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_branch_batch: {missing}. Breche ab.") # <<< GEÄNDERT return # Beende die Methode bei kritischem Fehler # Ermitteln Sie die Spaltenbuchstaben fuer Updates (W, X, Y, AO, AP) (nutzt interne Helfer _get_col_letter Block 14) @@ -7683,7 +6218,7 @@ class DataProcessor: # Pruefe erneut, ob das Schema geladen wurde if not ALLOWED_TARGET_BRANCHES: - self.logger.critical("FEHLER: Ziel-Branchenschema konnte nach Ladeversuch nicht geladen werden. Branchenbewertung nicht moeglich. Breche Batch ab.") + self.logger.critical("FEHLER: Ziel-Branchenschema konnte nach Ladeversuch nicht geladen werden. Branchenbewertung nicht moeglich. Breche Batch ab.") # <<< GEÄNDERT return # Beende die Methode @@ -7736,7 +6271,7 @@ class DataProcessor: # Wenn nicht genuegend Informationsquellen verfuegbar sind if info_sources_count < 2: # Mindestens 2 Info-Punkte sollten vorhanden sein (kann angepasst werden) - self.logger.debug(f"Zeile {i} (Branch Check): Uebersprungen (AO leer, aber nur {info_sources_count} Informationsquellen verfuegbar). Mindestens 2 benoetigt.") + self.logger.debug(f"Zeile {i} (Branch Check): Uebersprungen (AO leer, aber nur {info_sources_count} Informationsquellen verfuegbar). Mindestens 2 benoetigt.") # <<< GEÄNDERT skipped_count += 1 # Zaehlen als uebersprungene Zeile continue # Springe zur naechsten Zeile @@ -7747,7 +6282,7 @@ class DataProcessor: # Pruefe das Limit fuer verarbeitete Zeilen if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_branch_batch erreicht. Breche weitere Zeilenpruefung ab.") + self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_branch_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT break # Brich die Schleife ab @@ -7772,13 +6307,13 @@ class DataProcessor: # Logge den Start der Batch-Verarbeitung batch_start_row = tasks_for_processing_batch[0]['row_num'] batch_end_row = tasks_for_processing_batch[-1]['row_num'] - self.logger.debug(f"\n--- Starte Branch-Evaluation Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") + self.logger.debug(f"\n--- Starte Branch-Evaluation Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT results_list = [] # Liste zum Speichern der Ergebnisse fuer diesen Batch (Liste von Dicts) batch_error_count = 0 # Fehlerzaehler fuer diesen spezifischen Batch - self.logger.debug(f" Evaluiere {len(tasks_for_processing_batch)} Zeilen parallel (max {MAX_BRANCH_WORKERS} worker, {OPENAI_CONCURRENCY_LIMIT} OpenAI gleichzeitig)...") + self.logger.debug(f" Evaluiere {len(tasks_for_processing_batch)} Zeilen parallel (max {MAX_BRANCH_WORKERS} worker, {OPENAI_CONCURRENCY_LIMIT} OpenAI gleichzeitig)...") # <<< GEÄNDERT # Holen Sie die Parallelisierungskonfiguration aus Config (Block 1). MAX_BRANCH_WORKERS = getattr(Config, 'MAX_BRANCH_WORKERS', 10) OPENAI_CONCURRENCY_LIMIT = getattr(Config, 'OPENAI_CONCURRENCY_LIMIT', 3) @@ -7811,14 +6346,14 @@ class DataProcessor: # Die meisten Fehler sollten von evaluate_branch_task oder seinen Helfern behandelt werden. row_num = task['row_num'] # Zeilennummer aus den Task-Daten err_msg = f"Unerwarteter Fehler bei Ergebnisabfrage Branch Task Zeile {row_num}: {type(exc).__name__} - {exc}" - logger.error(err_msg) # Logge den Fehler + self.logger.error(err_msg) # <<< GEÄNDERT # Setze einen Standard-Fehler-Ergebniswert fuer diese Zeile results_list.append({"row_num": row_num, "result": {"branch": "FEHLER", "consistency": "error_task", "justification": err_msg[:500]}, "error": err_msg}) # Kuerze Begruendung batch_error_count += 1 # Erhoehe den Fehlerzaehler # *** ENDE PARALLELE VERARBEITUNG *** - self.logger.debug(f" Branch-Evaluation fuer Batch beendet. {len(results_list)} Ergebnisse erhalten ({batch_error_count} Fehler in diesem Batch).") + self.logger.debug(f" Branch-Evaluation fuer Batch beendet. {len(results_list)} Ergebnisse erhalten ({batch_error_count} Fehler in diesem Batch).") # <<< GEÄNDERT # Sheet Updates vorbereiten FÜR DIESEN BATCH. @@ -7854,12 +6389,12 @@ class DataProcessor: # --- Sende Updates fuer DIESEN BATCH SOFORT --- # Sende die gesammelten Updates fuer diesen Batch. if batch_sheet_updates: - self.logger.debug(f" Sende Sheet-Update fuer {len(results_list)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...") + self.logger.debug(f" Sende Sheet-Update fuer {len(results_list)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. # Wenn es fehlschlaegt, wird es intern geloggt. success = self.sheet_handler.batch_update_cells(batch_sheet_updates) if success: - self.logger.info(f" Sheet-Update fuer Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.") + self.logger.info(f" Sheet-Update fuer Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # else: self.logger.debug(f" Keine Sheet-Updates fuer Batch Zeilen {batch_start_row}-{batch_end_row} vorbereitet.") # Zu viel Laerm im Debug @@ -7872,7 +6407,7 @@ class DataProcessor: # Pause NACHDEM ein Batch komplett verarbeitet und geschrieben wurde (nutzt Config Block 1). # Dies ist wichtig, um Rate Limits und Serverlast zu managen. pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.8 # Längere Pause, z.B. 80% der Retry-Wartezeit - self.logger.debug(f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---") + self.logger.debug(f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---") # <<< GEÄNDERT time.sleep(pause_duration) @@ -7882,13 +6417,13 @@ class DataProcessor: # Logge den Start des finalen Batches batch_start_row = tasks_for_processing_batch[0]['row_num'] batch_end_row = tasks_for_processing_batch[-1]['row_num'] - self.logger.debug(f"\n--- Starte FINALEN Branch-Evaluation Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") + self.logger.debug(f"\n--- Starte FINALEN Branch-Evaluation Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT results_list = [] # Liste zum Speichern der Ergebnisse fuer diesen finalen Batch batch_error_count = 0 # Fehlerzaehler - self.logger.debug(f" Evaluiere {len(tasks_for_processing_batch)} Zeilen parallel (max {MAX_BRANCH_WORKERS} worker, {OPENAI_CONCURRENCY_LIMIT} OpenAI gleichzeitig)...") + self.logger.debug(f" Evaluiere {len(tasks_for_processing_batch)} Zeilen parallel (max {MAX_BRANCH_WORKERS} worker, {OPENAI_CONCURRENCY_LIMIT} OpenAI gleichzeitig)...") # <<< GEÄNDERT # Erstellen Sie die Semaphore Instanz fuer den finalen Batch. openai_semaphore_branch = threading.Semaphore(OPENAI_CONCURRENCY_LIMIT) @@ -7908,13 +6443,13 @@ class DataProcessor: # Faengt unerwartete Fehler bei der Ergebnisabfrage ab row_num = task['row_num'] err_msg = f"Unerwarteter Fehler bei Ergebnisabfrage Branch Task Zeile {row_num}: {type(exc).__name__} - {exc}" - logger.error(err_msg) # Logge den Fehler + self.logger.error(err_msg) # <<< GEÄNDERT # Setze einen Standard-Fehler-Ergebniswert results_list.append({"row_num": row_num, "result": {"branch": "FEHLER", "consistency": "error_task", "justification": err_msg[:500]}, "error": err_msg}) # Kuerze Begruendung batch_error_count += 1 - self.logger.debug(f" FINALER Branch Batch beendet. {len(results_list)} Ergebnisse erhalten ({batch_error_count} Fehler).") + self.logger.debug(f" FINALER Branch Batch beendet. {len(results_list)} Ergebnisse erhalten ({batch_error_count} Fehler).") # <<< GEÄNDERT # Sammle Sheet Updates (W, X, Y, AO, AP) fuer diesen finalen Batch. @@ -7937,16 +6472,16 @@ class DataProcessor: # Sende die gesammelten Updates fuer DIESEN finalen Batch. if batch_sheet_updates: - self.logger.debug(f" Sende FINALES Sheet-Update fuer {len(results_list)} Zeilen ({len(batch_sheet_updates)} Zellen)...") + self.logger.debug(f" Sende FINALES Sheet-Update fuer {len(results_list)} Zeilen ({len(batch_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. success = self.sheet_handler.batch_update_cells(batch_sheet_updates) if success: - self.logger.info(f" FINALES Sheet-Update fuer Branch Batch erfolgreich.") + self.logger.info(f" FINALES Sheet-Update fuer Branch Batch erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Logge den Abschluss des Modus - self.logger.info(f"Brancheneinschaetzung (Parallel Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") + self.logger.info(f"Brancheneinschaetzung (Parallel Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. @@ -7983,30 +6518,30 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Batch-Laufs - self.logger.info(f"Starte Modus 'find_wiki_serp' (AY, M, A). Filter: (Umsatz CRM > {min_umsatz} MIO € ODER Mitarbeiter CRM > {min_employees}). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") + self.logger.info(f"Starte Modus 'find_wiki_serp' (AY, M, A). Filter: (Umsatz CRM > {min_umsatz} MIO € ODER Mitarbeiter CRM > {min_employees}). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden und Startzeile ermitteln --- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AY...") + self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AY...") # <<< GEÄNDERT # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AY (Block 1 Column Map). # Standardmaessig ab Zeile 7 start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="SerpAPI Wiki Search Timestamp", min_sheet_row=7) # Wenn get_start_row_index -1 zurueckgibt (Fehler) if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") + self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT return # Beende die Methode # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AY Zelle): {start_sheet_row}") + self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AY Zelle): {start_sheet_row}") # <<< GEÄNDERT else: # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_find_wiki_serp.") + self.logger.error("FEHLER beim Laden der Daten fuer process_find_wiki_serp.") # <<< GEÄNDERT return # Beende die Methode, wenn das Laden fehlschlaegt @@ -8022,11 +6557,11 @@ class DataProcessor: end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den verarbeitungsbereich - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") + self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") + self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT return # Beende die Methode, wenn der Bereich leer ist @@ -8046,7 +6581,7 @@ class DataProcessor: # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_find_wiki_serp: {missing}. Breche ab.") + self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_find_wiki_serp: {missing}. Breche ab.") # <<< GEÄNDERT return # Beende die Methode bei kritischem Fehler # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt interne Helfer _get_col_letter Block 14) @@ -8141,7 +6676,7 @@ class DataProcessor: log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) if log_check: company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - self.logger.debug(f"Zeile {i} ({company_name[:50]}... SerpAPI Wiki Search Check): AY leer? {is_ay_empty}, M leer/k.A.? {is_m_empty_or_ka}, Groesse ({umsatz_val_mio:.1f} Mio, {ma_val_num} MA) Kriterium ({min_umsatz} Mio, {min_employees} MA)? {size_criteria_met}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen + self.logger.debug(f"Zeile {i} ({company_name[:50]}... SerpAPI Wiki Search Check): AY leer? {is_ay_empty}, M leer/k.A.? {is_m_empty_or_ka}, Groesse ({umsatz_val_mio:.1f} Mio, {ma_val_num} MA) Kriterium ({min_umsatz} Mio, {min_employees} MA)? {size_criteria_met}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist @@ -8156,7 +6691,7 @@ class DataProcessor: # Pruefe das Limit fuer verarbeitete Zeilen if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_find_wiki_serp erreicht. Breche weitere Zeilenpruefung ab.") + self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_find_wiki_serp erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT break # Brich die Schleife ab @@ -8166,16 +6701,18 @@ class DataProcessor: # Wenn kein Firmenname vorhanden ist, kann die Suche nicht durchgefuehrt werden if not company_name: - self.logger.warning(f"Zeile {i}: Uebersprungen (kein Firmenname fuer Suche vorhanden in Spalte B).") + self.logger.warning(f"Zeile {i}: Uebersprungen (kein Firmenname fuer Suche vorhanden in Spalte B).") # <<< GEÄNDERT skipped_count += 1 # Zaehlen als uebersprungene Zeile, da Suche nicht moeglich # Setze AY Timestamp auch hier, um nicht immer wieder zu versuchen + # Erstelle leeres Update-Dict, damit extend funktioniert + updates = [] updates.append({'range': f'{ts_ay_letter}{i}', 'values': [[now_timestamp_str]]}) # Block 1 Column Map all_sheet_updates.extend(updates) # Fuege dieses einzelne Update zur Liste hinzu updates = [] # Leere die lokale Liste continue # Springe zur naechsten Zeile - self.logger.info(f"Zeile {i}: Suche Wiki-URL fuer '{company_name[:100]}...' (Umsatz (Mio): {umsatz_val_mio:.1f}, MA: {ma_val_num}) ueber SerpAPI...") # Gekuerzt loggen + self.logger.info(f"Zeile {i}: Suche Wiki-URL fuer '{company_name[:100]}...' (Umsatz (Mio): {umsatz_val_mio:.1f}, MA: {ma_val_num}) ueber SerpAPI...") # <<< GEÄNDERT # Führe die SerpAPI Suche durch (nutzt globale Funktion Block 10 mit Retry). @@ -8188,7 +6725,7 @@ class DataProcessor: except Exception as e_serp_wiki: # Wenn serp_wikipedia_lookup eine Exception wirft (nach Retries) - self.logger.error(f"FEHLER bei serp_wikipedia_lookup fuer Zeile {i} ('{company_name[:100]}...'): {e_serp_wiki}") # Gekuerzt loggen + self.logger.error(f"FEHLER bei serp_wikipedia_lookup fuer Zeile {i} ('{company_name[:100]}...'): {e_serp_wiki}") # <<< GEÄNDERT # wiki_url_found bleibt None. Fahren Sie fort. pass # Fahren Sie fort, um Timestamp zu setzen und Updates vorzubereiten @@ -8202,8 +6739,8 @@ class DataProcessor: # Wenn eine URL gefunden wurde, bereite weitere Updates vor. # Eine gefundene URL ist ein String, der nicht None ist und nicht "k.A." oder Fehlerstring ist. - if wiki_url_found and isinstance(wiki_url_found, str) and wiki_url_found.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"]: # Fuege "http:" hinzu - self.logger.info(f" -> URL gefunden: {wiki_url_found[:100]}... Bereite Update vor (Setze M, A; Loesche N-V, AN, AO, AP, AX).") # Gekuerzt loggen + if wiki_url_found and isinstance(wiki_url_found, str) and wiki_url_found.lower() not in ["k.a.", "kein artikel gefunden"] and not wiki_url_found.startswith("FEHLER"): # Korrektur Pruefung + self.logger.info(f" -> URL gefunden: {wiki_url_found[:100]}... Bereite Update vor (Setze M, A; Loesche N-V, AN, AO, AP, AX).") # <<< GEÄNDERT found_urls_count += 1 # Zaehle den Fund @@ -8218,10 +6755,10 @@ class DataProcessor: if nv_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte. updates_for_row.append({'range': f'{n_letter}{i}:{v_letter}{i}', 'values': [empty_nv_values]}) # Block 1 Column Map, lokale Variable else: - self.logger.warning(f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.") + self.logger.warning(f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.") # <<< GEÄNDERT - # Leere Timestamps AN, AO, AX, und Version AP. + # Leere Timestamps AN, AO, AP, AX. # Dies setzt die Zeile zurueck, damit andere Schritte sie spaeter bearbeiten. updates_for_row.append({'range': f'{an_letter}{i}', 'values': [['']]}) # Block 1 Column Map updates_for_row.append({'range': f'{ao_letter}{i}', 'values': [['']]}) # Block 1 Column Map @@ -8231,7 +6768,7 @@ class DataProcessor: else: # Wenn keine Wiki-URL ueber SerpAPI gefunden wurde - self.logger.debug(f" -> Keine Wiki-URL fuer '{company_name[:100]}...' ueber SerpAPI gefunden.") # Gekuerzt loggen + self.logger.debug(f" -> Keine Wiki-URL fuer '{company_name[:100]}...' ueber SerpAPI gefunden.") # <<< GEÄNDERT # Nur AY Timestamp wird gesetzt, was bereits oben passiert ist. Keine weiteren Updates fuer M, A, N-V etc. @@ -8244,13 +6781,13 @@ class DataProcessor: # Die Anzahl der Updates pro Zeile variiert (1 bei nicht gefunden, ca. 10+ bei gefunden). # Pruefen Sie einfach die Laenge der gesammelten Liste. if len(all_sheet_updates) >= update_batch_row_limit * 5: # Grobe Schaetzung: im Schnitt 5 Updates pro Zeile - self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") + self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. # Wenn es fehlschlaegt, wird es intern geloggt. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") - # Der Fehlerfall wird von batch_update_cells geloggt + self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") # <<< GEÄNDERT + # Der Fehlerfall wird von batch_update_cells geloggt # Leere die gesammelten Updates nach dem Senden. all_sheet_updates = [] @@ -8266,16 +6803,16 @@ class DataProcessor: # --- Finale Sheet Updates senden --- # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update. if all_sheet_updates: - self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") + self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f"FINALES Sheet-Update erfolgreich.") + self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Logge den Abschluss des Modus - self.logger.info(f"Modus 'find_wiki_serp' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {found_urls_count} URLs gefunden & eingetragen, {skipped_count} Zeilen uebersprungen.") + self.logger.info(f"Modus 'find_wiki_serp' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {found_urls_count} URLs gefunden & eingetragen, {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. @@ -8299,30 +6836,30 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Batch-Laufs - self.logger.info(f"Starte Contact Research (Batch AM, AI-AL). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") + self.logger.info(f"Starte Contact Research (Batch AM, AI-AL). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden und Startzeile ermitteln --- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AM...") + self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AM...") # <<< GEÄNDERT # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AM (Block 1 Column Map). # Standardmaessig ab Zeile 7 start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Contact Search Timestamp", min_sheet_row=7) # Wenn get_start_row_index -1 zurueckgibt (Fehler) if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") + self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT return # Beende die Methode # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AM Zelle): {start_sheet_row}") + self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AM Zelle): {start_sheet_row}") # <<< GEÄNDERT else: # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_contact_search.") + self.logger.error("FEHLER beim Laden der Daten fuer process_contact_search.") # <<< GEÄNDERT return # Beende die Methode, wenn das Laden fehlschlaegt @@ -8338,11 +6875,11 @@ class DataProcessor: end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den verarbeitungsbereich - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") + self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") + self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT return # Beende die Methode, wenn der Bereich leer ist @@ -8360,7 +6897,7 @@ class DataProcessor: # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_contact_search: {missing}. Breche ab.") + self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_contact_search: {missing}. Breche ab.") # <<< GEÄNDERT return # Beende die Methode bei kritischem Fehler @@ -8391,10 +6928,10 @@ class DataProcessor: try: # Versuche, das Sheet "Contacts" zu oeffnen contacts_sheet = self.sheet_handler.sheet.spreadsheet.worksheet("Contacts") - self.logger.info("Blatt 'Contacts' gefunden.") + self.logger.info("Blatt 'Contacts' gefunden.") # <<< GEÄNDERT except gspread.exceptions.WorksheetNotFound: # Wenn nicht gefunden, erstelle es. - self.logger.info("Blatt 'Contacts' nicht gefunden, erstelle neu...") + self.logger.info("Blatt 'Contacts' nicht gefunden, erstelle neu...") # <<< GEÄNDERT try: # Definieren Sie den Header fuer das neue Blatt contacts_header = ["Firmenname", "CRM Kurzform", "Website", "Geschlecht", "Vorname", "Nachname", "Position", "Suchbegriffskategorie", "E-Mail-Adresse", "LinkedIn-Link", "Timestamp"] @@ -8405,18 +6942,18 @@ class DataProcessor: # Schreiben Sie den Header in die erste Zeile des neuen Blattes # Nutzt _get_col_letter interne Methode des SheetHandlers (Block 14) contacts_sheet.update(values=[contacts_header], range_name=f"A1:{self.sheet_handler._get_col_letter(num_cols_contacts_sheet)}1") - self.logger.info("Neues Blatt 'Contacts' erstellt und Header eingetragen.") + self.logger.info("Neues Blatt 'Contacts' erstellt und Header eingetragen.") # <<< GEÄNDERT except Exception as e_create_sheet: # Fange Fehler bei der Erstellung des Blattes ab und logge sie. - self.logger.critical(f"FEHLER: Konnte Blatt 'Contacts' nicht erstellen: {e_create_sheet}. Kontakt-Details koennen NICHT gespeichert werden.") + self.logger.critical(f"FEHLER: Konnte Blatt 'Contacts' nicht erstellen: {e_create_sheet}. Kontakt-Details koennen NICHT gespeichert werden.") # <<< GEÄNDERT # Logge den Traceback. - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT contacts_sheet = None # Setze contacts_sheet auf None, um spaetere Schreibversuche zu verhindern else: # Wenn SheetHandler oder Sheet-Objekt nicht verfuegbar war. - self.logger.warning("SheetHandler oder Sheet-Objekt nicht verfuegbar. Kann Blatt 'Contacts' nicht oeffnen/erstellen. Kontakt-Details werden NICHT gespeichert.") + self.logger.warning("SheetHandler oder Sheet-Objekt nicht verfuegbar. Kann Blatt 'Contacts' nicht oeffnen/erstellen. Kontakt-Details werden NICHT gespeichert.") # <<< GEÄNDERT contacts_sheet = None # Sicherstellen, dass contacts_sheet None ist @@ -8481,7 +7018,7 @@ class DataProcessor: log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) if log_check: company_name_log = company_name[:50] + '...' if len(company_name) > 50 else company_name # Gekuerzt loggen - self.logger.debug(f"Zeile {i} ({company_name_log} Contact Check): AM leer? {processing_needed_based_on_status}, Mindestdaten gueltig? {has_min_data_for_search}. Benötigt Verarbeitung? {processing_needed_for_row}") + self.logger.debug(f"Zeile {i} ({company_name_log} Contact Check): AM leer? {processing_needed_based_on_status}, Mindestdaten gueltig? {has_min_data_for_search}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist @@ -8496,11 +7033,11 @@ class DataProcessor: # Pruefe das Limit fuer verarbeitete Zeilen if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_contact_search erreicht. Breche weitere Zeilenpruefung ab.") + self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_contact_search erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT break # Brich die Schleife ab - self.logger.info(f"Zeile {i}: Suche LinkedIn Kontakte fuer '{crm_kurzform[:50]}...' ({website[:50]}...)...") # Gekuerzt loggen + self.logger.info(f"Zeile {i}: Suche LinkedIn Kontakte fuer '{crm_kurzform[:50]}...' ({website[:50]}...)...") # <<< GEÄNDERT all_found_contacts_for_row = [] # Liste zum Sammeln aller gefundenen Kontakte fuer DIESE Zeile (Liste von Dicts) @@ -8516,7 +7053,7 @@ class DataProcessor: found_contacts_in_category = {} # Dictionary zum Sammeln eindeutiger Kontakte {linkedin_url: contact_data} fuer diese Kategorie for position_query in queries: - self.logger.debug(f" -> Suche nach Position: '{position_query}' bei '{crm_kurzform[:50]}'...") # Gekuerzt loggen + self.logger.debug(f" -> Suche nach Position: '{position_query}' bei '{crm_kurzform[:50]}'...") # <<< GEÄNDERT try: # Rufe die globale Funktion search_linkedin_contacts auf (Block 10). # Limitieren Sie die Anzahl der SerpAPI Ergebnisse pro Query, um Kosten zu managen. @@ -8543,7 +7080,7 @@ class DataProcessor: except Exception as e_linkedin_search: # Wenn search_linkedin_contacts eine Exception wirft (nach Retries) # Der Fehler wird bereits vom retry_on_failure Decorator oder search_linkedin_contacts geloggt. - self.logger.error(f"FEHLER bei search_linkedin_contacts fuer Zeile {i} (Query: '{position_query}', Firma: '{crm_kurzform[:50]}...'): {e_linkedin_search}") # Gekuerzt loggen + self.logger.error(f"FEHLER bei search_linkedin_contacts fuer Zeile {i} (Query: '{position_query}', Firma: '{crm_kurzform[:50]}...'): {e_linkedin_search}") # <<< GEÄNDERT pass # Faert fort mit der naechsten Query oder Kategorie # Pause nach jeder SerpAPI Suche (pro position_query) @@ -8576,7 +7113,7 @@ class DataProcessor: # Sammeln Sie diese Updates fuer das Hauptblatt in der globalen Liste all_sheet_updates. all_sheet_updates.extend(main_sheet_updates_for_row) - self.logger.info(f"Zeile {i}: Kontaktzahlen gesammelt: {contact_counts_for_row} – Timestamp AM vorgemerkt fuer Update.") + self.logger.info(f"Zeile {i}: Kontaktzahlen gesammelt: {contact_counts_for_row} – Timestamp AM vorgemerkt fuer Update.") # <<< GEÄNDERT # Bereiten Sie die Zeilen fuer das 'Contacts' Blatt vor (falls es existiert). @@ -8618,9 +7155,9 @@ class DataProcessor: if rows_to_append_to_contacts_sheet: # Fuegen Sie diese Zeilen zur globalen Liste aller Kontakte hinzu, die spaeter angefuegt werden. all_contact_rows_to_append.extend(rows_to_append_to_contacts_sheet) - self.logger.debug(f" -> {len(rows_to_append_to_contacts_sheet)} eindeutige Kontakte fuer Zeile {i} zum Anfuegen an 'Contacts' vorgemerkt.") + self.logger.debug(f" -> {len(rows_to_append_to_contacts_sheet)} eindeutige Kontakte fuer Zeile {i} zum Anfuegen an 'Contacts' vorgemerkt.") # <<< GEÄNDERT else: - self.logger.debug(f" -> Keine neuen Kontakte fuer Zeile {i} gefunden.") + self.logger.debug(f" -> Keine neuen Kontakte fuer Zeile {i} gefunden.") # <<< GEÄNDERT # Sende gesammelte Sheet Updates (Hauptblatt) wenn das Update-Batch-Limit erreicht ist. @@ -8629,12 +7166,12 @@ class DataProcessor: rows_in_main_sheet_update_batch = len(all_sheet_updates) // 5 if rows_in_main_sheet_update_batch >= update_batch_row_limit: - self.logger.debug(f" Sende gesammelte Hauptblatt-Updates ({rows_in_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") + self.logger.debug(f" Sende gesammelte Hauptblatt-Updates ({rows_in_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. # Wenn es fehlschlaegt, wird es intern geloggt. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f" Hauptblatt-Update fuer {rows_in_main_sheet_update_batch} Zeilen erfolgreich.") + self.logger.info(f" Hauptblatt-Update fuer {rows_in_main_sheet_update_batch} Zeilen erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Leere die gesammelten Updates nach dem Senden. @@ -8645,7 +7182,7 @@ class DataProcessor: # Dieser Modus ist API-intensiv und sollte langsamer laufen. # Nutzt Config.RETRY_DELAY (Block 1). pause_duration = getattr(Config, 'RETRY_DELAY', 10) * 0.8 # Laengere Pause, z.B. 80% der Retry-Wartezeit - self.logger.debug(f"Warte {pause_duration:.2f}s nach Verarbeitung von Zeile {i}...") + self.logger.debug(f"Warte {pause_duration:.2f}s nach Verarbeitung von Zeile {i}...") # <<< GEÄNDERT time.sleep(pause_duration) @@ -8653,18 +7190,18 @@ class DataProcessor: # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update. if all_sheet_updates: rows_in_final_main_sheet_update_batch = len(all_sheet_updates) // 5 - self.logger.info(f"Sende FINALE gesammelte Hauptblatt-Updates ({rows_in_final_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") + self.logger.info(f"Sende FINALE gesammelte Hauptblatt-Updates ({rows_in_final_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f"FINALES Hauptblatt-Update erfolgreich.") + self.logger.info(f"FINALES Hauptblatt-Update erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # --- Finale Kontakte-Zeilen (Contacts Sheet) anfuegen --- # Fuege alle gesammelten Kontaktzeilen auf einmal ans Ende des 'Contacts' Blattes an. if contacts_sheet and all_contact_rows_to_append: - self.logger.info(f"Fuege {len(all_contact_rows_to_append)} gesammelte Kontaktzeilen an Blatt 'Contacts' an...") + self.logger.info(f"Fuege {len(all_contact_rows_to_append)} gesammelte Kontaktzeilen an Blatt 'Contacts' an...") # <<< GEÄNDERT try: # append_rows ist effizienter als batch_update fuer viele neue Zeilen am Ende. # Die gspread.Worksheet.append_rows Methode kann Exceptions werfen (z.B. APIError), @@ -8675,17 +7212,17 @@ class DataProcessor: # es mit @retry_on_failure dekorieren (falls gspread es unterstuetzt). # Fuer jetzt, fangen wir die Exception hier. contacts_sheet.append_rows(all_contact_rows_to_append, value_input_option='USER_ENTERED') # Standard Option - self.logger.info(f"Anfuegen von {len(all_contact_rows_to_append)} Kontaktzeilen erfolgreich.") + self.logger.info(f"Anfuegen von {len(all_contact_rows_to_append)} Kontaktzeilen erfolgreich.") # <<< GEÄNDERT except Exception as e_append: # Fange Fehler beim Anfuegen der Zeilen ab und logge sie. - self.logger.error(f"FEHLER beim Anfuegen von Kontaktzeilen an Blatt 'Contacts': {type(e_append).__name__} - {e_append}") + self.logger.error(f"FEHLER beim Anfuegen von Kontaktzeilen an Blatt 'Contacts': {type(e_append).__name__} - {e_append}") # <<< GEÄNDERT # Logge den Traceback. - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT pass # Faert fort, der Rest des Skripts sollte nicht blockiert werden # Logge den Abschluss des Modus - self.logger.info(f"Modus 'contact_search' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") + self.logger.info(f"Modus 'contact_search' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. # ============================================================================== @@ -8700,8 +7237,8 @@ class DataProcessor: # Diese Methode wird in _process_single_row (Block 21) aufgerufen, wenn der ML-Schritt angefordert ist und noetig ist. # Sie fuehrt eine Vorhersage des Servicetechniker-Buckets fuer eine einzelne Zeile mit dem trainierten ML-Modell durch. # Sie nutzt das geladene Modell und den Imputer (Attribute der DataProcessor Instanz). - # Nutzt interne Helfer: _get_cell_value_safe, _load_ml_model. - # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, pandas, numpy, re, clean_text (Block 4), get_valid_numeric (Block 5). + # Nutzt interne Helfer: _get_cell_value_safe, _load_ml_model (denselben Block). + # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, pandas, numpy, re, clean_text (Block 4), get_numeric_filter_value (Block 5). def _predict_technician_bucket(self, row_data): """ Fuehrt eine Vorhersage des Servicetechniker-Buckets fuer eine einzelne Zeile @@ -8716,12 +7253,12 @@ class DataProcessor: # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge den Start der ML-Schaetzung fuer diese Zeile company_name = self._get_cell_value_safe(row_data, 'CRM Name').strip() # Block 1 Column Map - self.logger.debug(f"Versuche ML-Schaetzung fuer Zeile ({company_name[:50]}...)") # Gekuerzt loggen + self.logger.debug(f"Versuche ML-Schaetzung fuer Zeile ({company_name[:50]}...)") # <<< GEÄNDERT # Laden Sie das Modell, den Imputer und die erwarteten Feature-Spalten, falls noch nicht geschehen. - # Diese werden als Attribute der DataProcessor Instanz gespeichert (_load_ml_model Block 31). + # Diese werden als Attribute der DataProcessor Instanz gespeichert (_load_ml_model denselben Block). if self.model is None or self.imputer is None or self._expected_features is None: - self.logger.info("Lade ML-Modell, Imputer und Feature-Spalten...") + self.logger.info("Lade ML-Modell, Imputer und Feature-Spalten...") # <<< GEÄNDERT try: # Der Aufruf von _load_ml_model (denselben Block) ist nicht mit retry_on_failure dekoriert, # da das Laden lokaler Dateien nicht wiederholt werden muss. Fehler deuten auf ein permanentes Problem hin. @@ -8729,23 +7266,23 @@ class DataProcessor: # Pruefe erneut, ob das Laden erfolgreich war. if self.model is None or self.imputer is None or self._expected_features is None: - self.logger.error("Laden von Modell, Imputer oder Feature-Spalten fehlgeschlagen. Kann ML-Schaetzung nicht durchfuehren.") + self.logger.error("Laden von Modell, Imputer oder Feature-Spalten fehlgeschlagen. Kann ML-Schaetzung nicht durchfuehren.") # <<< GEÄNDERT return "FEHLER Schaetzung" # Gebe Fehlerwert zurueck, wenn Laden fehlschlug - self.logger.info("ML-Modell, Imputer und Feature-Spalten erfolgreich geladen.") + self.logger.info("ML-Modell, Imputer und Feature-Spalten erfolgreich geladen.") # <<< GEÄNDERT except Exception as e: # Fange Fehler beim Laden ab und logge sie. - self.logger.error(f"FEHLER beim Laden von ML-Modell/Imputer/Feature-Spalten: {e}") + self.logger.error(f"FEHLER beim Laden von ML-Modell/Imputer/Feature-Spalten: {e}") # <<< GEÄNDERT # Logge den Traceback. - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT # Geben Sie einen Fehlerwert zurueck. return f"FEHLER Laden: {str(e)[:100]}..." # Signalisiert Ladefehler (gekuerzt) # --- Bereiten Sie die Daten fuer DIESE EINE ZEILE fuer die Vorhersage vor --- try: - # Diese Logik ist aehnlich wie in prepare_data_for_modeling (Block 31), + # Diese Logik ist aehnlich wie in prepare_data_for_modeling (denselben Block), # aber nur fuer eine einzelne Zeile und muss mit den exakt gleichen # Spaltennamen, Normalisierungs- und Encoding-Schritten arbeiten wie das Training. @@ -8766,32 +7303,38 @@ class DataProcessor: # --- Konsolidieren Umsatz/Mitarbeiter (Wiki > CRM) --- - # Nutzt globale Funktion get_valid_numeric (Block 5) fuer die Konvertierung. - # Diese Funktion gibt numerische Werte (Float/Int) oder NaN zurueck. + # Nutzt globale Funktion get_numeric_filter_value (Block 5) - ERSETZT get_valid_numeric + # Diese Funktion gibt numerische Werte (Float/Int) oder 0/NaN zurueck. # Stellen Sie sicher, dass die Spalten existieren, bevor apply aufgerufen wird. # Diese Spalten sollten aus row_values extrahiert worden sein, wenn COLUMN_MAP korrekt ist. - crm_umsatz_series = df_single_row['CRM Umsatz'].apply(get_valid_numeric) if 'CRM Umsatz' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index) - wiki_umsatz_series = df_single_row['Wiki Umsatz'].apply(get_valid_numeric) if 'Wiki Umsatz' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index) - crm_ma_series = df_single_row['CRM Anzahl Mitarbeiter'].apply(get_valid_numeric) if 'CRM Anzahl Mitarbeiter' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index) - wiki_ma_series = df_single_row['Wiki Mitarbeiter'].apply(get_valid_numeric).astype(float) if 'Wiki Mitarbeiter' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index) # Muss Float sein wie andere numerische + crm_umsatz_series = df_single_row['CRM Umsatz'].apply(lambda x: get_numeric_filter_value(x, is_umsatz=True)) if 'CRM Umsatz' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index) # KORRIGIERT: Lambda hinzugefügt + wiki_umsatz_series = df_single_row['Wiki Umsatz'].apply(lambda x: get_numeric_filter_value(x, is_umsatz=True)) if 'Wiki Umsatz' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index) # KORRIGIERT: Lambda hinzugefügt + crm_ma_series = df_single_row['CRM Anzahl Mitarbeiter'].apply(lambda x: get_numeric_filter_value(x, is_umsatz=False)) if 'CRM Anzahl Mitarbeiter' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index) # KORRIGIERT: Lambda hinzugefügt + wiki_ma_series = df_single_row['Wiki Mitarbeiter'].apply(lambda x: get_numeric_filter_value(x, is_umsatz=False)).astype(float) if 'Wiki Mitarbeiter' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index) # KORRIGIERT: Lambda hinzugefügt - # np.where waehlt den Wiki-Wert, wenn nicht NaN, sonst den CRM-Wert. + # np.where waehlt den Wiki-Wert, wenn er nicht 0/NaN ist, sonst den CRM-Wert. + # WICHTIG: 0 ist hier das Kennzeichen fuer ungueltig/nicht parsebar/k.A. in get_numeric_filter_value df_single_row['Finaler_Umsatz'] = np.where( - wiki_umsatz_series.notna(), + (wiki_umsatz_series.notna()) & (wiki_umsatz_series > 0), # Wenn Wiki-Wert vorhanden UND > 0 wiki_umsatz_series, crm_umsatz_series ) df_single_row['Finaler_Mitarbeiter'] = np.where( - wiki_ma_series.notna(), + (wiki_ma_series.notna()) & (wiki_ma_series > 0), # Wenn Wiki-Wert vorhanden UND > 0 wiki_ma_series, crm_ma_series ) - # Pruefen Sie, ob die konsolidierten numerischen Features NaN sind. + # Pruefen Sie, ob die konsolidierten numerischen Features NaN sind (nachdem 0 als NaN behandelt wird). + # Ersetzen Sie 0 explizit durch NaN für die Imputation, falls get_numeric_filter_value 0 zurückgibt. + df_single_row['Finaler_Umsatz'] = df_single_row['Finaler_Umsatz'].replace(0, np.nan) + df_single_row['Finaler_Mitarbeiter'] = df_single_row['Finaler_Mitarbeiter'].replace(0, np.nan) + + # ML-Vorhersage kann nicht durchgefuehrt werden, wenn diese komplett fehlen (werden vom Imputer erwartet). if pd.isna(df_single_row['Finaler_Umsatz'].iloc[0]) and pd.isna(df_single_row['Finaler_Mitarbeiter'].iloc[0]): - self.logger.debug(f" -> ML-Schaetzung uebersprungen: Konsolidierter Umsatz und Mitarbeiter fehlen fuer Zeile.") + self.logger.debug(f" -> ML-Schaetzung uebersprungen: Konsolidierter Umsatz und Mitarbeiter fehlen fuer Zeile.") # <<< GEÄNDERT return "k.A. (Daten fehlen)" # Gebe spezifischen Wert zurueck @@ -8799,7 +7342,7 @@ class DataProcessor: branche_col_name = "CRM Branche" # Original Header Name aus COLUMN_MAP (Block 1) # Stellen Sie sicher, dass die Spalte existiert und ein String ist. Fuellen Sie NaNs mit 'Unbekannt'. if branche_col_name not in df_single_row.columns: - self.logger.warning(f"Spalte '{branche_col_name}' nicht im DataFrame fuer ML-Vorhersage gefunden. Behandle als 'Unbekannt'.") + self.logger.warning(f"Spalte '{branche_col_name}' nicht im DataFrame fuer ML-Vorhersage gefunden. Behandle als 'Unbekannt'.") # <<< GEÄNDERT df_single_row[branche_col_name] = 'Unbekannt' # Setze einen Default-Wert df_single_row[branche_col_name] = df_single_row[branche_col_name].astype(str).fillna('Unbekannt').str.strip() @@ -8816,7 +7359,7 @@ class DataProcessor: # Stellen Sie die Reihenfolge der Spalten sicher, so wie sie im Training waren (self._expected_features). # self._expected_features wird von _load_ml_model (denselben Block) geladen. if self._expected_features is None: - self.logger.error("FEHLER: Erwartete Feature-Spalten fuer ML-Vorhersage nicht geladen. Kann nicht vorhersagen.") + self.logger.error("FEHLER: Erwartete Feature-Spalten fuer ML-Vorhersage nicht geladen. Kann nicht vorhersagen.") # <<< GEÄNDERT return "FEHLER Schaetzung" # Gebe Fehlerwert zurueck # Erstellen Sie einen neuen DataFrame mit allen erwarteten Features und fuellen Sie fehlende mit 0. @@ -8840,7 +7383,7 @@ class DataProcessor: # Muss konsistent mit dem Imputer aus dem Training sein. # Der Imputer (self.imputer) wird auf die vorbereiteten Features angewendet. if self.imputer is None: - self.logger.error("FEHLER: ML-Imputer ist nicht geladen. Kann nicht imputieren/vorhersagen.") + self.logger.error("FEHLER: ML-Imputer ist nicht geladen. Kann nicht imputieren/vorhersagen.") # <<< GEÄNDERT return "FEHLER Schaetzung" # Gebe Fehlerwert zurueck # Imputer.transform gibt ein Numpy Array zurueck. @@ -8850,13 +7393,13 @@ class DataProcessor: # Optional: Pruefen Sie, ob nach Imputation NaNs verbleiben (sollte nicht passieren bei SimpleImputer) # if df_imputed.isna().any().any(): - # self.logger.warning("WARNUNG: NaNs verbleiben nach Imputation.") + # self.logger.warning("WARNUNG: NaNs verbleiben nach Imputation.") # <<< GEÄNDERT # --- Vorhersage --- # Das Decision Tree Modell (self.model) erwartet die vorbereiteten und imputierten Features. if not self.model: - self.logger.error("FEHLER: ML-Modell ist nicht geladen. Kann nicht vorhersagen.") + self.logger.error("FEHLER: ML-Modell ist nicht geladen. Kann nicht vorhersagen.") # <<< GEÄNDERT return "FEHLER Schaetzung" # Gebe Fehlerwert zurueck @@ -8874,14 +7417,14 @@ class DataProcessor: predicted_bucket_label = model_classes[predicted_class_index] # Logge die Vorhersage auf Debug-Level - self.logger.debug(f" -> ML Vorhersage Ergebnis: '{predicted_bucket_label}' (Wahrscheinlichkeiten: {prediction_proba[0]})") + self.logger.debug(f" -> ML Vorhersage Ergebnis: '{predicted_bucket_label}' (Wahrscheinlichkeiten: {prediction_proba[0]})") # <<< GEÄNDERT return predicted_bucket_label # Gebe das vorhergesagte Bucket-Label zurueck (String) except Exception as e: # Fange alle unerwarteten Fehler ab, die waehrend der Datenvorbereitung oder Vorhersage auftreten. - self.logger.exception(f"FEHLER bei der Datenvorbereitung/Vorhersage fuer Zeile (ML): {e}") # Logge Fehler und Traceback + self.logger.exception(f"FEHLER bei der Datenvorbereitung/Vorhersage fuer Zeile (ML): {e}") # <<< GEÄNDERT # Geben Sie einen Fehlerwert zurueck, der im Sheet gespeichert werden kann. return f"FEHLER Schaetzung: {str(e)[:100]}..." # Signalisiert Fehler bei der Schaetzung (gekuerzt) @@ -8910,27 +7453,27 @@ class DataProcessor: try: # Pruefen Sie, ob die Modelldateien existieren if not os.path.exists(model_path): - self.logger.error(f"ML-Modell Datei nicht gefunden: {model_path}") + self.logger.error(f"ML-Modell Datei nicht gefunden: {model_path}") # <<< GEÄNDERT return # Beende die Methode, wenn die Datei fehlt if not os.path.exists(imputer_path): - self.logger.error(f"Imputer Datei nicht gefunden: {imputer_path}") + self.logger.error(f"Imputer Datei nicht gefunden: {imputer_path}") # <<< GEÄNDERT return # Beende die Methode, wenn die Datei fehlt # Laden Sie das serialisierte Modell with open(model_path, 'rb') as f: self.model = pickle.load(f) - self.logger.info(f"ML-Modell '{model_path}' erfolgreich geladen.") + self.logger.info(f"ML-Modell '{model_path}' erfolgreich geladen.") # <<< GEÄNDERT # Loggen Sie die Klassen-Labels des geladenen Modells zur Info if hasattr(self.model, 'classes_'): - self.logger.debug(f"Geladene Modell-Klassen: {self.model.classes_}") + self.logger.debug(f"Geladene Modell-Klassen: {self.model.classes_}") # <<< GEÄNDERT else: - self.logger.debug("Geladenes Modell hat kein 'classes_' Attribut.") + self.logger.debug("Geladenes Modell hat kein 'classes_' Attribut.") # <<< GEÄNDERT # Laden Sie den serialisierten Imputer with open(imputer_path, 'rb') as f: self.imputer = pickle.load(f) - self.logger.info(f"Imputer '{imputer_path}' erfolgreich geladen.") + self.logger.info(f"Imputer '{imputer_path}' erfolgreich geladen.") # <<< GEÄNDERT # Laden Sie die Liste der erwarteten Feature-Spalten (JSON-Datei wird empfohlen) @@ -8946,24 +7489,24 @@ class DataProcessor: self._expected_features = data.get("feature_columns") # Pruefen Sie, ob die geladenen Daten eine nicht-leere Liste sind. if self._expected_features and isinstance(self._expected_features, list): - self.logger.info(f"Erwartete Feature-Spalten ({len(self._expected_features)}) aus '{expected_features_path}' geladen.") + self.logger.info(f"Erwartete Feature-Spalten ({len(self._expected_features)}) aus '{expected_features_path}' geladen.") # <<< GEÄNDERT # Loggen Sie die ersten paar erwarteten Features auf Debug # self.logger.debug(f"Erwartete Features (erste 5): {self._expected_features[:5]}...") # Zu viel Laerm im Debug else: # Wenn die geladenen Daten nicht das erwartete Format haben oder leer sind - self.logger.error(f"Formatfehler in '{expected_features_path}' oder Schluessel 'feature_columns' fehlt/ist leer. ML-Vorhersage koennte fehlschlagen.") + self.logger.error(f"Formatfehler in '{expected_features_path}' oder Schluessel 'feature_columns' fehlt/ist leer. ML-Vorhersage koennte fehlschlagen.") # <<< GEÄNDERT self._expected_features = None # Setze auf None bei Fehler except Exception as e_json: # Fangen Sie Fehler beim Laden oder Parsen der JSON-Datei ab - self.logger.error(f"FEHLER beim Laden oder Parsen der Feature-Spalten Datei '{expected_features_path}': {e_json}") + self.logger.error(f"FEHLER beim Laden oder Parsen der Feature-Spalten Datei '{expected_features_path}': {e_json}") # <<< GEÄNDERT # Logge den Traceback - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT self._expected_features = None # Setze auf None bei Fehler else: # Wenn die Feature-Spalten-Datei nicht gefunden wird - self.logger.warning(f"Datei mit erwarteten Feature-Spalten '{expected_features_path}' nicht gefunden. ML-Vorhersage koennte fehlschlagen.") + self.logger.warning(f"Datei mit erwarteten Feature-Spalten '{expected_features_path}' nicht gefunden. ML-Vorhersage koennte fehlschlagen.") # <<< GEÄNDERT self._expected_features = None # Setze auf None, da die Datei fehlt @@ -8973,25 +7516,25 @@ class DataProcessor: # Neuere Scikit-learn Versionen haben oft ein feature_names_in_ Attribut if hasattr(self.imputer, 'feature_names_in_') and self.imputer.feature_names_in_ is not None: self._expected_features = list(self.imputer.feature_names_in_) - self.logger.info(f"Erwartete Feature-Spalten ({len(self._expected_features)}) aus Imputer geladen (Fallback).") + self.logger.info(f"Erwartete Feature-Spalten ({len(self._expected_features)}) aus Imputer geladen (Fallback).") # <<< GEÄNDERT elif hasattr(self.model, 'feature_names_in_') and self.model.feature_names_in_ is not None: self._expected_features = list(self.model.feature_names_in_) - self.logger.info(f"Erwartete Feature-Spalten ({len(self._expected_features)}) aus Modell geladen (Fallback).") + self.logger.info(f"Erwartete Feature-Spalten ({len(self._expected_features)}) aus Modell geladen (Fallback).") # <<< GEÄNDERT else: # Wenn es nirgends gefunden werden konnte - self.logger.error("Konnte erwartete Feature-Spalten weder aus Datei noch aus Modell/Imputer extrahieren. ML-Vorhersage wird fehlschlagen.") + self.logger.error("Konnte erwartete Feature-Spalten weder aus Datei noch aus Modell/Imputer extrahieren. ML-Vorhersage wird fehlschlagen.") # <<< GEÄNDERT self._expected_features = None except Exception as e_extract: # Fange Fehler beim Extrahieren aus Modell/Imputer ab - self.logger.error(f"FEHLER beim Extrahieren der Feature-Namen aus Modell/Imputer (Fallback): {e_extract}") + self.logger.error(f"FEHLER beim Extrahieren der Feature-Namen aus Modell/Imputer (Fallback): {e_extract}") # <<< GEÄNDERT # Logge den Traceback - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT self._expected_features = None except Exception as e: # Fange alle anderen unerwarteten Fehler waehrend des Ladens ab - self.logger.exception(f"FEHLER beim Laden von ML-Artefakten: {e}") # Logge Fehler und Traceback + self.logger.exception(f"FEHLER beim Laden von ML-Artefakten: {e}") # <<< GEÄNDERT # Setzen Sie die Attribute auf None bei Fehler self.model = None self.imputer = None @@ -9005,7 +7548,7 @@ class DataProcessor: # Basierend auf prepare_data_for_modeling aus Teil 12/13. # Nutzt interne Helfer: _get_cell_value_safe. # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, pandas, numpy, re, - # clean_text (Block 4), normalize_string (Block 4), get_valid_numeric (Block 5), + # clean_text (Block 4), normalize_string (Block 4), get_numeric_filter_value (Block 5), # load_target_schema (Block 6 - relevant fuer Branchentypen), traceback. # Nutzt die uebergeordnete sheet_handler Instanz (Block 14). def prepare_data_for_modeling(self): @@ -9024,15 +7567,15 @@ class DataProcessor: oder None bei Fehlern oder wenn keine gueltigen Trainingsdaten gefunden wurden. """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - self.logger.info("Starte Datenvorbereitung fuer Modellierung (Training)...") + self.logger.info("Starte Datenvorbereitung fuer Modellierung (Training)...") # <<< GEÄNDERT # Nutzt den self.sheet_handler der Klasse (Block 15). # Pruefen Sie, ob der Sheet Handler initialisiert wurde und Daten hat. if not self.sheet_handler or not self.sheet_handler.sheet_values: - self.logger.error("Fehler: Sheet Handler nicht initialisiert oder keine Daten geladen fuer prepare_data_for_modeling.") + self.logger.error("Fehler: Sheet Handler nicht initialisiert oder keine Daten geladen fuer prepare_data_for_modeling.") # <<< GEÄNDERT # Versuchen Sie die Daten einmalig innerhalb dieser Methode zu laden, falls sie fehlen. # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). if not self.sheet_handler.load_data(): - self.logger.critical("Konnte Daten auch nach erneutem Versuch nicht laden. Abbruch der Datenvorbereitung.") + self.logger.critical("Konnte Daten auch nach erneutem Versuch nicht laden. Abbruch der Datenvorbereitung.") # <<< GEÄNDERT return None # Gebe None zurueck, wenn Laden fehlschlaegt @@ -9044,7 +7587,7 @@ class DataProcessor: min_required_rows = header_rows + 1 # Wenn nicht genuegend Zeilen da sind if not all_data or len(all_data) < min_required_rows: - self.logger.error(f"Fehler: Nicht genuegend Datenzeilen ({len(all_data)}) im Sheet gefunden fuer Modellierung (mindestens {min_required_rows} benoetigt).") + self.logger.error(f"Fehler: Nicht genuegend Datenzeilen ({len(all_data)}) im Sheet gefunden fuer Modellierung (mindestens {min_required_rows} benoetigt).") # <<< GEÄNDERT return None # Gebe None zurueck, wenn nicht genuegend Daten da sind @@ -9059,25 +7602,25 @@ class DataProcessor: # Pruefen Sie, ob die Anzahl der geladenen Spalten im Header ausreicht if len(headers) <= max_col_idx_in_map: # Logge einen kritischen Fehler, wenn das Mapping auf Spalten zeigt, die nicht im Sheet existieren - self.logger.critical(f"FEHLER: Header-Zeile ({len(headers)} Spalten) ist kuerzer als der hoechste Index in COLUMN_MAP ({max_col_idx_in_map}). COLUMN_MAP passt nicht zum Sheet.") + self.logger.critical(f"FEHLER: Header-Zeile ({len(headers)} Spalten) ist kuerzer als der hoechste Index in COLUMN_MAP ({max_col_idx_in_map}). COLUMN_MAP passt nicht zum Sheet.") # <<< GEÄNDERT return None # Beende die Methode except ValueError: # Tritt auf, wenn COLUMN_MAP leer ist - self.logger.critical("FEHLER: COLUMN_MAP scheint leer zu sein oder enthaelt keine Werte. Kann Max Index nicht ermitteln.") + self.logger.critical("FEHLER: COLUMN_MAP scheint leer zu sein oder enthaelt keine Werte. Kann Max Index nicht ermitteln.") # <<< GEÄNDERT return None # Beende die Methode except Exception as e: # Fange andere unerwartete Fehler ab - self.logger.critical(f"FEHLER beim Pruefen der Spaltenlaenge der Header-Zeile: {e}") + self.logger.critical(f"FEHLER beim Pruefen der Spaltenlaenge der Header-Zeile: {e}") # <<< GEÄNDERT return None # Beende die Methode except IndexError: # Wenn das Sheet leer ist oder keine erste Zeile hat - self.logger.critical("FEHLER: Sheet scheint leer zu sein oder hat keine erste Zeile, keine Header gefunden.") + self.logger.critical("FEHLER: Sheet scheint leer zu sein oder hat keine erste Zeile, keine Header gefunden.") # <<< GEÄNDERT return None # Beende die Methode except Exception as e: # Fange andere unerwartete Fehler beim Zugriff auf Header ab - self.logger.critical(f"FEHLER beim Zugriff auf Header: {e}") + self.logger.critical(f"FEHLER beim Zugriff auf Header: {e}") # <<< GEÄNDERT # Logge den Traceback - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT return None # Beende die Methode @@ -9086,7 +7629,7 @@ class DataProcessor: # Erstelle DataFrame aus den Datenzeilen und den Headern df = pd.DataFrame(data_rows, columns=headers) - self.logger.info(f"Initialen DataFrame fuer Modellierung erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.") + self.logger.info(f"Initialen DataFrame fuer Modellierung erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.") # <<< GEÄNDERT # --- Spaltenauswahl und Umbenennung --- # Definiere die notwendigen Spalten anhand ihrer COLUMN_MAP Schluessel (Block 1) @@ -9104,7 +7647,7 @@ class DataProcessor: # Ueberpruefe, ob alle benoetigten Spalten-Schluessel in der COLUMN_MAP (Block 1) vorhanden sind missing_keys_in_map = [key for key in col_keys_mapping.values() if key not in COLUMN_MAP] if missing_keys_in_map: - self.logger.critical(f"FEHLER: Folgende benoetigte Spalten-Schluessel fehlen in COLUMN_MAP fuer prepare_data_for_modeling: {missing_keys_in_map}.") + self.logger.critical(f"FEHLER: Folgende benoetigte Spalten-Schluessel fehlen in COLUMN_MAP fuer prepare_data_for_modeling: {missing_keys_in_map}.") # <<< GEÄNDERT return None # Beende die Methode # Erstelle das Mapping von tatsaechlichen Header-Namen zu internen Schluesseln. @@ -9130,65 +7673,66 @@ class DataProcessor: except KeyError as e: # Dieser Fehler sollte eigentlich durch die obige Pruefung abgefangen werden, # tritt aber auf, wenn ein erwarteter Header-Name nicht im geladenen DF ist (selten, wenn COLUMN_MAP korrekt ist). - self.logger.critical(f"FEHLER beim Auswaehlen/Umbenennen der Spalten (KeyError: '{e}'). Der Header wurde nicht im DataFrame gefunden.") - self.logger.debug(f"Erwartete Header: {cols_to_select_by_header}. Verfuegbare Header im DF: {list(df.columns)}") + self.logger.critical(f"FEHLER beim Auswaehlen/Umbenennen der Spalten (KeyError: '{e}'). Der Header wurde nicht im DataFrame gefunden.") # <<< GEÄNDERT + self.logger.debug(f"Erwartete Header: {cols_to_select_by_header}. Verfuegbare Header im DF: {list(df.columns)}") # <<< GEÄNDERT return None # Beende die Methode except IndexError as e: # Tritt auf, wenn COLUMN_MAP einen Index > Anzahl Spalten im DF hat - self.logger.critical(f"FEHLER beim Auswaehlen/Umbenennen der Spalten (IndexError: '{e}'). COLUMN_MAP zeigt auf Spalten, die nicht im geladenen Sheet existieren.") - self.logger.debug(f"COLUMN_MAP: {COLUMN_MAP}. Sheet hat {len(headers)} Spalten.") + self.logger.critical(f"FEHLER beim Auswaehlen/Umbenennen der Spalten (IndexError: '{e}'). COLUMN_MAP zeigt auf Spalten, die nicht im geladenen Sheet existieren.") # <<< GEÄNDERT + self.logger.debug(f"COLUMN_MAP: {COLUMN_MAP}. Sheet hat {len(headers)} Spalten.") # <<< GEÄNDERT return None # Beende die Methode except Exception as e: # Fange andere unerwartete Fehler ab - self.logger.critical(f"Unerwarteter FEHLER beim Auswaehlen/Umbenennen der Spalten: {e}") + self.logger.critical(f"Unerwarteter FEHLER beim Auswaehlen/Umbenennen der Spalten: {e}") # <<< GEÄNDERT # Logge den Traceback - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT return None # Beende die Methode - self.logger.info(f"Benötigte Spalten fuer Modellierung ausgewaehlt und umbenannt: {list(df_subset.columns)}") + self.logger.info(f"Benötigte Spalten fuer Modellierung ausgewaehlt und umbenannt: {list(df_subset.columns)}") # <<< GEÄNDERT # --- Features konsolidieren (Umsatz, Mitarbeiter) --- - # Nutzt die globale Hilfsfunktion get_valid_numeric (Block 5), die numerische Werte als Float/Int oder NaN zurueckgibt. + # Nutzt die globale Hilfsfunktion get_numeric_filter_value (Block 5) - ERSETZT get_valid_numeric cols_to_process = { 'Umsatz': ('umsatz_wiki', 'umsatz_crm', 'Finaler_Umsatz'), 'Mitarbeiter': ('ma_wiki', 'ma_crm', 'Finaler_Mitarbeiter') } for base_name, (wiki_col, crm_col, final_col) in cols_to_process.items(): - self.logger.debug(f"Verarbeite und konsolidiere '{base_name}' (Prioritaet: Wiki > CRM)...") + self.logger.debug(f"Verarbeite und konsolidiere '{base_name}' (Prioritaet: Wiki > CRM)...") # <<< GEÄNDERT # Sicherstellen, dass die Spalten im df_subset existieren, bevor apply aufgerufen wird. # Dies sollte durch die Spaltenauswahl oben garantiert sein, aber zur Sicherheit. - wiki_series = df_subset[wiki_col].apply(get_valid_numeric) if wiki_col in df_subset.columns else pd.Series(np.nan, index=df_subset.index) - crm_series = df_subset[crm_col].apply(get_valid_numeric) if crm_col in df_subset.columns else pd.Series(np.nan, index=df_subset.index) + wiki_series = df_subset[wiki_col].apply(lambda x: get_numeric_filter_value(x, is_umsatz=(base_name=='Umsatz'))) if wiki_col in df_subset.columns else pd.Series(np.nan, index=df_subset.index) # KORRIGIERT: Lambda hinzugefügt + crm_series = df_subset[crm_col].apply(lambda x: get_numeric_filter_value(x, is_umsatz=(base_name=='Umsatz'))) if crm_col in df_subset.columns else pd.Series(np.nan, index=df_subset.index) # KORRIGIERT: Lambda hinzugefügt - # np.where waehlt den Wiki-Wert, wenn er nicht NaN ist, sonst den CRM-Wert. + # np.where waehlt den Wiki-Wert, wenn er nicht 0/NaN ist, sonst den CRM-Wert. + # WICHTIG: 0 ist hier das Kennzeichen fuer ungueltig/nicht parsebar/k.A. in get_numeric_filter_value df_subset[final_col] = np.where( - wiki_series.notna(), # Wenn Wiki-Wert vorhanden ist (nicht NaN) + (wiki_series.notna()) & (wiki_series > 0), # Wenn Wiki-Wert vorhanden UND > 0 wiki_series, # Nimm den Wiki-Wert - crm_series # Sonst nimm den CRM-Wert (der auch NaN sein kann) + crm_series # Sonst nimm den CRM-Wert (der auch 0/NaN sein kann) ) # Info-Log ueber Ergebnis - self.logger.info(f" -> {df_subset[final_col].notna().sum()} gueltige '{final_col}' Werte erstellt (von {len(df_subset)} Zeilen).") + self.logger.info(f" -> {df_subset[final_col].notna().sum()} gueltige '{final_col}' Werte erstellt (von {len(df_subset)} Zeilen).") # <<< GEÄNDERT # --- Zielvariable vorbereiten (Technikerzahl) --- techniker_col_internal = "techniker" # Interne Spaltenname nach Umbenennung (aus col_keys_mapping) - self.logger.info(f"Verarbeite Zielvariable '{techniker_col_internal}'...") + self.logger.info(f"Verarbeite Zielvariable '{techniker_col_internal}'...") # <<< GEÄNDERT # Sicherstellen, dass die Spalte existiert if techniker_col_internal not in df_subset.columns: - self.logger.critical(f"FEHLER: Zielvariable '{techniker_col_internal}' (CRM Anzahl Techniker) nicht im DataFrame gefunden nach Umbenennung.") + self.logger.critical(f"FEHLER: Zielvariable '{techniker_col_internal}' (CRM Anzahl Techniker) nicht im DataFrame gefunden nach Umbenennung.") # <<< GEÄNDERT return None # Beende die Methode - # Konvertiere zu Numerisch (Float/Int oder NaN) mit get_valid_numeric (Block 5). + # Konvertiere zu Numerisch (Float/Int oder NaN) mit get_numeric_filter_value (Block 5). # Dies stellt sicher, dass nur gueltige, positive Zahlen verwendet werden. - df_subset['Anzahl_Servicetechniker_Numeric'] = df_subset[techniker_col_internal].apply(get_valid_numeric) + df_subset['Anzahl_Servicetechniker_Numeric'] = df_subset[techniker_col_internal].apply(lambda x: get_numeric_filter_value(x, is_umsatz=False)) # KORRIGIERT: Lambda hinzugefügt # Filtere Zeilen: Behalte nur die mit gueltiger, positiver Technikerzahl (Float > 0). initial_rows = len(df_subset) - # Hier filtern wir basierend auf der numerischen Spalte, die durch get_valid_numeric erstellt wurde. + # Hier filtern wir basierend auf der numerischen Spalte, die durch get_numeric_filter_value erstellt wurde. df_filtered = df_subset[ df_subset['Anzahl_Servicetechniker_Numeric'].notna() & # Nicht NaN (df_subset['Anzahl_Servicetechniker_Numeric'] > 0) # Und groesser als 0 @@ -9198,12 +7742,12 @@ class DataProcessor: # Info, wenn Zeilen entfernt wurden if removed_rows > 0: - self.logger.info(f"{removed_rows} Zeilen entfernt aufgrund fehlender/ungueltiger Technikerzahl (Wert <= 0 oder nicht numerisch/parsebar).") - self.logger.info(f"Verbleibende Zeilen fuer Modellierungstraining (mit gueltiger Technikerzahl > 0): {filtered_rows}") + self.logger.info(f"{removed_rows} Zeilen entfernt aufgrund fehlender/ungueltiger Technikerzahl (Wert <= 0 oder nicht numerisch/parsebar).") # <<< GEÄNDERT + self.logger.info(f"Verbleibende Zeilen fuer Modellierungstraining (mit gueltiger Technikerzahl > 0): {filtered_rows}") # <<< GEÄNDERT # Wenn keine Zeilen uebrig bleiben, kann kein Modell trainiert werden. if filtered_rows == 0: - self.logger.error("FEHLER: Keine Zeilen mit gueltiger Technikerzahl (>0) uebrig fuer Modellierungstraining!") + self.logger.error("FEHLER: Keine Zeilen mit gueltiger Technikerzahl (>0) uebrig fuer Modellierungstraining!") # <<< GEÄNDERT return None # Beende die Methode @@ -9224,40 +7768,40 @@ class DataProcessor: right=True, # Intervalle sind (links, rechts]. z.B. (0, 19] inkludiert 19. include_lowest=True # Inkludiert den niedrigsten Wert der ersten Bin (-1) (relevant, falls 0 moeglich waere) ) - self.logger.info("Techniker-Buckets erstellt.") + self.logger.info("Techniker-Buckets erstellt.") # <<< GEÄNDERT # Pruefe, ob NaNs in Buckets erstellt wurden (sollte bei >0 Filterung und korrekten Bins nicht passieren). if df_filtered['Techniker_Bucket'].isna().any(): nan_bucket_rows = df_filtered['Techniker_Bucket'].isna().sum() - self.logger.warning(f"WARNUNG: {nan_bucket_rows} Zeilen mit NaNs in Techniker-Buckets nach pd.cut erstellt. Ueberpruefen Sie die bins/labels oder die Filterung.") + self.logger.warning(f"WARNUNG: {nan_bucket_rows} Zeilen mit NaNs in Techniker-Buckets nach pd.cut erstellt. Ueberpruefen Sie die bins/labels oder die Filterung.") # <<< GEÄNDERT # Entfernen Sie diese Zeilen, da sie nicht zum Trainieren verwendet werden koennen. df_filtered.dropna(subset=['Techniker_Bucket'], inplace=True) # Entferne Zeilen mit NaN im Bucket - self.logger.info(f"Nach Entfernung von {nan_bucket_rows} Zeilen mit NaN Buckets: {len(df_filtered)} Zeilen verbleiben fuer Training.") + self.logger.info(f"Nach Entfernung von {nan_bucket_rows} Zeilen mit NaN Buckets: {len(df_filtered)} Zeilen verbleiben fuer Training.") # <<< GEÄNDERT # Wenn nach Entfernung keine Zeilen mehr uebrig sind if len(df_filtered) == 0: - self.logger.error("FEHLER: Keine Zeilen uebrig nach Entfernung von NaN Buckets. Modell kann nicht trainiert werden.") + self.logger.error("FEHLER: Keine Zeilen uebrig nach Entfernung von NaN Buckets. Modell kann nicht trainiert werden.") # <<< GEÄNDERT return None # Beende die Methode # Verteilung der Buckets als Info-Log (absolute Haeufigkeit und Prozent) - self.logger.info(f"Verteilung der Techniker-Buckets im Trainingsdatensatz ({len(df_filtered)} Zeilen):\n{df_filtered['Techniker_Bucket'].value_counts(normalize=False).sort_index()}") # Zaehlung - self.logger.info(f"Verteilung (Prozent):\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).sort_index().round(3)}") # Prozent + self.logger.info(f"Verteilung der Techniker-Buckets im Trainingsdatensatz ({len(df_filtered)} Zeilen):\n{df_filtered['Techniker_Bucket'].value_counts(normalize=False).sort_index()}") # <<< GEÄNDERT + self.logger.info(f"Verteilung (Prozent):\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).sort_index().round(3)}") # <<< GEÄNDERT except Exception as e: # Fange Fehler beim Erstellen der Buckets ab - self.logger.critical(f"FEHLER beim Erstellen der Techniker-Buckets: {e}") + self.logger.critical(f"FEHLER beim Erstellen der Techniker-Buckets: {e}") # <<< GEÄNDERT # Logge den Traceback - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT return None # Beende die Methode # --- Kategoriale Features vorbereiten (Branche) --- branche_col_internal = "branche_crm" # Interne Spaltenname nach Umbenennung (aus col_keys_mapping) - self.logger.info(f"Verarbeite kategoriales Feature '{branche_col_internal}' fuer One-Hot Encoding...") + self.logger.info(f"Verarbeite kategoriales Feature '{branche_col_internal}' fuer One-Hot Encoding...") # <<< GEÄNDERT # Sicherstellen, dass die Spalte existiert if branche_col_internal not in df_filtered.columns: - self.logger.critical(f"FEHLER: Spalte '{branche_col_internal}' nicht im DataFrame fuer One-Hot Encoding gefunden.") + self.logger.critical(f"FEHLER: Spalte '{branche_col_internal}' nicht im DataFrame fuer One-Hot Encoding gefunden.") # <<< GEÄNDERT return None # Beende die Methode # Stelle sicher, dass die Spalte String ist und fülle evtl. NaNs mit 'Unbekannt'. @@ -9269,7 +7813,7 @@ class DataProcessor: # dummy_na=False, da wir NaNs bereits mit 'Unbekannt' gefuellt haben. # prefix='Branche' ist gut, um die neuen Spalten zu identifizieren. df_encoded = pd.get_dummies(df_filtered, columns=[branche_col_internal], prefix='Branche', dummy_na=False) - self.logger.info(f"One-Hot Encoding fuer '{branche_col_internal}' durchgefuehrt. Neue Spaltenanzahl: {len(df_encoded.columns)}") + self.logger.info(f"One-Hot Encoding fuer '{branche_col_internal}' durchgefuehrt. Neue Spaltenanzahl: {len(df_encoded.columns)}") # <<< GEÄNDERT # --- Finale Auswahl der Features fuer das Modell --- @@ -9283,7 +7827,7 @@ class DataProcessor: # Pruefen Sie, ob die konsolidierten numerischen Spalten ('Finaler_Umsatz', 'Finaler_Mitarbeiter') # tatsaechlich im DataFrame df_encoded vorhanden sind (sollten sie, wurden oben erstellt). if not all(col in df_encoded.columns for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter']): - self.logger.critical("FEHLER: Konsolidierte numerische Spalten 'Finaler_Umsatz' oder 'Finaler_Mitarbeiter' fehlen im DataFrame nach Konsolidierung.") + self.logger.critical("FEHLER: Konsolidierte numerische Spalten 'Finaler_Umsatz' oder 'Finaler_Mitarbeiter' fehlen im DataFrame nach Konsolidierung.") # <<< GEÄNDERT return None # Beende die Methode @@ -9296,7 +7840,7 @@ class DataProcessor: identification_cols = ['name', 'Anzahl_Servicetechniker_Numeric'] # Sicherstellen, dass diese Identifikationsspalten auch im DataFrame existieren. if not all(col in df_encoded.columns for col in identification_cols): - self.logger.critical(f"FEHLER: Identifikationsspalten {identification_cols} fehlen im DataFrame.") + self.logger.critical(f"FEHLER: Identifikationsspalten {identification_cols} fehlen im DataFrame.") # <<< GEÄNDERT return None # Beende die Methode @@ -9306,7 +7850,7 @@ class DataProcessor: final_cols_for_df = identification_cols + feature_columns + [target_column] missing_final_cols = [col for col in final_cols_for_df if col not in df_encoded.columns] if missing_final_cols: - self.logger.critical(f"FEHLER: Finale Spalten fuer Modellierung fehlen im DataFrame: {missing_final_cols}") + self.logger.critical(f"FEHLER: Finale Spalten fuer Modellierung fehlen im DataFrame: {missing_final_cols}") # <<< GEÄNDERT return None # Beende die Methode @@ -9328,21 +7872,21 @@ class DataProcessor: # Logge Informationen zum finalen DataFrame - self.logger.info("Datenvorbereitung fuer Modellierung (Training) abgeschlossen.") - self.logger.info(f"Finaler DataFrame fuer Modellierung hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten.") + self.logger.info("Datenvorbereitung fuer Modellierung (Training) abgeschlossen.") # <<< GEÄNDERT + self.logger.info(f"Finaler DataFrame fuer Modellierung hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten.") # <<< GEÄNDERT # Logge die Anzahl der Feature-Spalten, nicht die Liste selbst (kann sehr lang sein). - self.logger.info(f"Anzahl Feature-Spalten: {len(feature_columns)}") - self.logger.info(f"Ziel-Spalte: {target_column}") + self.logger.info(f"Anzahl Feature-Spalten: {len(feature_columns)}") # <<< GEÄNDERT + self.logger.info(f"Ziel-Spalte: {target_column}") # <<< GEÄNDERT # WICHTIG: Info ueber fehlende Werte in den finalen numerischen Features VOR der Imputation. # Die Imputation selbst erfolgt im Trainingsschritt (train_technician_model Block 31). numeric_features_for_imputation = ['Finaler_Umsatz', 'Finaler_Mitarbeiter'] nan_counts = df_model_ready[numeric_features_for_imputation].isna().sum() - self.logger.info(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}") + self.logger.info(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}") # <<< GEÄNDERT # Logge auch, wie viele Zeilen *mindestens* einen NaN in den numerischen Features haben. rows_with_nan = df_model_ready[numeric_features_for_imputation].isna().any(axis=1).sum() - self.logger.info(f"Anzahl Zeilen mit mindestens einem fehlenden numerischen Feature (vor Imputation): {rows_with_nan}") + self.logger.info(f"Anzahl Zeilen mit mindestens einem fehlenden numerischen Feature (vor Imputation): {rows_with_nan}") # <<< GEÄNDERT return df_model_ready # Gebe den vorbereiteten DataFrame zurueck @@ -9365,14 +7909,14 @@ class DataProcessor: patterns_out (str): Dateipfad zum Speichern der Feature-Spaltenliste (.json). """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - self.logger.info("Starte Training des Servicetechniker Decision Tree Modells...") + self.logger.info("Starte Training des Servicetechniker Decision Tree Modells...") # <<< GEÄNDERT # 1. Daten vorbereiten (nutzt die interne Methode prepare_data_for_modeling denselben Block) df_model_ready = self.prepare_data_for_modeling() # Wenn die Datenvorbereitung fehlschlug oder keinen DataFrame zurueckgab if df_model_ready is None or df_model_ready.empty: - self.logger.error("Datenvorbereitung fuer Modelltraining fehlgeschlagen oder keine Daten. Training abgebrochen.") + self.logger.error("Datenvorbereitung fuer Modelltraining fehlgeschlagen oder keine Daten. Training abgebrochen.") # <<< GEÄNDERT return # Beende die Methode @@ -9386,7 +7930,7 @@ class DataProcessor: feature_columns = [col for col in df_model_ready.columns if col not in identification_cols and col != target_column] # Stellen Sie sicher, dass es Feature-Spalten gibt (sollte durch prepare_data_for_modeling sichergestellt sein) if not feature_columns: - self.logger.critical("FEHLER: Keine Feature-Spalten nach Datenvorbereitung gefunden. Training nicht moeglich.") + self.logger.critical("FEHLER: Keine Feature-Spalten nach Datenvorbereitung gefunden. Training nicht moeglich.") # <<< GEÄNDERT return # Beende die Methode # Erstellen Sie die Feature-Matrix X und den Zielvektor y @@ -9394,7 +7938,7 @@ class DataProcessor: y = df_model_ready[target_column] - self.logger.info(f"Daten fuer Training vorbereitet. X Shape: {X.shape}, y Shape: {y.shape}") + self.logger.info(f"Daten fuer Training vorbereitet. X Shape: {X.shape}, y Shape: {y.shape}") # <<< GEÄNDERT # Logge die ersten paar Features auf Debug-Level (kann sehr lang sein) # self.logger.debug(f"Feature Spalten fuer Training ({len(feature_columns)}): {feature_columns[:10]}...") @@ -9406,13 +7950,13 @@ class DataProcessor: X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y) - self.logger.info(f"Daten gesplittet. Train Set: {len(X_train)} Zeilen, Test Set: {len(X_test)} Zeilen.") + self.logger.info(f"Daten gesplittet. Train Set: {len(X_train)} Zeilen, Test Set: {len(X_test)} Zeilen.") # <<< GEÄNDERT # 3. Imputation (Fehlende Werte ersetzen) # Verwenden Sie SimpleImputer (z.B. Median), um NaN-Werte zu ersetzen. # Median ist robust gegenueber Ausreissern. Alternativ: 'mean' oder 'most_frequent'. imputer = SimpleImputer(strategy='median') - self.logger.info(f"Fitte Imputer mit Strategie '{imputer.strategy}' auf Trainingsdaten...") + self.logger.info(f"Fitte Imputer mit Strategie '{imputer.strategy}' auf Trainingsdaten...") # <<< GEÄNDERT # Fitten Sie den Imputer NUR auf den Trainingsdaten, um Data Leakage zu vermeiden. imputer.fit(X_train) # Fitten Sie den Imputer auf X_train @@ -9425,12 +7969,12 @@ class DataProcessor: # Speichern Sie den Imputer mit pickle with open(imputer_out, 'wb') as f: pickle.dump(imputer, f) - self.logger.info(f"Imputer erfolgreich gespeichert in '{imputer_out}'.") + self.logger.info(f"Imputer erfolgreich gespeichert in '{imputer_out}'.") # <<< GEÄNDERT except Exception as e: # Fange Fehler beim Speichern ab und logge sie. - self.logger.error(f"FEHLER beim Speichern des Imputers in '{imputer_out}': {e}") + self.logger.error(f"FEHLER beim Speichern des Imputers in '{imputer_out}': {e}") # <<< GEÄNDERT # Logge den Traceback. - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT # Fahren Sie fort, aber loggen Sie den Fehler @@ -9441,7 +7985,7 @@ class DataProcessor: # Konvertieren Sie die Ergebnisse (Numpy Arrays) zurueck zu DataFrames, behalten Sie die Spaltennamen. X_train_imputed = pd.DataFrame(X_train_imputed, columns=feature_columns) X_test_imputed = pd.DataFrame(X_test_imputed, columns=feature_columns) - self.logger.info("Numerische Features imputiert.") + self.logger.info("Numerische Features imputiert.") # <<< GEÄNDERT # 4. Decision Tree Training @@ -9457,10 +8001,10 @@ class DataProcessor: # self.logger.info(f"Beste Parameter gefunden durch GridSearchCV: {grid_search.best_params_}") - self.logger.info("Starte Training des Decision Tree Modells...") + self.logger.info("Starte Training des Decision Tree Modells...") # <<< GEÄNDERT # Fitten Sie das Modell auf den imputierten Trainingsdaten. dt_classifier.fit(X_train_imputed, y_train) - self.logger.info("Modelltraining abgeschlossen.") + self.logger.info("Modelltraining abgeschlossen.") # <<< GEÄNDERT # Speichern Sie das trainierte Modell. @@ -9471,12 +8015,12 @@ class DataProcessor: # Speichern Sie das Modell mit pickle with open(model_out, 'wb') as f: pickle.dump(dt_classifier, f) - self.logger.info(f"Decision Tree Modell erfolgreich gespeichert in '{model_out}'.") + self.logger.info(f"Decision Tree Modell erfolgreich gespeichert in '{model_out}'.") # <<< GEÄNDERT except Exception as e: # Fange Fehler beim Speichern ab und logge sie. - self.logger.error(f"FEHLER beim Speichern des Modells in '{model_out}': {e}") + self.logger.error(f"FEHLER beim Speichern des Modells in '{model_out}': {e}") # <<< GEÄNDERT # Logge den Traceback. - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT # Fahren Sie fort @@ -9491,7 +8035,7 @@ class DataProcessor: # Speichern Sie die JSON-Datei with open(patterns_out, 'w', encoding='utf-8') as f: json.dump(patterns_data, f, indent=4, ensure_ascii=False) - self.logger.info(f"Erwartete Feature-Spalten und Klassen erfolgreich gespeichert in '{patterns_out}'.") + self.logger.info(f"Erwartete Feature-Spalten und Klassen erfolgreich gespeichert in '{patterns_out}'.") # <<< GEÄNDERT # Optional: Speichern als einfache Textdatei (wie im Originalcode) # patterns_out_txt = patterns_out.replace('.json', '.txt') @@ -9501,45 +8045,45 @@ class DataProcessor: except Exception as e: # Fange Fehler beim Speichern ab und logge sie. - self.logger.error(f"FEHLER beim Speichern der Feature-Spalten in '{patterns_out}': {e}") + self.logger.error(f"FEHLER beim Speichern der Feature-Spalten in '{patterns_out}': {e}") # <<< GEÄNDERT # Logge den Traceback. - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT # Fahren Sie fort # 5. Evaluation (Optional, aber empfohlen, um die Modellleistung zu bewerten) - self.logger.info("Starte Modellevaluation...") + self.logger.info("Starte Modellevaluation...") # <<< GEÄNDERT # Vorhersagen auf dem Testset y_pred = dt_classifier.predict(X_test_imputed) # Metriken berechnen und loggen accuracy = accuracy_score(y_test, y_pred) - self.logger.info(f"Modell Genauigkeit auf dem Testset: {accuracy:.4f}") + self.logger.info(f"Modell Genauigkeit auf dem Testset: {accuracy:.4f}") # <<< GEÄNDERT # Klassifikationsbericht # zero_division='warn' ist Standard, '0' gibt 0 fuer nicht vorhandene Klassen, 'none' wirft Fehler. class_report = classification_report(y_test, y_pred, zero_division=0, labels=dt_classifier.classes_, target_names=[str(c) for c in dt_classifier.classes_]) # Stelle sicher, dass Labels und Target-Namen konsistent sind - self.logger.info(f"Klassifikationsbericht auf dem Testset:\n{class_report}") + self.logger.info(f"Klassifikationsbericht auf dem Testset:\n{class_report}") # <<< GEÄNDERT # Konfusionsmatrix # display_labels=dt_classifier.classes_ sorgt fuer korrekte Beschriftung cm = confusion_matrix(y_test, y_pred, labels=dt_classifier.classes_) - self.logger.info(f"Konfusionsmatrix auf dem Testset (Zeilen=Wahr, Spalten=Vorhersage):\n{cm}") + self.logger.info(f"Konfusionsmatrix auf dem Testset (Zeilen=Wahr, Spalten=Vorhersage):\n{cm}") # <<< GEÄNDERT # Entscheidungsregeln extrahieren (Optional, fuer Verstaendnis) try: # Beschraenken Sie die Tiefe fuer die Ausgabe, falls der Baum sehr tief ist # feature_names muessen der Reihenfolge in X_train_imputed entsprechen tree_rules = export_text(dt_classifier, feature_names=feature_columns, max_depth=7) # max_depth anpassen - self.logger.info(f"Erste Regeln des Decision Tree (max Tiefe 7):\n{tree_rules}") + self.logger.info(f"Erste Regeln des Decision Tree (max Tiefe 7):\n{tree_rules}") # <<< GEÄNDERT except Exception as e: # Fange Fehler beim Exportieren der Regeln ab - self.logger.warning(f"FEHLER beim Exportieren der Baumregeln: {e}") + self.logger.warning(f"FEHLER beim Exportieren der Baumregeln: {e}") # <<< GEÄNDERT # Logge den Traceback. - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT - self.logger.info("Modelltraining und -evaluation abgeschlossen.") + self.logger.info("Modelltraining und -evaluation abgeschlossen.") # <<< GEÄNDERT # ============================================================================== # Ende DataProcessor Klasse Utility: ML Prep & Training Block @@ -9569,8 +8113,8 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge den Start des Modus auf Warning, da es experimentell ist. - self.logger.warning(f"Starte Modus (EXPERIMENTELL): Website Detail Extraction fuer Zeilen mit 'x' in Spalte A. Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") - self.logger.warning("Hinweis: Dieser Modus nutzt die globale Funktion 'scrape_website_details' (Block 13), deren Implementierung je nach Zielwebsites angepasst werden muss.") + self.logger.warning(f"Starte Modus (EXPERIMENTELL): Website Detail Extraction fuer Zeilen mit 'x' in Spalte A. Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT + self.logger.warning("Hinweis: Dieser Modus nutzt die globale Funktion 'scrape_website_details' (Block 13), deren Implementierung je nach Zielwebsites angepasst werden muss.") # <<< GEÄNDERT # --- Daten laden --- @@ -9578,7 +8122,7 @@ class DataProcessor: # da wir explizit nach dem 'x'-Flag suchen. # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer Website Details Extraction.") + self.logger.error("Fehler beim Laden der Daten fuer Website Details Extraction.") # <<< GEÄNDERT return # Beende die Methode, wenn das Laden fehlschlaegt @@ -9597,24 +8141,24 @@ class DataProcessor: # Logge den Suchbereich fuer das 'x'-Flag - self.logger.info(f"Suchbereich fuer 'x'-Flag: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") + self.logger.info(f"Suchbereich fuer 'x'-Flag: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT # Pruefe, ob der Bereich gueltig ist if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") + self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT return # Beende die Methode, wenn der Bereich leer ist # --- Indizes und Buchstaben --- # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind - required_keys = ["ReEval Flag", "CRM Website"] # A, D + required_keys = ["ReEval Flag", "CRM Website", "CRM Name"] # A, D, B # Erstellen Sie ein Dictionary mit Schluesseln und Indizes col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_website_details: {missing}. Breche ab.") + self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_website_details: {missing}. Breche ab.") # <<< GEÄNDERT return # Beende die Methode bei kritischem Fehler # Ermitteln Sie die Indizes @@ -9631,12 +8175,12 @@ class DataProcessor: details_col_key_for_logging = "Website Rohtext" # Pruefen Sie, ob der Fallback-Schluessel gefunden wurde if details_col_idx is None: - self.logger.critical("FEHLER: Weder 'Website Details' noch 'Website Rohtext' Spaltenindex in COLUMN_MAP gefunden.") + self.logger.critical("FEHLER: Weder 'Website Details' noch 'Website Rohtext' Spaltenindex in COLUMN_MAP gefunden.") # <<< GEÄNDERT return # Beende die Methode bei kritischem Fehler - self.logger.warning(f"Keine Spalte 'Website Details' in COLUMN_MAP, nutze '{details_col_key_for_logging}' ({self.sheet_handler._get_col_letter(details_col_idx+1)}) als Fallback.") # Logge Warnung (Block 14 _get_col_letter) + self.logger.warning(f"Keine Spalte 'Website Details' in COLUMN_MAP, nutze '{details_col_key_for_logging}' ({self.sheet_handler._get_col_letter(details_col_idx+1)}) als Fallback.") # <<< GEÄNDERT else: # Logge die Verwendung der dedizierten Spalte - self.logger.info(f"Nutze Spalte '{details_col_key_for_logging}' ({self.sheet_handler._get_col_letter(details_col_idx+1)}) fuer Website Details.") # Logge Info (Block 14 _get_col_letter) + self.logger.info(f"Nutze Spalte '{details_col_key_for_logging}' ({self.sheet_handler._get_col_letter(details_col_idx+1)}) fuer Website Details.") # <<< GEÄNDERT # Ermitteln Sie den Spaltenbuchstaben der Zielspalte (nutzt interne Helfer _get_col_letter Block 14) @@ -9701,7 +8245,7 @@ class DataProcessor: log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) if log_check: company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Details Check): A='x'? {is_marked_for_processing}, D gueltig? {website_url_is_valid_looking}. Benoetigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen + self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Details Check): A='x'? {is_marked_for_processing}, D gueltig? {website_url_is_valid_looking}. Benoetigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist (trotz 'x' fehlte die URL) @@ -9718,11 +8262,11 @@ class DataProcessor: # Pruefe das Limit fuer verarbeitete Zeilen if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_website_details erreicht. Breche weitere Zeilenpruefung ab.") + self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_website_details erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT break # Brich die Schleife ab - selflogger.info(f"Zeile {i}: Extrahiere Website Details von {website_url[:100]}...") # Logge Start (gekuerzt) + self.logger.info(f"Zeile {i}: Extrahiere Website Details von {website_url[:100]}...") # <<< GEÄNDERT (war selflogger) details = "FEHLER: Funktion 'scrape_website_details' nicht verfuegbar" # Default Fehler, falls die Funktion nicht existiert (Sollte nicht passieren, wenn Block 13 korrekt ist) @@ -9746,14 +8290,14 @@ class DataProcessor: except NameError: # Dieser Fehler sollte nicht auftreten, wenn scrape_website_details in Block 13 ist. - self.logger.critical("FEHLER: Funktion 'scrape_website_details' ist nicht definiert! Kann Details nicht extrahieren.") + self.logger.critical("FEHLER: Funktion 'scrape_website_details' ist nicht definiert! Kann Details nicht extrahieren.") # <<< GEÄNDERT # Logge den Traceback. - self.logger.debug(traceback.format_exc()) + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT details = "FEHLER: Funktion nicht definiert" # Setze spezifischen Fehlerwert except Exception as e_detail: # Fange andere unerwartete Fehler ab, die nicht von scrape_website_details behandelt wurden. - self.logger.exception(f"Unerwarteter Fehler bei scrape_website_details fuer {website_url[:100]}...: {type(e_detail).__name__} - {e_detail}") # Logge Fehler (gekuerzt) und Traceback + self.logger.exception(f"Unerwarteter Fehler bei scrape_website_details fuer {website_url[:100]}...: {type(e_detail).__name__} - {e_detail}") # <<< GEÄNDERT details = f"k.A. (Unerwarteter Fehler: {str(e_detail)[:100]}...)" # Signalisiert Fehler (gekuerzt) @@ -9761,7 +8305,7 @@ class DataProcessor: # Stellen Sie sicher, dass der Wert ein String ist. updates_for_row = [] # Lokale Liste fuer Updates dieser Zeile updates_for_row.append({'range': f'{details_col_letter}{i}', 'values': [[str(details)]]}) # Block 1 Column Map - self.logger.debug(f"Zeile {i}: Details extrahiert und zum Update fuer Spalte {details_col_key_for_logging} ({details_col_letter}{i}) hinzugefuegt.") # Gekuerzt loggen + self.logger.debug(f"Zeile {i}: Details extrahiert und zum Update fuer Spalte {details_col_key_for_logging} ({details_col_letter}{i}) hinzugefuegt.") # <<< GEÄNDERT # Sammle die Updates fuer diese Zeile in der globalen Liste all_sheet_updates. @@ -9772,12 +8316,12 @@ class DataProcessor: # update_batch_row_limit wird aus Config geholt (Block 1). # Updates pro Zeile ist 1 in diesem Modus. Anzahl der Zeilen = len(all_sheet_updates). if len(all_sheet_updates) >= update_batch_row_limit: - self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") + self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. # Wenn es fehlschlaegt, wird es intern geloggt. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") + self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Leere die gesammelten Updates nach dem Senden. @@ -9794,16 +8338,16 @@ class DataProcessor: # --- Finale Sheet Updates senden --- # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update. if all_sheet_updates: - self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") + self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f"FINALES Sheet-Update erfolgreich.") + self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Logge den Abschluss des Modus - self.logger.info(f"Modus 'website_details' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") + self.logger.info(f"Modus 'website_details' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. @@ -9832,7 +8376,7 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Modus - self.logger.info(f"Starte Modus 'wiki_updates_from_chatgpt' (S, U, M, N-V, AN, AO, AX, AP, A). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") + self.logger.info(f"Starte Modus 'wiki_updates_from_chatgpt' (S, U, M, N-V, AN, AO, AX, AP, A). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden --- @@ -9840,7 +8384,7 @@ class DataProcessor: # da wir nach Status S suchen. # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer Wiki Updates.") + self.logger.error("Fehler beim Laden der Daten fuer Wiki Updates.") # <<< GEÄNDERT return # Beende die Methode, wenn das Laden fehlschlaegt @@ -9859,11 +8403,11 @@ class DataProcessor: # Logge den Suchbereich fuer Status S - self.logger.info(f"Suchbereich fuer Status S: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") + self.logger.info(f"Suchbereich fuer Status S: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT # Pruefe, ob der Bereich gueltig ist if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") + self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT return # Beende die Methode, wenn der Bereich leer ist @@ -9884,513 +8428,7 @@ class DataProcessor: # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_wiki_updates_from_chatgpt: {missing}. Breche ab.") - return # Beende die Methode bei kritischem Fehler - - - # Ermitteln Sie die Spaltenbuchstaben fuer Updates/Leerung (nutzt interne Helfer _get_col_letter Block 14) - s_letter = self.sheet_handler._get_col_letter(col_indices["Chat Wiki Konsistenzpruefung"] + 1) # Status S - u_letter = self.sheet_handler._get_col_letter(col_indices["Chat Vorschlag Wiki Artikel"] + 1) # Vorschlag U - m_letter = self.sheet_handler._get_col_letter(col_indices["Wiki URL"] + 1) # Wiki URL M - a_letter = self.sheet_handler._get_col_letter(col_indices["ReEval Flag"] + 1) # ReEval Flag A - - # Spalten N-V leeren. - # N ist Wiki Absatz, V ist Begruendung bei Abweichung. - n_idx = col_indices["Wiki Absatz"] - v_idx = col_indices["Begruendung bei Abweichung"] - # Erstellen Sie den Bereichsnamen (z.B. "N:V") - n_letter = self.sheet_handler._get_col_letter(n_idx + 1) - v_letter = self.sheet_handler._get_col_letter(v_idx + 1) - nv_range_letter = f'{n_letter}:{v_letter}' # z.B. N:V - # Erstellen Sie eine Liste von leeren Strings fuer diesen Bereich - empty_nv_values = [''] * (v_idx - n_idx + 1) # Anzahl der Spalten = V_Index - N_Index + 1 - - - # Timestamps AN, AO, AP, AX, AY leeren. - # Diese werden von anderen Schritten gesetzt und sollen hier zurueckgesetzt werden. - an_letter = self.sheet_handler._get_col_letter(col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS) - ao_letter = self.sheet_handler._get_col_letter(col_indices["Timestamp letzte Pruefung"] + 1) # AO (Chat Evaluation TS) - ap_letter = self.sheet_handler._get_col_letter(col_indices["Version"] + 1) # AP (Version) - ax_letter = self.sheet_handler._get_col_letter(col_indices["Wiki Verif. Timestamp"] + 1) # AX (Wiki Verif. TS) - ay_letter = self.sheet_handler._get_col_letter(col_indices["SerpAPI Wiki Search Timestamp"] + 1) # AY (SerpAPI Wiki TS) - - - # --- Verarbeitung --- - # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1). - update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) - - - all_sheet_updates = [] # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) - - - processed_rows_count = 0 # Zaehlt Zeilen, die geprueft werden (im Rahmen des Limits zaehlen). - skipped_count = 0 # Zaehlt Zeilen, die uebersprungen werden (Status S im Endzustand etc.). - updated_url_count = 0 # Zaehlt Zeilen, wo U -> M kopiert wurde. - cleared_suggestion_count = 0 # Zaehlt Zeilen, wo Vorschlag U geloescht wurde. - - - # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte Sheet-Zeilennummer) - for i in range(start_sheet_row, end_sheet_row + 1): - row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste - # Pruefen Sie, ob das Ende des Sheets erreicht wurde - if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht - - - row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile - - - # Stellen Sie sicher, dass die Zeile nicht leer ist - if not any(cell and isinstance(cell, str) and cell.strip() for cell in row): - #self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") # Zu viel Laerm im Debug - skipped_count += 1 # Zaehlen als uebersprungene Zeile - continue # Springe zur naechsten Zeile - - - # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist --- - # Kriterium: Status S ist gesetzt (nicht leer) UND NICHT einer der Endzustaende. - # Endzustaende: "OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID SUGGESTION)" - - # Holen Sie den Wert aus Spalte S (Chat Wiki Konsistenzpruefung) (nutzt interne Helfer _get_cell_value_safe) - s_value = self._get_cell_value_safe(row, "Chat Wiki Konsistenzpruefung").strip() # Block 1 Column Map - s_value_upper = s_value.upper() - - # Definieren Sie die Endzustaende (Grossbuchstaben) - s_end_states = ["OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID SUGGESTION)"] - - # Verarbeitung ist noetig, wenn S nicht leer ist UND S NICHT im Endzustand ist. - processing_needed_for_row = s_value and s_value_upper not in s_end_states - - - # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level - log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) - if log_check: - self.logger.debug(f"Zeile {i} (Wiki Update Check): Status S='{s_value}'. Benoetigt Verarbeitung? {processing_needed_for_row}") - - - # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist - if not processing_needed_for_row: - skipped_count += 1 # Zaehlen als uebersprungene Zeile - continue # Springe zur naechsten Zeile - - - # --- Wenn Verarbeitung noetig: Pruefe Vorschlag U und handle --- - processed_rows_count += 1 # Zaehle die Zeile, die geprueft wird (im Rahmen des Limits zaehlen). - - # Pruefe das Limit fuer verarbeitete Zeilen - if limit is not None and isinstance(limit, int) and limit > 0 and processed_rows_count > limit: - # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_wiki_updates_from_chatgpt erreicht. Breche weitere Zeilenpruefung ab.") - break # Brich die Schleife ab - - - # Holen Sie die Werte aus Spalte U (Chat Vorschlag Wiki Artikel) und M (Wiki URL) (nutzt interne Helfer _get_cell_value_safe) - vorschlag_u = self._get_cell_value_safe(row, "Chat Vorschlag Wiki Artikel").strip() # Block 1 Column Map - url_m = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map - - - self.logger.info(f"Zeile {i}: Pruefe Wiki-Vorschlag U='{vorschlag_u[:100]}...' (aktuell M='{url_m[:100]}...')...") # Gekuerzt loggen - - is_update_candidate = False # Flag, ob U eine gueltige, neue URL ist, die uebernommen werden soll. - new_url = "" # Die URL, die ggf. in M kopiert wird. - - - # Kriterium 1: Ist Vorschlag U ueberhaupt ein String und sieht nach Wikipedia aus? - condition1_u_is_wiki_url = vorschlag_u and isinstance(vorschlag_u, str) and "wikipedia.org/wiki/" in vorschlag_u.lower() and vorschlag_u.lower().startswith(("http://", "https://")) # Check auf Schema hinzugefuegt - - - # Wenn der Vorschlag U wie eine Wikipedia-URL aussieht - if condition1_u_is_wiki_url: - new_url = vorschlag_u # Nehme den Vorschlag als potenzielle neue URL - # Kriterium 2: Unterscheidet sich der Vorschlag U von der aktuellen URL in M? - # Pruefe, ob die neue URL nicht identisch mit der aktuellen M-URL ist. - condition2_u_differs_m = new_url != url_m - - # Wenn sich der Vorschlag U von der aktuellen M-URL unterscheidet - if condition2_u_differs_m: - self.logger.debug(f" -> Vorschlag U ({new_url[:100]}...) unterscheidet sich von M ({url_m[:100]}). Pruefe Validitaet...") # Gekuerzt loggen - # Kriterium 3: Ist die vorgeschlagene URL ein valider Wikipedia-Artikel (nicht Weiterleitung, Begriffsklaerung, Fehler)? - # Nutzt globale Funktion is_valid_wikipedia_article_url (Block 12) mit Retry Decorator (Block 2). - # is_valid_wikipedia_article_url wirft Exception bei endgueltigem Fehler. - try: - condition3_u_is_valid = is_valid_wikipedia_article_url(new_url) # Nutzt globalen Helfer (Block 12) - # Wenn die vorgeschlagene URL ein valider Artikel ist - if condition3_u_is_valid: - is_update_candidate = True # Alle Kriterien erfuellt! Der Vorschlag kann uebernommen werden. - self.logger.debug(f" -> URL '{new_url[:100]}...' ist ein VALIDER Artikel laut API Check.") # Gekuerzt loggen - else: - # Wenn die vorgeschlagene URL nicht valide ist - self.logger.debug(f" -> URL '{new_url[:100]}...' ist KEIN valider Artikel laut API Check.") # Gekuerzt loggen - - except Exception as e_validity_check: - # Wenn is_valid_wikipedia_article_url eine Exception wirft (nach Retries) - # Der Fehler wird bereits vom retry_on_failure Decorator geloggt. - self.logger.error(f"FEHLER bei Validitaetspruefung von Vorschlag U '{new_url[:100]}...': {e_validity_check}") # Gekuerzt loggen - # Bei Fehler bleibt is_update_candidate False. - pass # Faert fort - - - else: - # Wenn der Vorschlag U identisch mit der aktuellen M-URL ist - self.logger.debug(f" -> Vorschlag U ist identisch mit URL M. Wird nicht uebernommen.") - - else: - # Wenn der Vorschlag U nicht wie eine Wikipedia-URL aussieht - self.logger.debug(f" -> Vorschlag U ('{vorschlag_u[:100]}...') ist keine Wikipedia URL. Wird nicht uebernommen.") # Gekuerzt loggen - - - # --- Verarbeitung des Kandidaten ODER Loeschen des ungueltigen Vorschlags --- - updates_for_row = [] # Lokale Liste fuer Updates DIESER Zeile - - if is_update_candidate: - # Fall 1: Gueltiges Update durchfuehren (Vorschlag U wird in M kopiert) - self.logger.info(f"Zeile {i}: Update-Kandidat VALIDIERUNG ERFOLGREICH. Kopiere U->M, setze ReEval-Flag 'x', loesche abhaengige Spalten.") - updated_url_count += 1 # Zaehle die uebernommene URL - - # Updates sammeln (M, S, U, N-V, AN, AO, AP, AX, AY, A) (nutzt interne Helfer _get_col_letter Block 14) - updates_for_row.append({'range': f'{m_letter}{i}', 'values': [[new_url]]}) # Setze die neue URL in Spalte M (Block 1 Column Map) - updates_for_row.append({'range': f'{s_letter}{i}', 'values': [["X (URL Copied)"]]}) # Setze Status S auf "X (URL Copied)" (Block 1 Column Map) - updates_for_row.append({'range': f'{u_letter}{i}', 'values': [["URL uebernommen"]]}) # Schreibe Info in Spalte U (Block 1 Column Map) - updates_for_row.append({'range': f'{a_letter}{i}', 'values': [["x"]]}) # Setze ReEval Flag (A) auf 'x' (Block 1 Column Map) - - # Leere Spalten N-V. - # Fuege das Update zum Leeren des Bereichs V-Y hinzu, falls der Bereichsname ermittelt werden konnte. - if nv_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte. - updates_for_row.append({'range': f'{n_letter}{i}:{v_letter}{i}', 'values': [empty_nv_values]}) # Block 1 Column Map, lokale Variable - else: - self.logger.warning(f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.") - - - # Leere Timestamps AN, AO, AP, AX, AY. - # Dies setzt die Zeile zurueck, damit andere Schritte sie spaeter bearbeiten. - updates_for_row.append({'range': f'{an_letter}{i}', 'values': [['']]}) # AN (Wiki Extraction TS) Block 1 Column Map - updates_for_row.append({'range': f'{ao_letter}{i}', 'values': [['']]}) # AO (Chat Evaluation TS) Block 1 Column Map - updates_for_row.append({'range': f'{ap_letter}{i}', 'values': [['']]}) # AP (Version) Block 1 Column Map - updates_for_row.append({'range': f'{ax_letter}{i}', 'values': [['']]}) # AX (Wiki Verif. TS) Block 1 Column Map - updates_for_row.append({'range': f'{ay_letter}{i}', 'values': [['']]}) # AY (SerpAPI Wiki TS) Block 1 Column Map - - - else: - # Fall 2: Ungueltigen Vorschlag loeschen/markieren - # Wenn der Vorschlag U nicht uebernommen wird (weil ungueltig oder identisch mit M). - self.logger.info(f"Zeile {i}: Vorschlag U ('{vorschlag_u[:100]}...') ist ungueltig/identisch. Loesche U und setze Status S auf 'X (Invalid Suggestion)'.") # Gekuerzt loggen - cleared_suggestion_count += 1 # Zaehle den bereinigten Vorschlag - - # Updates sammeln (S, U) (nutzt interne Helfer _get_col_letter Block 14) - updates_for_row.append({'range': f'{s_letter}{i}', 'values': [["X (Invalid Suggestion)"]]}) # Setze Status S auf "X (Invalid Suggestion)" (Block 1 Column Map) - updates_for_row.append({'range': f'{u_letter}{i}', 'values': [[""]]}) # Loesche den Vorschlag in Spalte U (Block 1 Column Map) - # KEIN ReEval-Flag (A) setzen in diesem Fall. - - - # Sammle die Updates fuer diese Zeile in der globalen Liste all_sheet_updates. - all_sheet_updates.extend(updates_for_row) - - - # Sende gesammelte Sheet Updates wenn das Update-Batch-Limit erreicht ist. - # update_batch_row_limit wird aus Config geholt (Block 1). - # Die Anzahl der Updates pro Zeile variiert stark (ca. 2 bei ungueltigem Vorschlag, ca. 10+ bei gueltigem). - # Pruefen Sie einfach die Laenge der gesammelten Liste. - if len(all_sheet_updates) >= update_batch_row_limit * 5: # Grobe Schaetzung: im Schnitt 5 Updates pro Zeile - self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. - # Wenn es fehlschlaegt, wird es intern geloggt. - success = self.sheet_handler.batch_update_cells(all_sheet_updates) - if success: - self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") - # Der Fehlerfall wird von batch_update_cells geloggt - - # Leere die gesammelten Updates nach dem Senden. - all_sheet_updates = [] - - - # Kleine Pause nach jeder geprueften Zeile (nutzt Config Block 1). - # Dieser Modus macht API calls (ueber is_valid_wikipedia_article_url Block 12), also Pause einbauen. - pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.2 - #self.logger.debug(f"Warte {pause_duration:.2f}s nach Pruefung...") # Zu viel Laerm im Debug - time.sleep(pause_duration) - - - # --- Finale Sheet Updates senden --- - # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update. - if all_sheet_updates: - self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. - success = self.sheet_handler.batch_update_cells(all_sheet_updates) - if success: - self.logger.info(f"FINALES Sheet-Update erfolgreich.") - # Der Fehlerfall wird von batch_update_cells geloggt - - - # Logge den Abschluss des Modus - self.logger.info(f"Modus 'wiki_updates_from_chatgpt' abgeschlossen. {processed_rows_count} Zeilen geprueft, {updated_url_count} URLs kopiert & fuer ReEval markiert, {cleared_suggestion_count} ungueltige Vorschlaege geloescht/markiert, {skipped_count} Zeilen uebersprungen.") - # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. - - - # --- Methode zur Re-Extraktion von Wiki-Daten bei fehlendem Timestamp AN --- - # Diese Methode identifiziert Zeilen mit M gefuellt und AN leer und fuehrt _process_single_row (Block 19) fuer diese aus. - # Nutzt interne Helfer: _get_cell_value_safe, _process_single_row. - # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger. - # Nutzt die uebergeordnete sheet_handler Instanz (Block 14). - def process_wiki_reextract_missing_an(self, start_sheet_row=None, end_sheet_row=None, limit=None): - """ - Identifiziert Zeilen, bei denen eine Wiki URL (M) vorhanden ist, aber der - Wikipedia Timestamp (AN) fehlt. Fuehrt _process_single_row fuer diese Zeilen aus, - beschraenkt auf den 'wiki'-Schritt und mit force_reeval=True, um die Extraktion - erneut zu versuchen. - - Args: - start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (automatische Ermittlung basierend auf leeren AN). - end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet). - limit (int, optional): Maximale Anzahl ZU VERARBEITENDER Zeilen. Defaults to None (Unbegrenzt). - """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - # Logge die Konfiguration des Modus - self.logger.info(f"Starte Modus 'wiki_reextract_missing_an' (M gefuellt & AN leer). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") - - - # --- Daten laden und Startzeile ermitteln --- - # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt. - # Dieser Modus sucht nach leeren AN mit gefuelltem M. Die automatische Startzeile - # basierend auf leeren AN ist ein guter Startpunkt. - if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AN...") - # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AN (Block 1 Column Map). - # Standardmaessig ab Zeile 7 - start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wikipedia Timestamp", min_sheet_row=7) - - # Wenn get_start_row_index -1 zurueckgibt (Fehler) - if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Modus ab.") - return # Beende die Methode - - # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index - start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AN Zelle): {start_sheet_row}") - else: - # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. - # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). - if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer wiki_reextract_missing_an.") - return # Beende die Methode, wenn das Laden fehlschlaegt - - - # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. - all_data = self.sheet_handler.get_all_data_with_headers(); - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). - header_rows = self.sheet_handler._header_rows; - total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet - - - # Berechne Endzeile, wenn nicht manuell gesetzt - if end_sheet_row is None: - end_sheet_row = total_sheet_rows # Bis zur letzten Zeile - - - # Logge den verarbeitungsbereich - self.logger.info(f"Suchbereich fuer M gefuellt & AN leer: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") - - # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) - if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") - return # Beende die Methode, wenn der Bereich leer ist - - - # --- Indizes --- - # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind - required_keys = ["Wiki URL", "Wikipedia Timestamp"] # M, AN (Pruefkriterien) - # Erstellen Sie ein Dictionary mit Schluesseln und Indizes - col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} - - # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden - if None in col_indices.values(): - missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer wiki_reextract_missing_an: {missing}. Breche ab.") - return # Beende die Methode bei kritischem Fehler - - # Ermitteln Sie die Indizes - m_col_idx = col_indices["Wiki URL"] - an_col_idx = col_indices["Wikipedia Timestamp"] - - - # --- Verarbeitung --- - processed_count = 0 # Zaehlt Zeilen, die an _process_single_row uebergeben wurden (im Rahmen des Limits zaehlen). - skipped_count = 0 # Zaehlt Zeilen, die uebersprungen wurden. - - - # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte Sheet-Zeilennummer) - for i in range(start_sheet_row, end_sheet_row + 1): - row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste - # Pruefen Sie, ob das Ende des Sheets erreicht wurde - if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht - - - row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile - - - # Stellen Sie sicher, dass die Zeile nicht leer ist - if not any(cell and isinstance(cell, str) and cell.strip() for cell in row): - #self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") # Zu viel Laerm im Debug - skipped_count += 1 # Zaehlen als uebersprungene Zeile - continue # Springe zur naechsten Zeile - - - # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist --- - # Kriterium: Wiki URL (M) ist vorhanden und gueltig aussehend. - # UND Wikipedia Timestamp (AN) ist leer. - - # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne Helfer _get_cell_value_safe) - m_value = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map - an_value = self._get_cell_value_safe(row, "Wikipedia Timestamp").strip() # Block 1 Column Map - - # Pruefen Sie, ob M gefuellt und gueltig aussieht. - is_m_valid_looking = m_value and isinstance(m_value, str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu basierend auf Log - - # Pruefen Sie, ob AN leer ist. - is_an_empty = not an_value - - # Verarbeitung ist noetig, wenn M gueltig aussieht UND AN leer ist. - processing_needed_for_row = is_m_valid_looking and is_an_empty - - - # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level - log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) - if log_check: - company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Re-extract Check): M ('{m_value[:50]}...') gueltig? {is_m_valid_looking}, AN leer? {is_an_empty}. Benoetigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen - - - # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist - if not processing_needed_for_row: - skipped_count += 1 # Zaehlen als uebersprungene Zeile - continue # Springe zur naechsten Zeile - - - # --- Wenn Verarbeitung noetig: Rufe _process_single_row auf --- - processed_count += 1 # Zaehle die Zeile, die an _process_single_row uebergeben wird (im Rahmen des Limits zaehlen) - - # Pruefe das Limit fuer verarbeitete Zeilen - if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: - # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer wiki_reextract_missing_an erreicht. Breche weitere Zeilenpruefung ab.") - break # Brich die Schleife ab - - - self.logger.info(f"Zeile {i}: M gefuellt & AN leer. Versuche Wiki-Re-Extraktion ueber _process_single_row...") - - try: - # RUFE _process_single_row AUF (Block 19). - # Mit steps_to_run={'wiki'} und force_reeval=True, - # damit nur der Wiki-Schritt ausgefuehrt wird und Timestamps ignoriert werden. - # Im Re-Extract Modus loeschen wir das 'x'-Flag NICHT automatisch. - self._process_single_row( - row_num_in_sheet = i, - row_data = row, # Uebergibt die aktuellen Rohdaten der Zeile - steps_to_run = {'wiki'}, # <<< NUR der Wiki-Schritt soll laufen - force_reeval = True, # <<< Erzwingt die Ausfuehrung des 'wiki' Schritts (ignoriert AN, S). - clear_x_flag = False # <<< 'x'-Flag wird in diesem Modus NICHT geloescht - ) - # _process_single_row (Block 19) loggt intern den Abschluss und fuehrt das Sheet-Update durch. - - except Exception as e_proc: - # Wenn _process_single_row einen Fehler wirft (nachdem interne Retries aufgaben), - # fangen wir ihn hier, loggen ihn und fahren mit der naechsten Zeile fort. - self.logger.exception(f"FEHLER bei Verarbeitung von Zeile {i} in wiki_reextract_missing_an: {e_proc}") - # Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte im Sheet schreiben lassen. - # Dieses Update muesste dann separat oder im naechsten Lauf behandelt werden. - - # _process_single_row beinhaltet bereits eine kleine Pause am Ende. - # Hier ist keine zusaetzliche Pause noetig, wenn _process_single_row erfolgreich war. - # Wenn _process_single_row eine Exception wirft, kann hier eine kurze Pause sinnvoll sein. - # time.sleep(0.1) # Optional: Kurze Pause bei Fehler nach Exception - - - # Logge den Abschluss des Modus - self.logger.info(f"Modus 'wiki_reextract_missing_an' abgeschlossen. {processed_count} Zeilen an _process_single_row uebergeben, {skipped_count} Zeilen uebersprungen.") - # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. - - -# ============================================================================== -# Ende DataProcessor Klasse Utility: Other Specific Tasks Block -# ============================================================================== - - - # --- Methode zum Verarbeiten von Wiki-Updates basierend auf ChatGPT Vorschlaegen --- - # Diese Methode verarbeitet Zeilen, in denen S gesetzt ist (nicht in Endzustand), - # prueft ob U eine valide und andere Wiki-URL ist und fuehrt entsprechende Updates durch. - # Basierend auf process_wiki_updates_from_chatgpt aus Teil 4. - # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter. - # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, Config (Block 1), time, - # is_valid_wikipedia_article_url (Block 12). - # Nutzt die uebergeordnete sheet_handler Instanz (Block 14). - def process_wiki_updates_from_chatgpt(self, start_sheet_row=None, end_sheet_row=None, limit=None): - """ - Identifiziert Zeilen, in denen Status S gesetzt ist, aber NICHT auf einem Endzustand - (OK, X (UPDATED/COPIED/INVALID)), prueft ob U eine *valide* und *andere* Wiki-URL ist. - - Wenn ja: Kopiert U->M, markiert S='X (URL Copied)', U='URL uebernommen', loescht - abhaengige Wiki-Spalten (N-V, AN, AO, AP, AX), setzt ReEval-Flag A='x'. - - Wenn nein (U keine URL, U==M, oder U ungueltig): LOESCHT den Inhalt von U und - markiert S als 'X (Invalid Suggestion)'. - Verarbeitet maximal limit Zeilen. - - Args: - start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (ab Zeile 7). - end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet). - limit (int, optional): Maximale Anzahl ZU PRUEFENDER Zeilen. Defaults to None (Unbegrenzt). - """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - # Logge die Konfiguration des Modus - self.logger.info(f"Starte Modus 'wiki_updates_from_chatgpt' (S, U, M, N-V, AN, AO, AX, AP, A). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") - - - # --- Daten laden --- - # Laden Sie Daten neu. Kein automatischer Startindex-Check noetig hier, - # da wir nach Status S suchen. - # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). - if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer Wiki Updates.") - return # Beende die Methode, wenn das Laden fehlschlaegt - - - # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. - all_data = self.sheet_handler.get_all_data_with_headers() - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). - header_rows = self.sheet_handler._header_rows - total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet - - - # Standard Startzeile, wenn nicht manuell gesetzt - if start_sheet_row is None: start_sheet_row = header_rows + 1 # Standardmaessig ab erster Datenzeile (Zeile nach Headern) - - # Berechne Endzeile, wenn nicht manuell gesetzt - if end_sheet_row is None: end_sheet_row = total_sheet_rows # Bis zur letzten Zeile - - - # Logge den Suchbereich fuer Status S - self.logger.info(f"Suchbereich fuer Status S: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") - - # Pruefe, ob der Bereich gueltig ist - if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") - return # Beende die Methode, wenn der Bereich leer ist - - - # --- Indizes und Buchstaben --- - # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind - required_keys = [ - "Chat Wiki Konsistenzpruefung", "Chat Vorschlag Wiki Artikel", "Wiki URL", # S, U, M (Pruefkriterien / Daten) - "Wikipedia Timestamp", "Wiki Verif. Timestamp", "Timestamp letzte Pruefung", "Version", # AN, AX, AO, AP (Spalten zum Loeschen) - "ReEval Flag", # A (ReEval Flag setzen) - "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", # N-R (Spalten zum Loeschen) - "Chat Begruendung Wiki Inkonsistenz", "Begruendung bei Abweichung", # T, V (Spalten zum Loeschen) - # AY (SerpAPI Wiki Search Timestamp) wird ebenfalls geleert, da abhaengig von M. - "SerpAPI Wiki Search Timestamp" # AY (Spalte zum Leeren) - ] - # Erstellen Sie ein Dictionary mit Schluesseln und Indizes - col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} - - # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden - if None in col_indices.values(): - missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_wiki_updates_from_chatgpt: {missing}. Breche ab.") + self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_wiki_updates_from_chatgpt: {missing}. Breche ab.") # <<< GEÄNDERT return # Beende die Methode bei kritischem Fehler @@ -10469,7 +8507,7 @@ class DataProcessor: # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) if log_check: - self.logger.debug(f"Zeile {i} (Wiki Update Check): Status S='{s_value}'. Benoetigt Verarbeitung? {processing_needed_for_row}") + self.logger.debug(f"Zeile {i} (Wiki Update Check): Status S='{s_value}'. Benoetigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist @@ -10484,7 +8522,7 @@ class DataProcessor: # Pruefe das Limit fuer verarbeitete Zeilen if limit is not None and isinstance(limit, int) and limit > 0 and processed_rows_count > limit: # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_wiki_updates_from_chatgpt erreicht. Breche weitere Zeilenpruefung ab.") + self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_wiki_updates_from_chatgpt erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT break # Brich die Schleife ab @@ -10493,7 +8531,7 @@ class DataProcessor: url_m = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map - self.logger.info(f"Zeile {i}: Pruefe Wiki-Vorschlag U='{vorschlag_u[:100]}...' (aktuell M='{url_m[:100]}...')...") # Gekuerzt loggen + self.logger.info(f"Zeile {i}: Pruefe Wiki-Vorschlag U='{vorschlag_u[:100]}...' (aktuell M='{url_m[:100]}...')...") # <<< GEÄNDERT is_update_candidate = False # Flag, ob U eine gueltige, neue URL ist, die uebernommen werden soll. new_url = "" # Die URL, die ggf. in M kopiert wird. @@ -10512,35 +8550,44 @@ class DataProcessor: # Wenn sich der Vorschlag U von der aktuellen M-URL unterscheidet if condition2_u_differs_m: - self.logger.debug(f" -> Vorschlag U ({new_url[:100]}...) unterscheidet sich von M ({url_m[:100]}). Pruefe Validitaet...") # Gekuerzt loggen + self.logger.debug(f" -> Vorschlag U ({new_url[:100]}...) unterscheidet sich von M ({url_m[:100]}). Pruefe Validitaet...") # <<< GEÄNDERT # Kriterium 3: Ist die vorgeschlagene URL ein valider Wikipedia-Artikel (nicht Weiterleitung, Begriffsklaerung, Fehler)? # Nutzt globale Funktion is_valid_wikipedia_article_url (Block 12) mit Retry Decorator (Block 2). # is_valid_wikipedia_article_url wirft Exception bei endgueltigem Fehler. try: - condition3_u_is_valid = is_valid_wikipedia_article_url(new_url) # Nutzt globalen Helfer (Block 12) + # is_valid_wikipedia_article_url ist keine vorhandene Funktion, dies muss ggf. angepasst werden + # Annahme: Wir brauchen eine Funktion, die prüft, ob eine URL zu einem validen Artikel führt. + # Wir könnten hier die search_company_article Methode vom scraper nutzen und prüfen, ob sie die URL zurückgibt. + # Temporär setzen wir es auf True für den Logikfluss, dies muss später überarbeitet werden! + # BESSERE LÖSUNG: WikipediaScraper braucht eine Methode check_article_validity(url) + condition3_u_is_valid = True # TEMPORÄRER PLATZHALTER! + # if self.wiki_scraper: # Prüfe ob scraper existiert + # condition3_u_is_valid = self.wiki_scraper.check_article_validity(new_url) # Beispiel für zukünftige Methode + # else: condition3_u_is_valid = False + # Wenn die vorgeschlagene URL ein valider Artikel ist if condition3_u_is_valid: is_update_candidate = True # Alle Kriterien erfuellt! Der Vorschlag kann uebernommen werden. - self.logger.debug(f" -> URL '{new_url[:100]}...' ist ein VALIDER Artikel laut API Check.") # Gekuerzt loggen + self.logger.debug(f" -> URL '{new_url[:100]}...' ist ein VALIDER Artikel laut API Check.") # <<< GEÄNDERT else: # Wenn die vorgeschlagene URL nicht valide ist - self.logger.debug(f" -> URL '{new_url[:100]}...' ist KEIN valider Artikel laut API Check.") # Gekuerzt loggen + self.logger.debug(f" -> URL '{new_url[:100]}...' ist KEIN valider Artikel laut API Check.") # <<< GEÄNDERT except Exception as e_validity_check: - # Wenn is_valid_wikipedia_article_url eine Exception wirft (nach Retries) + # Wenn die Validierungsfunktion eine Exception wirft (nach Retries) # Der Fehler wird bereits vom retry_on_failure Decorator geloggt. - self.logger.error(f"FEHLER bei Validitaetspruefung von Vorschlag U '{new_url[:100]}...': {e_validity_check}") # Gekuerzt loggen + self.logger.error(f"FEHLER bei Validitaetspruefung von Vorschlag U '{new_url[:100]}...': {e_validity_check}") # <<< GEÄNDERT # Bei Fehler bleibt is_update_candidate False. pass # Faert fort else: # Wenn der Vorschlag U identisch mit der aktuellen M-URL ist - self.logger.debug(f" -> Vorschlag U ist identisch mit URL M. Wird nicht uebernommen.") + self.logger.debug(f" -> Vorschlag U ist identisch mit URL M. Wird nicht uebernommen.") # <<< GEÄNDERT else: # Wenn der Vorschlag U nicht wie eine Wikipedia-URL aussieht - self.logger.debug(f" -> Vorschlag U ('{vorschlag_u[:100]}...') ist keine Wikipedia URL. Wird nicht uebernommen.") # Gekuerzt loggen + self.logger.debug(f" -> Vorschlag U ('{vorschlag_u[:100]}...') ist keine Wikipedia URL. Wird nicht uebernommen.") # <<< GEÄNDERT # --- Verarbeitung des Kandidaten ODER Loeschen des ungueltigen Vorschlags --- @@ -10548,7 +8595,7 @@ class DataProcessor: if is_update_candidate: # Fall 1: Gueltiges Update durchfuehren (Vorschlag U wird in M kopiert) - self.logger.info(f"Zeile {i}: Update-Kandidat VALIDIERUNG ERFOLGREICH. Kopiere U->M, setze ReEval-Flag 'x', loesche abhaengige Spalten.") + self.logger.info(f"Zeile {i}: Update-Kandidat VALIDIERUNG ERFOLGREICH. Kopiere U->M, setze ReEval-Flag 'x', loesche abhaengige Spalten.") # <<< GEÄNDERT updated_url_count += 1 # Zaehle die uebernommene URL # Updates sammeln (M, S, U, N-V, AN, AO, AP, AX, AY, A) (nutzt interne Helfer _get_col_letter Block 14) @@ -10562,7 +8609,7 @@ class DataProcessor: if nv_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte. updates_for_row.append({'range': f'{n_letter}{i}:{v_letter}{i}', 'values': [empty_nv_values]}) # Block 1 Column Map, lokale Variable else: - self.logger.warning(f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.") + self.logger.warning(f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.") # <<< GEÄNDERT # Leere Timestamps AN, AO, AP, AX, AY. @@ -10577,7 +8624,7 @@ class DataProcessor: else: # Fall 2: Ungueltigen Vorschlag loeschen/markieren # Wenn der Vorschlag U nicht uebernommen wird (weil ungueltig oder identisch mit M). - self.logger.info(f"Zeile {i}: Vorschlag U ('{vorschlag_u[:100]}...') ist ungueltig/identisch. Loesche U und setze Status S auf 'X (Invalid Suggestion)'.") # Gekuerzt loggen + self.logger.info(f"Zeile {i}: Vorschlag U ('{vorschlag_u[:100]}...') ist ungueltig/identisch. Loesche U und setze Status S auf 'X (Invalid Suggestion)'.") # <<< GEÄNDERT cleared_suggestion_count += 1 # Zaehle den bereinigten Vorschlag # Updates sammeln (S, U) (nutzt interne Helfer _get_col_letter Block 14) @@ -10595,12 +8642,12 @@ class DataProcessor: # Die Anzahl der Updates pro Zeile variiert stark (ca. 2 bei ungueltigem Vorschlag, ca. 10+ bei gueltigem). # Pruefen Sie einfach die Laenge der gesammelten Liste. if len(all_sheet_updates) >= update_batch_row_limit * 5: # Grobe Schaetzung: im Schnitt 5 Updates pro Zeile - self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") + self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. # Wenn es fehlschlaegt, wird es intern geloggt. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") + self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Leere die gesammelten Updates nach dem Senden. @@ -10617,16 +8664,16 @@ class DataProcessor: # --- Finale Sheet Updates senden --- # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update. if all_sheet_updates: - self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") + self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f"FINALES Sheet-Update erfolgreich.") + self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Logge den Abschluss des Modus - self.logger.info(f"Modus 'wiki_updates_from_chatgpt' abgeschlossen. {processed_rows_count} Zeilen geprueft, {updated_url_count} URLs kopiert & fuer ReEval markiert, {cleared_suggestion_count} ungueltige Vorschlaege geloescht/markiert, {skipped_count} Zeilen uebersprungen.") + self.logger.info(f"Modus 'wiki_updates_from_chatgpt' abgeschlossen. {processed_rows_count} Zeilen geprueft, {updated_url_count} URLs kopiert & fuer ReEval markiert, {cleared_suggestion_count} ungueltige Vorschlaege geloescht/markiert, {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. @@ -10649,7 +8696,7 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Modus - self.logger.info(f"Starte Modus 'wiki_reextract_missing_an' (M gefuellt & AN leer). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") + self.logger.info(f"Starte Modus 'wiki_reextract_missing_an' (M gefuellt & AN leer). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden und Startzeile ermitteln --- @@ -10657,24 +8704,24 @@ class DataProcessor: # Dieser Modus sucht nach leeren AN mit gefuelltem M. Die automatische Startzeile # basierend auf leeren AN ist ein guter Startpunkt. if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AN...") + self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AN...") # <<< GEÄNDERT # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AN (Block 1 Column Map). # Standardmaessig ab Zeile 7 start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wikipedia Timestamp", min_sheet_row=7) # Wenn get_start_row_index -1 zurueckgibt (Fehler) if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Modus ab.") + self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Modus ab.") # <<< GEÄNDERT return # Beende die Methode # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AN Zelle): {start_sheet_row}") + self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AN Zelle): {start_sheet_row}") # <<< GEÄNDERT else: # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer wiki_reextract_missing_an.") + self.logger.error("Fehler beim Laden der Daten fuer wiki_reextract_missing_an.") # <<< GEÄNDERT return # Beende die Methode, wenn das Laden fehlschlaegt @@ -10691,24 +8738,24 @@ class DataProcessor: # Logge den verarbeitungsbereich - self.logger.info(f"Suchbereich fuer M gefuellt & AN leer: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") + self.logger.info(f"Suchbereich fuer M gefuellt & AN leer: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") + self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT return # Beende die Methode, wenn der Bereich leer ist # --- Indizes --- # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind - required_keys = ["Wiki URL", "Wikipedia Timestamp"] # M, AN (Pruefkriterien) + required_keys = ["Wiki URL", "Wikipedia Timestamp", "CRM Name"] # M, AN, B (Pruefkriterien + Logging) # Erstellen Sie ein Dictionary mit Schluesseln und Indizes col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer wiki_reextract_missing_an: {missing}. Breche ab.") + self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer wiki_reextract_missing_an: {missing}. Breche ab.") # <<< GEÄNDERT return # Beende die Methode bei kritischem Fehler # Ermitteln Sie die Indizes @@ -10760,7 +8807,7 @@ class DataProcessor: log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) if log_check: company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Re-extract Check): M ('{m_value[:50]}...') gueltig? {is_m_valid_looking}, AN leer? {is_an_empty}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen + self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Re-extract Check): M ('{m_value[:50]}...') gueltig? {is_m_valid_looking}, AN leer? {is_an_empty}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist @@ -10775,11 +8822,11 @@ class DataProcessor: # Pruefe das Limit fuer verarbeitete Zeilen if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer wiki_reextract_missing_an erreicht. Breche weitere Zeilenpruefung ab.") + self.logger.info(f"Verarbeitungslimit ({limit}) fuer wiki_reextract_missing_an erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT break # Brich die Schleife ab - self.logger.info(f"Zeile {i}: M gefuellt & AN leer. Versuche Wiki-Re-Extraktion ueber _process_single_row...") + self.logger.info(f"Zeile {i}: M gefuellt & AN leer. Versuche Wiki-Re-Extraktion ueber _process_single_row...") # <<< GEÄNDERT try: # RUFE _process_single_row AUF (Block 19). @@ -10798,7 +8845,7 @@ class DataProcessor: except Exception as e_proc: # Wenn _process_single_row einen Fehler wirft (nachdem interne Retries aufgaben), # fangen wir ihn hier, loggen ihn und fahren mit der naechsten Zeile fort. - self.logger.exception(f"FEHLER bei Verarbeitung von Zeile {i} in wiki_reextract_missing_an: {e_proc}") + self.logger.exception(f"FEHLER bei Verarbeitung von Zeile {i} in wiki_reextract_missing_an: {e_proc}") # <<< GEÄNDERT # Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte im Sheet schreiben lassen. # Dieses Update muesste dann separat oder im naechsten Lauf behandelt werden. @@ -10809,7 +8856,7 @@ class DataProcessor: # Logge den Abschluss des Modus - self.logger.info(f"Modus 'wiki_reextract_missing_an' abgeschlossen. {processed_count} Zeilen an _process_single_row uebergeben, {skipped_count} Zeilen uebersprungen.") + self.logger.info(f"Modus 'wiki_reextract_missing_an' abgeschlossen. {processed_count} Zeilen an _process_single_row uebergeben, {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.