From 97ca4cd254f51188de3632a9b8c5373b7d1e2fb4 Mon Sep 17 00:00:00 2001 From: Floke Date: Sat, 3 Jan 2026 12:19:51 +0000 Subject: [PATCH] Fix: Gemini API modernization, dynamic model selection, and config path corrections --- BUILDER_APPS_MIGRATION.md | 114 +++++++++++++++++++++++- config.py | 14 +-- docker-compose.yml | 2 +- gtm-architect/requirements.txt | 4 +- gtm_architect_documentation.md | 17 ++++ helpers.py | 157 +++++++++++++++++++-------------- 6 files changed, 233 insertions(+), 75 deletions(-) diff --git a/BUILDER_APPS_MIGRATION.md b/BUILDER_APPS_MIGRATION.md index 2b42a738..91d84f65 100644 --- a/BUILDER_APPS_MIGRATION.md +++ b/BUILDER_APPS_MIGRATION.md @@ -1,5 +1,7 @@ # Migration Guide: Google AI Builder Apps -> Local Docker Stack +> **WICHTIGER HINWEIS:** Der Gemini-Agent führt Code **innerhalb** dieses Docker-Containers aus. Er hat keinen Zugriff auf den Docker-Daemon des Host-Systems. Daher kann und wird der Agent **NIEMALS** in der Lage sein, Befehle wie `docker build`, `docker-compose up` oder andere Docker-Management-Aufgaben auszuführen. Diese Befehle müssen immer vom Benutzer auf dem Host-System ausgeführt werden. + **Ziel:** Standardisierter Prozess, um eine von Google AI Studio generierte React-App schnell, robust und fehlerfrei in die lokale Docker/Python-Architektur zu integrieren. **Grundsatz:** "Minimalset & Robustheit". Wir bauen keine aufgeblähten Container, und wir verhindern Timeouts und fehlende Abhängigkeiten proaktiv. @@ -93,6 +95,17 @@ Multi-Line Prompts können in Docker-Umgebungen zu **sehr hartnäckigen Syntaxfe * **Signaturen prüfen:** Shared Libraries (`helpers.py`) haben oft ältere Signaturen. Immer die tatsächliche Definition prüfen! * Beispiel: `call_openai_chat` unterstützt oft kein `system_message` Argument. Stattdessen Prompt manuell zusammenbauen (`sys_instr + "\n\n" + prompt`). +### 1.6 Pitfall: Veraltete API-Nutzung & Bibliotheksnamen +**ACHTUNG:** Dies ist eine der häufigsten Fehlerquellen bei der Migration älterer KI-Skripte. + +* **Das Problem 1 (Name):** Der Name des Pakets (`google-generativeai`) stimmt nicht mit dem Import (`import google.genai`) überein, den neuere Versionen erwarten. In unserem Fall bleiben wir vorerst beim Import von `google.generativeai`. + * **Installation (`requirements.txt`):** `google-generativeai` + * **Import (z.B. in `helpers.py`):** `import google.generativeai as genai` + +* **Das Problem 2 (API-Nutzung):** Älterer Code verwendet eine `genai.Client`-Klasse, die **nicht mehr existiert**. Dies führt zu einem Absturz. + * **Fehlerbild im Log:** `AttributeError: module 'google.generativeai' has no attribute 'Client'` + * **Lösung:** Der Code MUSS auf die moderne `GenerativeModel`-API umgestellt werden. Siehe **Appendix A.4** für ein Code-Beispiel. + --- ## 2. Die Backend-Bridge (`server.cjs`) @@ -277,5 +290,104 @@ Achtung beim Routing. Wenn die App unter `/app/` laufen soll, muss der Trailing ### A.2 Neuer Standard für KI-Apps Für zukünftige Apps gilt: 1. **Prompts in JSON/Text-Files:** Niemals riesige Strings im Python-Code hardcoden. -2. **`helpers.call_gemini_flash` nutzen:** Diese Funktion ist nun der Gold-Standard für einfache, stateless Calls. +2. **`helpers.call_gemini_flash` nutzen:** Diese Funktion ist nun der Gold-Standard für einfache, stateless Calls. Siehe **Appendix A.4** für die korrekte Implementierung. 3. **JSON im Dockerfile:** Vergesst nicht, die externen Prompt-Files mit `COPY` in den Container zu holen! + +### A.3 Kritisches Problem & Lösung: `AttributeError` bei Gemini API (Jan 2026) +- **Problem:** Nach der Migration auf die `google-generativeai` Bibliothek schlugen alle API-Aufrufe mit einem `AttributeError: module 'google.generativeai' has no attribute 'Client'` Fehler fehl. +- **Log-Analyse:** + ``` + ERROR:helpers:Fehler beim Gemini-Flash-Aufruf: module 'google.generativeai' has no attribute 'Client' + ``` +- **Untersuchung:** + 1. Die erste Annahme, es handle sich um einen falschen Modellnamen (`404 NOT_FOUND`), erwies sich als irreführend. + 2. Die Analyse des `helpers.py`-Skripts zeigte, dass der Code versuchte, eine `genai.Client`-Klasse zu verwenden. +- **Schlussfolgerung & Lösung:** + Der Fehler lag in der Verwendung einer **veralteten API-Initialisierungsmethode**. Die `google-generativeai`-Bibliothek hat die `Client`-Klasse entfernt und erfordert nun einen modernen Ansatz. + + **Die Korrektur (implementiert in `helpers.py`):** + 1. **Konfigurieren des API-Schlüssels:** `genai.configure(api_key="IHR_KEY")` + 2. **Instanziieren des Modells:** `model = genai.GenerativeModel('gemini-1.5-flash-latest')` + 3. **Aufrufen der Generierung:** `response = model.generate_content(...)` + + Dieser Fix wurde in `helpers.py` umgesetzt und hat die Funktionalität des GTM Architect wiederhergestellt. Alle neuen KI-Anwendungen müssen diesem Muster folgen. + +### A.4 Gold-Standard: Gemini API Wrapper (Robust & Dynamic) +Um 404-Fehler durch sich ändernde Modellnamen oder regionale Unterschiede zu vermeiden, nutzt der neue Standard eine **dynamische Modell-Ermittlung**. + +```python +import google.generativeai as genai +import logging + +# Cache für den Modellnamen +_CACHED_MODEL_NAME = None + +def _get_best_flash_model(api_key): + """ + Ermittelt dynamisch das beste verfügbare Flash-Modell via list_models(). + """ + global _CACHED_MODEL_NAME + if _CACHED_MODEL_NAME: + return _CACHED_MODEL_NAME + + default_model = "gemini-1.5-flash" + try: + genai.configure(api_key=api_key) + # Suche nach Modellen mit 'flash' und 'generateContent' Support + models = list(genai.list_models()) + flash_models = [ + m.name.replace('models/', '') + for m in models + if 'flash' in m.name.lower() and 'generateContent' in m.supported_generation_methods + ] + + # Priorisierung + if "gemini-1.5-flash" in flash_models: + _CACHED_MODEL_NAME = "gemini-1.5-flash" + elif flash_models: + _CACHED_MODEL_NAME = flash_models[0] + else: + _CACHED_MODEL_NAME = default_model + + return _CACHED_MODEL_NAME + except Exception: + return default_model + +@retry_on_failure +def call_gemini_flash(prompt, system_instruction=None, temperature=0.3, json_mode=False): + """ + Robuster Wrapper mit dynamischer Modellwahl. + """ + logger = logging.getLogger(__name__) + api_key = _get_gemini_api_key() + + try: + genai.configure(api_key=api_key) + + generation_config = { + "temperature": temperature, + "top_p": 0.95, + "top_k": 40, + "max_output_tokens": 8192, + } + if json_mode: + generation_config["response_mime_type"] = "application/json" + + # Dynamisch das richtige Modell wählen + model_name = _get_best_flash_model(api_key) + + model = genai.GenerativeModel( + model_name=model_name, + generation_config=generation_config, + system_instruction=system_instruction + ) + + contents = [prompt] if isinstance(prompt, str) else prompt + response = model.generate_content(contents) + + return response.text.strip() + + except Exception as e: + logger.error(f"Fehler beim Gemini-Flash-Aufruf: {e}") + raise e +``` diff --git a/config.py b/config.py index a1375be5..0a15a4c4 100644 --- a/config.py +++ b/config.py @@ -16,16 +16,16 @@ import logging # 1. GLOBALE KONSTANTEN UND DATEIPFADE # ============================================================================== -# --- Dateipfade (NEU: Absolute Pfade) --- -# Basisverzeichnis des Projekts, in dem sich diese config.py befindet. -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +# --- Dateipfade (NEU: Feste Pfade für Docker-Betrieb) --- +# Das Basisverzeichnis ist im Docker-Kontext immer /app. +BASE_DIR = "/app" CREDENTIALS_FILE = os.path.join(BASE_DIR, "service_account.json") -API_KEY_FILE = os.path.join(BASE_DIR, "gemini_api_key.txt") # Standard für Gemini -SERP_API_KEY_FILE = os.path.join(BASE_DIR, "serpApiKey.txt") -GENDERIZE_API_KEY_FILE = os.path.join(BASE_DIR, "genderize_API_Key.txt") +API_KEY_FILE = os.path.join(BASE_DIR, "gemini_api_key.txt") +SERP_API_KEY_FILE = os.path.join(BASE_DIR, "serpapikey.txt") +GENDERIZE_API_KEY_FILE = os.path.join(BASE_DIR, "genderize_api_key.txt") BRANCH_MAPPING_FILE = None -LOG_DIR = os.path.join(BASE_DIR, "Log") +LOG_DIR = os.path.join(BASE_DIR, "Log_from_docker") # Log in den gemounteten Ordner schreiben # --- ML Modell Artefakte --- MODEL_FILE = os.path.join(BASE_DIR, "technician_decision_tree_model.pkl") diff --git a/docker-compose.yml b/docker-compose.yml index 56b89a21..34cbf24d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -101,9 +101,9 @@ services: - ./gtm_db_manager.py:/app/gtm_db_manager.py - ./gtm_projects.db:/app/gtm_projects.db - ./Log_from_docker:/app/Log_from_docker - # Mount API keys - ./gemini_api_key.txt:/app/gemini_api_key.txt - ./serpapikey.txt:/app/serpapikey.txt + - ./genderize_api_key.txt:/app/genderize_api_key.txt environment: - PYTHONUNBUFFERED=1 - DB_PATH=/app/gtm_projects.db \ No newline at end of file diff --git a/gtm-architect/requirements.txt b/gtm-architect/requirements.txt index 51121600..352cf035 100644 --- a/gtm-architect/requirements.txt +++ b/gtm-architect/requirements.txt @@ -1,5 +1,5 @@ -google-genai +google-generativeai requests beautifulsoup4 werkzeug -python-dotenv \ No newline at end of file +python-dotenv diff --git a/gtm_architect_documentation.md b/gtm_architect_documentation.md index 444dd5be..793134f3 100644 --- a/gtm_architect_documentation.md +++ b/gtm_architect_documentation.md @@ -82,3 +82,20 @@ docker-compose up -d --build gtm-app * **Logs:** Werden in `/app/Log_from_docker/` geschrieben. * **Format:** `YY-MM-DD_HH-MM-SS_step_type.txt` (z.B. `phase1_extract_response.txt`). * **Node.js Logs:** `docker logs gtm-app`. + +## 6. Aktueller Status (Jan 2026) - FIXED & OPERATIONAL + +Das System ist nach umfangreichen Fixes nun **voll funktionsfähig**. + +* **[GELÖST] Gemini API Integration:** + * **Fehler:** `AttributeError: module 'google.generativeai' has no attribute 'Client'` (Veraltete API). + * **Fehler:** `404 models/gemini-1.5-flash... not found` (Falscher Modellname/Version). + * **Lösung:** Umstellung auf moderne `genai.GenerativeModel` Syntax. Implementierung einer **dynamischen Modell-Ermittlung** (`_get_best_flash_model`), die automatisch das beste verfügbare Modell (z.B. `gemini-1.5-flash`, `gemini-1.5-flash-001`) wählt, anstatt abzustürzen. +* **[GELÖST] Konfiguration & Pfade:** + * Korrekte Einbindung der API-Keys (`serpapikey.txt`, `genderize_api_key.txt`) durch Anpassung der Groß-/Kleinschreibung in `config.py` und Mounts in `docker-compose.yml`. + * Behebung von `IndentationError` in `config.py`. + * Hinzufügen der fehlenden `gtm-architect/requirements.txt` für korrekten Container-Build. + +**Nächste Schritte:** +* Reguläre Nutzung des GTM Architect. +* Bei Bedarf: Erweiterung der `helpers.py` um weitere spezialisierte Modelle. diff --git a/helpers.py b/helpers.py index 56de2ab4..3695e353 100644 --- a/helpers.py +++ b/helpers.py @@ -56,9 +56,15 @@ try: import google.genai as genai HAS_GEMINI = True except ImportError: - HAS_GEMINI = False - genai = None # Sicherstellen, dass genai definiert ist - logging.warning("google-genai Bibliothek nicht gefunden. KI-Funktionen deaktiviert.") + try: + # Fallback auf die ältere Bibliothek, falls die neue nicht da ist + import google.generativeai as genai + HAS_GEMINI = True + logging.warning("Veraltetes Paket 'google.generativeai' wird verwendet. Bitte auf 'google-genai' aktualisieren.") + except ImportError: + HAS_GEMINI = False + genai = None # Sicherstellen, dass genai definiert ist + logging.warning("Keine Google-KI-Bibliothek (weder google.genai noch google.generativeai) gefunden.") # OpenAI Imports entfernen wir oder machen sie optional, um Verwirrung zu vermeiden try: @@ -320,50 +326,99 @@ def _get_gemini_api_key(): logger.error("CRITICAL: No API Key found in Config.API_KEYS or environment variables.") raise ValueError("API Key missing.") +# Cache für den ermittelten Modellnamen, um API-Aufrufe zu sparen +_CACHED_MODEL_NAME = None + +def _get_best_flash_model(api_key): + """ + Ermittelt dynamisch das beste verfügbare Flash-Modell. + Versucht, 'gemini-1.5-flash' zu finden, oder fällt auf Alternativen zurück. + """ + global _CACHED_MODEL_NAME + if _CACHED_MODEL_NAME: + return _CACHED_MODEL_NAME + + logger = logging.getLogger(__name__) + default_model = "gemini-1.5-flash" + + try: + if not HAS_GEMINI: + return default_model + + genai.configure(api_key=api_key) + + # Liste alle Modelle auf + models = list(genai.list_models()) + flash_models = [m.name for m in models if 'flash' in m.name.lower() and 'generateContent' in m.supported_generation_methods] + + # Bereinige die Namen (entferne 'models/' Präfix für den Vergleich, falls nötig) + clean_flash_models = [m.replace('models/', '') for m in flash_models] + + logger.info(f"Gefundene Flash-Modelle: {clean_flash_models}") + + # Priorisierung + if "gemini-1.5-flash" in clean_flash_models: + _CACHED_MODEL_NAME = "gemini-1.5-flash" + elif "gemini-1.5-flash-latest" in clean_flash_models: + _CACHED_MODEL_NAME = "gemini-1.5-flash-latest" + elif "gemini-1.5-flash-001" in clean_flash_models: + _CACHED_MODEL_NAME = "gemini-1.5-flash-001" + elif clean_flash_models: + _CACHED_MODEL_NAME = clean_flash_models[0] # Nimm das erste verfügbare + else: + logger.warning("Kein 'Flash'-Modell gefunden. Versuche Fallback auf 'gemini-pro'.") + pro_models = [m.name for m in models if 'pro' in m.name.lower() and 'generateContent' in m.supported_generation_methods] + if pro_models: + _CACHED_MODEL_NAME = pro_models[0].replace('models/', '') + else: + _CACHED_MODEL_NAME = default_model + + logger.info(f"Ausgewähltes Gemini-Modell: {_CACHED_MODEL_NAME}") + return _CACHED_MODEL_NAME + + except Exception as e: + logger.error(f"Fehler beim Ermitteln des Modells: {e}. Verwende Standard: {default_model}") + return default_model + @retry_on_failure def call_gemini_flash(prompt, system_instruction=None, temperature=0.3, json_mode=False): """ Spezifische Funktion für Gemini 1.5 Flash Aufrufe mit System-Instruction Support. - Aktualisiert für die neue 'google-genai' Bibliothek (v1.0+). + Verwendet die korrekte `GenerativeModel` API. """ logger = logging.getLogger(__name__) - + if not HAS_GEMINI: - logger.error("Fehler: google-genai Bibliothek fehlt.") - raise ImportError("google-genai not installed.") + logger.error("Fehler: google-generativeai Bibliothek fehlt.") + raise ImportError("google-generativeai not installed.") api_key = _get_gemini_api_key() - + try: - # NEU: Client-basierte Instanziierung für google-genai v1.0+ - client = genai.Client(api_key=api_key) - - # Konfiguration für den Aufruf - config = { + genai.configure(api_key=api_key) + + generation_config = { "temperature": temperature, "top_p": 0.95, "top_k": 40, "max_output_tokens": 8192, } - if json_mode: - config["response_mime_type"] = "application/json" + generation_config["response_mime_type"] = "application/json" - # Verwende eine spezifische Version, die oft stabiler ist - model_id = "gemini-1.5-flash-001" + # Dynamische Modell-Ermittlung + model_name = _get_best_flash_model(api_key) - response = client.models.generate_content( - model=model_id, - contents=prompt, - config=genai.types.GenerateContentConfig( - temperature=temperature, - top_p=0.95, - top_k=40, - max_output_tokens=8192, - response_mime_type="application/json" if json_mode else "text/plain", - system_instruction=system_instruction - ) + model = genai.GenerativeModel( + model_name=model_name, + generation_config=generation_config, + system_instruction=system_instruction ) + + # Der Prompt kann ein String oder eine Liste von Teilen sein + contents = [prompt] if isinstance(prompt, str) else prompt + + response = model.generate_content(contents) return response.text.strip() @@ -376,42 +431,16 @@ def call_gemini_flash(prompt, system_instruction=None, temperature=0.3, json_mod @retry_on_failure def call_openai_chat(prompt, temperature=0.3, model=None, response_format_json=False): """ - Zentrale Funktion fuer KI API Aufrufe (jetzt Gemini via google-genai Client). + Zentrale Funktion fuer KI API Aufrufe (jetzt Gemini). + Leitet an `call_gemini_flash` weiter, um Code-Duplizierung zu vermeiden. """ - logger = logging.getLogger(__name__) - - # Lade Gemini API Key - api_key = _get_gemini_api_key() - - if not HAS_GEMINI: - logger.error("Fehler: google-genai Bibliothek fehlt.") - raise ImportError("google-genai not installed.") - - try: - # NEU: Client Instanziierung - client = genai.Client(api_key=api_key) - - model_id = "gemini-1.5-flash-001" - - response = client.models.generate_content( - model=model_id, - contents=prompt, - config=genai.types.GenerateContentConfig( - temperature=temperature, - top_p=0.95, - top_k=40, - max_output_tokens=8192, - response_mime_type="application/json" if response_format_json else "text/plain" - ) - ) - return response.text.strip() - - except Exception as e: - logger.error(f"Fehler beim Gemini-Aufruf: {e}") - # Wenn der Key falsch ist, werfen wir einen lesbaren Fehler - if "API_KEY_INVALID" in str(e) or "403" in str(e): - raise ValueError(f"Invalid API Key: {str(e)}") - raise e + # Das 'model' Argument wird ignoriert, da wir jetzt fest auf Gemini Flash setzen. + return call_gemini_flash( + prompt=prompt, + temperature=temperature, + json_mode=response_format_json, + system_instruction=None # Alte Signatur hatte keine System-Instruction + ) # ... (Rest der Funktionen wie summarize_website_content bleiben, rufen aber jetzt die neue call_openai_chat auf) def summarize_website_content(raw_text, company_name): return "k.A." # Placeholder @@ -477,4 +506,4 @@ def scrape_website_details(url): logger.error(f"Unerwarteter Fehler beim Parsen der URL {url}: {e}") return "Fehler: Ein unerwarteter Fehler ist beim Verarbeiten der Website aufgetreten." def is_valid_wikipedia_article_url(url): return False # Placeholder -def alignment_demo(sheet_handler): pass # Placeholder +def alignment_demo(sheet_handler): pass # Placeholder \ No newline at end of file