diff --git a/brancheneinstufung.py b/brancheneinstufung.py index c70bc090..502ea3e0 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -8395,24 +8395,27 @@ class DataProcessor: """ Batch-Prozess zur Generierung von Parent-Account-Vorschlägen mittels ChatGPT. Schreibt Ergebnisse in Spalten O, P, Q. + Bearbeitet nur Zeilen, bei denen Spalte Q (Parent Vorschlag Timestamp) leer ist, + es sei denn re_evaluate_question_mark ist True und Spalte P ist '?'. Args: start_sheet_row (int, optional): Die 1-basierte Startzeile. end_sheet_row (int, optional): Die 1-basierte Endzeile. limit (int, optional): Maximale Anzahl zu verarbeitender Zeilen. - re_evaluate_question_mark (bool, optional): Wenn True, werden auch Zeilen mit '?' - in Spalte P (Parent Vorschlag Status) erneut bewertet. + re_evaluate_question_mark (bool, optional): Wenn True, werden auch Zeilen mit '?' + in Spalte P (Parent Vorschlag Status) erneut bewertet, + AUCH WENN Spalte Q bereits einen Timestamp hat. """ self.logger.info(f"Starte Parent Account Suggestion Batch. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}, Re-Eval ?: {re_evaluate_question_mark}") # --- Daten laden und Startzeile ermitteln --- col_o_key = "System Vorschlag Parent Account" col_p_key = "Parent Vorschlag Status" - col_q_key = "Parent Vorschlag Timestamp" + col_q_key = "Parent Vorschlag Timestamp" # Timestamp-Spalte für die Auswahl if start_sheet_row is None: - self.logger.info(f"Automatische Ermittlung der Startzeile basierend auf leerem '{col_o_key}'...") - start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key=col_o_key, min_sheet_row=7) + self.logger.info(f"Automatische Ermittlung der Startzeile basierend auf leerem '{col_q_key}'...") # Geändert: Start basierend auf leerem Q + start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key=col_q_key, min_sheet_row=7) if start_data_index_no_header == -1: self.logger.error("FEHLER bei autom. Startzeilenermittlung. Breche ab.") return @@ -8446,24 +8449,21 @@ class DataProcessor: col_o_letter = self.sheet_handler._get_col_letter(COLUMN_MAP[col_o_key] + 1) col_p_letter = self.sheet_handler._get_col_letter(COLUMN_MAP[col_p_key] + 1) col_q_letter = self.sheet_handler._get_col_letter(COLUMN_MAP[col_q_key] + 1) - # Begründung kann optional in eine neue Spalte oder hier ins Log - # Fürs Erste nur Logging der Begründung. - # --- Konfiguration für Parallelverarbeitung --- openai_sem = threading.Semaphore(getattr(Config, 'OPENAI_CONCURRENCY_LIMIT', 3)) - max_workers = getattr(Config, 'MAX_BRANCH_WORKERS', 10) # Wiederverwendung der Branch Worker Config - processing_batch_size = getattr(Config, 'PROCESSING_BRANCH_BATCH_SIZE', 10) # Batch-Größe für Tasks + max_workers = getattr(Config, 'MAX_BRANCH_WORKERS', 10) + processing_batch_size = getattr(Config, 'PROCESSING_BRANCH_BATCH_SIZE', 10) update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) - tasks_for_current_openai_batch = [] all_sheet_updates = [] processed_count = 0 skipped_count = 0 - # Funktion zum Verarbeiten und Schreiben eines Batches + # Funktion zum Verarbeiten und Schreiben eines Batches (bleibt intern gleich) def _execute_and_write_openai_batch(current_tasks): - nonlocal processed_count # Um processed_count im äußeren Scope zu modifizieren + # ... (Code der inneren Funktion bleibt identisch wie im vorherigen Vorschlag) ... + nonlocal processed_count if not current_tasks: return @@ -8482,17 +8482,16 @@ class DataProcessor: except Exception as e_future: self.logger.error(f"Exception im Future für Parent Suggestion Zeile {task_info_orig['row_num']}: {e_future}") batch_results_list.append({ - "row_num": task_info_orig['row_num'], - "suggested_parent": "FEHLER_TASK", + "row_num": task_info_orig['row_num'], + "suggested_parent": "FEHLER_TASK", "justification": str(e_future)[:150], "error": str(e_future) }) - + self.logger.debug(f" OpenAI Batch ({batch_start_log_row}-{batch_end_log_row}) abgeschlossen. {len(batch_results_list)} Ergebnisse erhalten.") if batch_results_list: now_ts_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - current_version_str = getattr(Config, 'VERSION', 'unknown') updates_for_this_batch = [] for res_item in batch_results_list: @@ -8500,33 +8499,29 @@ class DataProcessor: parent_val = res_item.get('suggested_parent', 'k.A. (Fehler)') updates_for_this_batch.append({'range': f'{col_o_letter}{rn}', 'values': [[parent_val]]}) - # Nur auf "?" setzen, wenn ein Vorschlag gemacht wurde (nicht "k.A." oder Fehler) status_val = "?" if parent_val and parent_val.lower() != "k.a." and not parent_val.startswith("FEHLER") else "" updates_for_this_batch.append({'range': f'{col_p_letter}{rn}', 'values': [[status_val]]}) updates_for_this_batch.append({'range': f'{col_q_letter}{rn}', 'values': [[now_ts_str]]}) - # Optional: Begründung in eine neue Spalte schreiben - # updates_for_this_batch.append({'range': f'{BEGRUENDUNG_SPALTE_LETTER}{rn}', 'values': [[res_item.get('justification', '')]]}) - - # Logge die Begründung, auch wenn sie nicht ins Sheet geschrieben wird + if res_item.get('justification'): self.logger.debug(f"Zeile {rn} - Parent Begründung: {res_item.get('justification')[:200]}...") - all_sheet_updates.extend(updates_for_this_batch) - processed_count += len(current_tasks) # Erhöhe processed_count hier + + processed_count += len(current_tasks) # Zähle hier, wenn Tasks tatsächlich an OpenAI gingen - # Sheet Updates in Batches senden - if len(all_sheet_updates) >= update_batch_row_limit * 3: # 3 Spalten pro Update (O, P, Q) - self.logger.info(f"Sende Batch-Updates für Parent Suggestions ({len(all_sheet_updates)//3} Zeilen)...") - self.sheet_handler.batch_update_cells(all_sheet_updates) - all_sheet_updates.clear() + if len(all_sheet_updates) >= update_batch_row_limit * 3: + self.logger.info(f"Sende Batch-Updates für Parent Suggestions ({len(all_sheet_updates)//3} Zeilen)...") + self.sheet_handler.batch_update_cells(all_sheet_updates) + all_sheet_updates.clear() - # Pause nach dem Batch time.sleep(getattr(Config, 'RETRY_DELAY', 5) * 0.5) # Ende der Hilfsfunktion _execute_and_write_openai_batch # Hauptschleife über die Zeilen for i in range(start_sheet_row, end_sheet_row + 1): + # Limit-Prüfung erfolgt jetzt innerhalb der _execute_and_write_openai_batch + # oder besser hier vor dem Sammeln von Tasks, um nicht unnötig zu iterieren. if limit is not None and processed_count >= limit: self.logger.info(f"Verarbeitungslimit ({limit}) für Parent Suggestions erreicht.") break @@ -8540,15 +8535,15 @@ class DataProcessor: continue # Kriterien für Verarbeitung - val_o = self._get_cell_value_safe(row, col_o_key).strip() - val_p = self._get_cell_value_safe(row, col_p_key).strip() - # val_q = self._get_cell_value_safe(row, col_q_key).strip() # Timestamp nicht direkt für Auswahl relevant + val_q_timestamp = self._get_cell_value_safe(row, col_q_key).strip() # Timestamp aus Spalte Q + val_p_status = self._get_cell_value_safe(row, col_p_key).strip() # Status aus Spalte P needs_processing = False - if not val_o or val_o.lower() == "k.a.": # Spalte O ist leer oder k.A. + if not val_q_timestamp: # Spalte Q (Timestamp) ist leer needs_processing = True - elif re_evaluate_question_mark and val_p == "?": # Neubewertung für Status "?" + elif re_evaluate_question_mark and val_p_status == "?": # Neubewertung für Status "?" auch wenn Timestamp Q gesetzt ist needs_processing = True + self.logger.debug(f"Zeile {i}: Wird trotz vorhandenem Timestamp in Q ('{val_q_timestamp}') verarbeitet, da P='?' und re_evaluate_question_mark=True.") if not needs_processing: skipped_count += 1 @@ -8571,11 +8566,9 @@ class DataProcessor: _execute_and_write_openai_batch(tasks_for_current_openai_batch) tasks_for_current_openai_batch.clear() - # Letzten Batch verarbeiten if tasks_for_current_openai_batch: _execute_and_write_openai_batch(tasks_for_current_openai_batch) - # Letzte Sheet Updates senden if all_sheet_updates: self.logger.info(f"Sende finale Batch-Updates für Parent Suggestions ({len(all_sheet_updates)//3} Zeilen)...") self.sheet_handler.batch_update_cells(all_sheet_updates)