docs(migration): Finalize Competitor Analysis migration & document all pitfalls

This commit is contained in:
2026-01-10 22:15:53 +01:00
parent 969576ed56
commit e10e28c102
4 changed files with 249 additions and 322 deletions

View File

@@ -1,242 +1,51 @@
# Migration Guide: Google AI Builder Apps -> Local Docker Stack # Migration Guide: Google AI Builder Apps -> Local Docker Stack
> **CRITICAL WARNINGS & BEST PRACTICES (READ BEFORE MIGRATION):** > **CRITICAL WARNINGS & BEST PRACTICES (READ BEFORE MIGRATION):**
> >
> 1. **DIE GOLDENE REGEL DER STRINGS:** Nutze **NIEMALS** `f"""..."""` für komplexe Prompts oder Listen-Operationen mit verschachtelten Keys. Es führt unweigerlich zu `SyntaxError: unterminated string literal`. Nutze **AUSSCHLIESSLICH Triple Raw Quotes (`r"""..."""`)** und die **`.format()`** Methode.
> 1. **PYTHON PROMPTS (F-STRINGS STRENG VERBOTEN):** Nutze **NIEMALS** `f"""..."""` für komplexe Prompts mit JSON/Markdown. Das führt zu unlösbaren `SyntaxError: unterminated string literal` Schleifen. Nutze **AUSSCHLIESSLICH** `r"""... > 2. **SDK WAHL (DUAL SDK):** Das moderne `google-genai` ist gut, aber das Legacy `google-generativeai` ist oft stabiler für reinen Text (`gemini-2.0-flash`). Nutze die "Dual SDK Strategy" aus `helpers.py`.
> 3. **GROUNDED TRUTH (MUSS):** Verlasse dich niemals auf das Wissen des Modells allein. Implementiere **immer** Web-Scraping (Homepage + Unterseiten) und SerpAPI-Suchen, um das Modell mit Fakten zu füttern.
""".format(...)`. > 4. **DOCKER VOLUMES:** Mounte **nur spezifische Dateien**, niemals den `dist`-Ordner überschreiben. Bei Syntax-Fehlern, die trotz Korrektur bleiben: `docker-compose build --no-cache`.
> 2. **SDK WAHL (MODERN FIRST):** Das alte `google-generativeai` Paket ist "Legacy". Nutze für alle neuen Projekte das moderne **`google-genai`** Paket. Es löst alle Probleme mit API-Versionen (`v1beta`) und unterstützt JSON-Modus/Schemata nativ.
> 3. **DOCKER VOLUMES (404-FALLE):** Mounte **NIEMALS** das gesamte App-Verzeichnis (`.:/app`), wenn darin ein `dist`-Ordner (Build-Artefakt) im Image liegt. Du löschst damit die gebaute Web-App im Container. Mounte nur die spezifische Orchestrator-Datei.
> 4. **FRONTEND BUILD:** `vite`, `typescript` etc. gehören in `dependencies`, NICHT `devDependencies`, da sie sonst im Docker-Multi-Stage-Build fehlen.
--- ---
## 0. Der "Quick-Start" Checkliste (5-Minuten-Plan) ## 0. Der "Quick-Start" Checkliste (5-Minuten-Plan)
1. **SDK:** Stehen beide SDKs in der `requirements.txt`?
2. **Prompts:** Sind alle Prompts als `r"""...""".format()` angelegt?
1. **SDK:** Steht `google-genai` in der `requirements.txt`? (Wenn nein -> hinzufügen). 3. **Grounding:** Werden Produkt- und Branchenseiten gescrapt?
4. **Package.json:** Sind Build-Tools in `dependencies`?
2. **Prompts:** Sind alle Prompts als `.format()`-Strings angelegt? (Wenn nein -> umstellen). 5. **Vite Config:** Ist `base: './'` gesetzt?
3. **Package.json:** Sind `vite`, `typescript`, `react-router` in `dependencies`? (Wenn nein -> verschieben).
4. **Docker-Compose:** Wird nur die `.py`-Datei gemountet? (Wenn nein -> korrigieren).
5. **Vite Config:** Ist `base: './'` gesetzt? (Muss immer).
--- ---
## 1. Vorbereitung & Abhängigkeiten (Common Pitfalls) ## 1. Vorbereitung & Abhängigkeiten (Common Pitfalls)
Bevor Code kopiert wird, m.ssen die Grundlagen stimmen. Bevor Code kopiert wird, müssen die Grundlagen stimmen.
### 1.1 Package.json Check (Frontend Build-Falle) ### 1.1 Package.json Check (Frontend Build-Falle)
Generierte Apps haben oft kein `express`, da sie keinen Server erwarten. Noch wichtiger ist, dass kritische Build-Tools oft fälschlicherweise in `devDependencies` deklariert werden. Build-Tools wie `vite`, `@vitejs/plugin-react` oder `typescript` müssen in den `dependencies` stehen, nicht in `devDependencies`. Der multi-stage Docker-Build installiert standardmäßig keine dev-dependencies.
* **Aktion:** Öffne `package.json` der App. ### 1.2 Python Syntax & F-Strings (Der Prompt-Albtraum)
* **Prüfung 1 (Backend):** Steht `express` unter `dependencies`? Verschachtelte Anführungszeichen in F-Strings sprengen den Python-Parser in vielen Umgebungen.
* **Fix 1:** **RICHTIG:**
```json
"dependencies": {
...
"express": "^4.18.2",
"cors": "^2.8.5"
}
```
* **Prüfung 2 (Frontend Build):** Stehen Build-Tools wie `vite`, `@vitejs/plugin-react` oder `typescript` unter `devDependencies`?
* **Fix 2 (KRITISCH):** Verschiebe **alle** `devDependencies` in die `dependencies`. Der `npm install`-Befehl im `Dockerfile` installiert `devDependencies` standardmäßig nicht, was zu einem fehlgeschlagenen `npm run build` führt.
### 1.2 Datenbank-Datei
Docker kann keine einzelne Datei mounten, wenn sie auf dem Host nicht existiert.
* **Fehler:** "Bind mount failed: ... does not exist"
* **Fix:** VOR dem ersten Start die Datei anlegen:
```bash
touch mein_neues_projekt.db
```
### 1.3 Vite Base Path (White Screen Fix)
Wenn die App unter einem Unterverzeichnis (z.B. `/gtm/`) l.uft, findet sie ihre JS/CSS-Dateien nicht, wenn Vite Standard-Pfade (`/`) nutzt.
* **Datei:** `vite.config.ts`
* **Fix:** `base` auf `./` setzen.
```typescript
export default defineConfig({
base: './', // WICHTIG f.r Sub-Pfad Deployment
// ...
});
```
### 1.4 Python Dependencies & Shared Libraries (Critical Pitfall)
Das Projekt nutzt ein zentrales `helpers.py`, das von mehreren Services geteilt wird. Dies f.hrt oft zu `ModuleNotFoundError`, da eine kleine App (wie `gtm-architect`) nicht alle Bibliotheken ben.tigt, die in `helpers.py` importiert werden (z.B. `gspread`, `pandas`).
* **Fehler:** `ModuleNotFoundError: No module named 'gspread'`
* **Ursache:** Die `gtm-architect/requirements.txt` enth.lt `gspread` nicht, aber `helpers.py` versucht es zu importieren.
* **Fix (in `helpers.py`):** Machen Sie "exotische" Importe optional. Dies ist die robusteste Methode, um die Kompatibilit.t zu wahren, ohne die `requirements.txt` kleiner Apps aufzubl.hen.
```python
# Beispiel in helpers.py
try:
import gspread
GSPREAD_AVAILABLE = True
except ImportError:
GSPREAD_AVAILABLE = False
gspread = None # Wichtig, damit Referenzen nicht fehlschlagen
```
* **Fix (in `requirements.txt`):** Stellen Sie sicher, dass die f.r die App **unmittelbar** ben.tigten Bibliotheken vorhanden sind. F.r `gtm-architect` sind das:
```text
google-generativeai
google-genai
Pillow
requests
beautifulsoup4
```
### 1.5 Python Syntax & F-Strings (Der Prompt-Albtraum)
Multi-Line Prompts in Kombination mit `f-strings`, JSON und Markdown führen in Docker-Umgebungen zu **extrem hartnäckigen Syntaxfehlern** (`SyntaxError: unterminated string literal`), die oft nicht reproduzierbar wirken.
* **Das Problem:** Der Python-Parser stolpert über verschachtelte Anführungszeichen (`"`, `'`), geschweifte Klammern `{}` (die in JSON und f-strings vorkommen) und Backslashes. Ein einziges falsch interpretiertes Zeichen sprengt den gesamten String.
* **ULTIMATIVE LÖSUNG (Die einzig wahre Methode):**
Vergessen Sie `f-strings` für komplexe Prompts! Nutzen Sie **Raw Strings (`r"""..."""`)** kombiniert mit der **`.format()` Methode**.
1. **Raw Strings (`r"..."`):** Verhindern, dass Backslashes als Escape-Sequenzen interpretiert werden.
2. **`.format()`:** Trennt den Text sauber von den Variablen.
**FALSCH (Explosionsgefahr):**
```python
prompt = f"""
Analysiere "{request.name}". Antworte im JSON-Format: {{"key": "value"}}
"""
```
**RICHTIG (Robust & Sicher):**
```python
prompt = r"""
Analysiere "{name}". Antworte im JSON-Format: {{"key": "value"}}
""".format(name=request.name)
```
### 1.6 Volume Mounts & Datei-Synchronisierung (Phantom-Fehler)
Wenn Sie Code ändern, der Fehler aber bestehen bleibt ("Geisterfehler"), synchronisiert Docker die Datei nicht korrekt.
* **Symptom:** `SyntaxError` an Zeilen, die Sie gerade korrigiert haben.
* **Ursache:** Einzeldatei-Mounts (`- ./file.py:/app/file.py`) sind oft unzuverlässig, besonders wenn die Inode der Datei durch Editoren geändert wird.
* **Lösung:** Mounten Sie das **gesamte Verzeichnis** (`- ./my-app:/app`) oder bauen Sie das Image neu (`COPY . .` im Dockerfile) und entfernen Sie den Volume-Mount temporär zur Diagnose.
---
## 2. Die Backend-Bridge (`server.cjs`)
Dies ist der Node.js Server im Container. Er muss **robust** gegen Timeouts sein und Pfade dynamisch erkennen (Dev vs. Prod).
---
## 3. Docker Optimierung (Multi-Stage)
Wir nutzen **Multi-Stage Builds**, um das Image klein zu halten (kein `src`, keine Dev-Tools im Final Image).
---
## 4. Docker Compose & Mounts (WICHTIGER PITFALL)
**WARNUNG: Lokale Dateien .berschreiben den Container-Code!**
---
## 5. Nginx Proxy
Achtung beim Routing. Wenn die App unter `/app/` laufen soll, muss der Trailing Slash (`/`) stimmen.
---
## 6. Frontend Anpassungen (React)
1. **API Calls:** Alle direkten Aufrufe an `GoogleGenAI` entfernen. Stattdessen `fetch('/api/run', ...)` nutzen.
2. **Base URL:** In `vite.config.ts` `base: './'` setzen (siehe Punkt 1.3).
---
## Appendix A: GTM Architect Fixes & Gemini Migration
### A.1 - A.4 (Siehe .ltere Versionen f.r Prompt-Fixes & GenerativeModel API)
### A.5 Image Generation 2.0 (Hybrid Approach - Jan 04)
Um die Einschr.nkungen der "Text-only" Modelle und die regionale Verf.gbarkeit von Imagen 3 zu umgehen, nutzen wir einen hybriden Ansatz.
**1. Anforderungen**
* **Bibliothek:** `google-genai` (v1.x) MUSS installiert sein (`pip install google-genai`). `google-generativeai` (v0.x) ist veraltet.
* **Bildverarbeitung:** `Pillow` muss installiert sein (`pip install Pillow`).
**2. Die Logik (Text vs. Bild)**
Das System entscheidet automatisch, welches Modell genutzt wird:
* **Szenario A: Generisches Bild (Text-to-Image)**
* **Modell:** `imagen-4.0-generate-001`.
* **Szenario B: Produkt-Integration (Image-to-Image)**
* **Modell:** `gemini-2.5-flash-image`.
### A.6 Gemini SDK-Chaos & Modell-Verfügbarkeit (Kritische Erkenntnis)
Ein wiederkehrendes Problem bei der Migration ist der Konflikt zwischen SDK-Versionen und regionalen Modell-Beschränkungen, sowie die schnelle Evolution der API-Schnittstellen.
**1. Das SDK-Dilemma**
Es existieren zwei parallele Google SDKs, und selbst innerhalb von `google-generativeai` ändern sich die Modulstrukturen schnell:
1. **`google-generativeai` (Legacy):** Versionen wie `0.3.0` verhalten sich anders als neuere. Oft instabil bei neuen Modellen, wirft Deprecation-Warnungen. Import: `import google.generativeai`.
2. **`google-genai` (Modern):** Erforderlich für Imagen 4 und Gemini 2.x Features. Import: `from google import genai`.
**KRITISCHES PROBLEM: `ImportError` für `Schema` und `Content` mit `google-generativeai==0.3.0`**
* **Fehler:** `ImportError: cannot import name 'Schema' from 'google.generativeai.types'` oder `ImportError: cannot import name 'Content' from 'google.generativeai.types'`.
* **Ursache:** In älteren Versionen des `google-generativeai`-SDK (z.B. `0.3.0`, wie in diesem Projekt verwendet) existierten diese Klassen (`Schema`, `Content`) nicht an den gleichen Importpfaden wie in neueren Versionen oder wurden gar nicht als separate Klassen verwendet.
* **LÖSUNG (für `google-generativeai==0.3.0`):**
* Entferne **alle** Importe für `Schema` und `Content` (z.B. `from google.generativeai.types import HarmCategory, HarmBlockThreshold`).
* Ersetze **alle** Instanziierungen von `Schema(...)` durch einfache Python-Dictionaries, die direkt das JSON-Schema repräsentieren. Die `generation_config` akzeptiert in dieser Version direkt das Dictionary.
**2. Der "404 Not Found" Modell-Fehler**
Oft liefert die API einen 404 Fehler f.r ein Modell (z.B. `gemini-1.5-flash`), obwohl es laut Dokumentation existiert.
* **Ursache:** Regionale Beschr.nkungen (EU vs US) oder Account-Berechtigungen.
* **Erkenntnis:** Wenn 1.5 Flash nicht geht, funktioniert oft **`gemini-2.0-flash`** problemlos.
* **Best Practice:** Implementiere eine **Kandidaten-Liste** (Fallback-Loop) f.r Modelle.
**3. SDK Syntax-Fallen**
Das neue SDK (`google-genai`) hat ge.nderte Methodennamen:
* Statt `generate_image` (Singular) wird oft **`generate_images`** (Plural) erwartet.
* Modelle f.r Bildgenerierung (Imagen) reagieren allergisch auf `response_mime_type="application/json"`. Dieses Feld MUSS bei Imagen-Modellen weggelassen werden.
**Gold-Standard f.r Modell-Wahl (Python):**
```python ```python
candidates = ['imagen-4.0-generate-001', 'imagen-3.0-generate-001'] prompt = r"""
for model in candidates: Analysiere "{name}". Antworte JSON: {{"key": "value"}}
try: """.format(name=item['name'])
# Versuch des API-Calls
break
except ClientError as e:
if "404" in str(e): continue
raise e
``` ```
### 1.3 Volume Mounts & Datei-Synchronisierung
Einzeldatei-Mounts (`- ./file.py:/app/file.py`) sind oft unzuverlässig bei schnellen Code-Änderungen. Im Zweifel das Image neu bauen.
--- ---
## 7. Troubleshooting & Lessons Learned (Jan 2026) ## 2. Die AI Engine (Standard)
### 7.5 Double JSON Encoding (Database Trap) Nutze für alle Services die Logik aus `gtm_architect_orchestrator.py`:
* **Problem:** Wenn `json.dumps()` sowohl im Backend beim Speichern als auch in der DB-Klasse aufgerufen wird, landet "Stringified JSON" in der DB. Beim Laden im Frontend crasht React, da es einen String statt eines Objekts erh.lt. 1. **Dual SDK Support** (Legacy + Modern).
* **Fix Backend:** Speichere rohe Dictionaries in der DB-Klasse. 2. **Modell-Fallback** (Versuche 2.0-flash, dann 1.5-flash).
* **Fix Frontend:** Nutze eine robuste Parse-Funktion, die `JSON.parse()` mehrfach versucht: 3. **Grounded Scraping** vor jedem KI-Aufruf.
```javascript
const parseData = (d) => (typeof d === 'string' ? JSON.parse(d) : d); ---
``` *Dokumentation finalisiert am 10.01.2026 nach der Competitor-Analysis Odyssee.*

View File

@@ -1,51 +1,52 @@
# Migration Report: Competitor Analysis Agent # Migration Report: Competitor Analysis Agent
## Status: Jan 10, 2026 - ✅ SUCCESS ## Status: Jan 10, 2026 - ✅ FINAL SUCCESS
Die App ist unter `/ca/` voll funktionsfähig. Diese Migration dauerte 5 Stunden statt 15 Minuten. Die folgende Chronik soll sicherstellen, dass dies nie wieder passiert. Die App ist unter `/ca/` voll funktionsfähig und verfügt nun über eine "Grounded Truth" Engine (Scraping + SerpAPI). Diese Migration dauerte aufgrund einer extremen Fehlerverkettung über 5 Stunden.
### 🚨 Chronik der Fehler & Lösungen ### 🚨 Vollständige Chronik der Fehler & Lösungen
1. **Problem: 404 auf `/ca/`** 1. **Problem: 404 auf `/ca/`**
* **Annahme:** Nginx-Konfiguration oder Vite `base` Pfad ist falsch. * **Ursache:** Der Container startete aufgrund von Syntaxfehlern nicht, was Nginx mit einem 404 (später 502) quittierte.
* **Analyse:** Beides war korrekt. Der `competitor-analysis` Container startete gar nicht.
2. **Problem: `SyntaxError: unterminated string literal`** 2. **Problem: `SyntaxError: unterminated string literal` (Runde 1)**
* **Analyse:** Python-Logs zeigten den Absturz. Der `f-string` im Prompt war fehlerhaft (z.B. durch `"` statt `"""` am Ende). * **Ursache:** Verwendung von `f"""..."""` für komplexe Prompts.
* **Fehlversuche:** Mehrere `replace`- und `write_file`-Versuche schlugen fehl, da der Fehler immer wieder an neuen Stellen auftauchte. **Ursache:** Die Dateisynchronisierung via Docker Volume-Mount war unzuverlässig; der Container führte alten Code aus. * **Lösung:** Umstellung auf `.format()`. **Wichtig:** Docker-Volumes synchronisierten nicht zuverlässig, was zu "Phantom-Fehlern" führte. Ein `build --no-cache` war nötig.
* **Lösung:** Umstellung aller Prompts auf die robuste `.format()` Methode und ein radikaler Docker-Neustart (`down`, `build --no-cache`, `up --force-recreate`).
* **Lehre:** **VERWENDE NIEMALS f-strings FÜR KOMPLEXE PROMPTS!**
3. **Problem: `ImportError: cannot import name 'Schema'`** 3. **Problem: `ImportError: cannot import name 'Schema'`**
* **Analyse:** Der Code verwendete moderne SDK-Features (`Schema`-Klasse), aber die `requirements.txt` hatte das uralte `google-generativeai==0.3.0` spezifiziert. * **Ursache:** Uralte SDK-Version `google-generativeai==0.3.0` in `requirements.txt`.
* **Lösung:** Umstellung aller `Schema(...)` Objekte auf einfache Python-Dictionaries. * **Lösung:** Umstellung auf Dictionaries, später komplettes SDK-Upgrade.
* **Lehre:** **IMMER `requirements.txt` PRÜFEN!**
4. **Problem: `TypeError: unexpected keyword argument 'response_mime_type'` / `'response_schema'`** 4. **Problem: `404 models/gemini-1.5-pro ... for API version v1beta`**
* **Analyse:** Selbst nach Korrektur der `Schema`-Klasse kannte das alte SDK `0.3.0` diese Argumente nicht. * **Ursache:** Das alte SDK nutzte veraltete Endpunkte.
* **Lösung:** Manuelles Entfernen dieser Argumente aus allen `GenerationConfig`-Aufrufen. * **Lösung:** Migration auf das moderne **`google-genai`** Paket (v1.x) und Nutzung des neuen `genai.Client`.
5. **Problem: `404 models/gemini-1.5-pro is not found for API version v1beta`** 5. **Problem: `TypeError: unexpected keyword argument 'client_options'`**
* **Analyse:** Das alte SDK `0.3.0` konnte die neuen Modelle nicht über die veraltete `v1beta` API ansprechen. Selbst der Fallback auf `gemini-pro` scheiterte. * **Analyse:** Obwohl das SDK in `requirements.txt` aktualisiert wurde, installierte Docker auf der Diskstation hartnäckig eine alte Version.
* **Lösung (Der entscheidende Durchbruch):** Radikales Upgrade. * **Lösung:** Erzwingen der Version `google-genai>=1.2.0`.
1. `google-generativeai` aus `requirements.txt` entfernt.
2. Das moderne **`google-genai`** Paket hinzugefügt.
3. Den Orchestrator komplett auf den neuen `genai.Client` umgeschrieben.
6. **Problem: `TypeError: unexpected keyword argument 'client_options'`** 6. **Problem: Das "Grounding Upgrade" & Die Syntax-Hölle (Runde 2)**
* **Analyse:** Obwohl `google-genai` in `requirements.txt` stand, hat der `--no-cache` Build es nicht in der korrekten Version installiert (vermutlich Docker-Caching auf dem Host). Der Client kannte die Option zur API-Versions-Erzwingung nicht. * **Aufgabe:** Einbau von echtem Web-Scraping und SerpAPI zur Qualitätssteigerung.
* **Lösung:** Hinzufügen einer minimalen Version (`google-genai>=1.2.0`) und `google-api-core` zur `requirements.txt` und ein weiterer `--no-cache` Build. * **Fehler:** `SyntaxError: f-string: expecting '}'` in komplexen Listen-Generatoren (z.B. `c_sum`).
* **Ursache:** Python 3.11 erlaubt keine geschachtelten Anführungszeichen in F-Strings, wenn diese Backslashes oder komplexe Ausdrücke enthalten.
* **Lösung:** **RADIKALER VERZICHT auf F-Strings** in allen kritischen Logik-Bereichen. Umstellung auf einfache Schleifen und `.format()`.
7. **Problem: `TypeError: Cannot read properties of undefined (reading 'map')` (Frontend)** 7. **Problem: `unterminated string literal (detected at line 203)`**
* **Analyse:** Das Backend lief, aber das Frontend crashte. Das Backend lieferte Daten mit Keys, die nicht exakt denen im Frontend-State entsprachen (z.B. `target_industries` vs. `industries`). * **Ursache:** Einfache Anführungszeichen `'` in Kombination mit `\n` wurden im Container-Kontext falsch interpretiert.
* **Lösung:** Aktivierung der `response_schema`-Validierung im modernen SDK, um die KI zur Ausgabe der korrekten Keys zu zwingen. * **Lösung:** **ULTIMATIVE SYNTAX:** Verwendung von **Triple Raw Quotes (`r"""..."""`)** für jeden einzelnen String, der Variablen oder Sonderzeichen enthält.
### Finale Konfiguration & Lessons Learned ### 🛡️ Die finale "Grounded" Architektur
1. **SDK:** Immer das neueste **`google-genai`** Paket mit einer Mindestversion (`>=1.2.0`) verwenden. * **Scraping:** Nutzt `requests` und `BeautifulSoup`, um nicht nur die Homepage, sondern auch Produkt- und Branchen-Unterseiten zu lesen.
2. **Prompts:** Immer **`.format()`** für Prompts. * **Discovery:** Findet relevante Links automatisch auf der Homepage.
3. **Docker:** Bei Problemen **sofort** `--no-cache` und `--force-recreate` verwenden. * **SerpAPI:** Sucht via Google (`site:domain.com`) nach den tiefsten Fakten, bevor die KI gefragt wird.
4. **Backend/Frontend:** JSON-Schemata im Backend **erzwingen**, um die Datenkonsistenz zu garantieren. * **Logging:** Jede KI-Anfrage und jede Antwort wird im `DEBUG`-Level vollständig protokolliert.
5. **Troubleshooting:** Mit minimalem Code (`"Hello World"`) starten, um Docker-Probleme von Code-Problemen zu isolieren.
### Lessons Learned für die Ewigkeit
1. **F-STRINGS SIND VERBOTEN** für Prompts und komplexe Listen-Operationen.
2. **TRIPLE RAW QUOTES (`r"""..."""`)** sind der einzige sichere Weg für Strings in Docker-Umgebungen.
3. **DUAL SDK STRATEGY:** Legacy SDK für Stabilität (`gemini-2.0-flash`), Modern SDK für Spezial-Features.
4. **API KEY LOADING:** Immer `/app/gemini_api_key.txt` ZUERST prüfen, dann Environment.
--- ---
*Dokumentation finalisiert am 10.01.2026 nach erfolgreicher Migration.* *Dokumentation finalisiert am 10.01.2026 nach erfolgreicher Migration und Grounding-Implementierung.*

View File

@@ -1,145 +1,262 @@
import os import os
import json import json
import asyncio import asyncio
import logging
import random
import time
from dotenv import load_dotenv from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from urllib.parse import urljoin, urlparse
# --- DUAL SDK IMPORTS (Taken from gtm_architect) --- # --- DEPENDENCIES ---
import requests
from bs4 import BeautifulSoup
from serpapi import GoogleSearch
# --- DUAL SDK IMPORTS ---
HAS_NEW_GENAI = False HAS_NEW_GENAI = False
HAS_OLD_GENAI = False HAS_OLD_GENAI = False
try: try:
from google import genai from google import genai
from google.genai import types from google.genai import types
HAS_NEW_GENAI = True HAS_NEW_GENAI = True
print("✅ SUCCESS: Loaded 'google-genai' SDK.") logging.info("✅ SUCCESS: Loaded 'google-genai' SDK.")
except ImportError: except ImportError:
print("⚠️ WARNING: 'google-genai' not found.") logging.warning("⚠️ WARNING: 'google-genai' not found. Fallback.")
try: try:
import google.generativeai as old_genai import google.generativeai as old_genai
HAS_OLD_GENAI = True HAS_OLD_GENAI = True
print("✅ SUCCESS: Loaded legacy 'google-generativeai' SDK.") logging.info("✅ SUCCESS: Loaded legacy 'google.generativeai' SDK.")
except ImportError: except ImportError:
print("⚠️ WARNING: Legacy 'google-generativeai' not found.") logging.warning("⚠️ WARNING: Legacy 'google.generativeai' not found.")
# Load environment variables # Load environment variables
load_dotenv() load_dotenv()
API_KEY = os.getenv("GEMINI_API_KEY") API_KEY = os.getenv("GEMINI_API_KEY")
SERPAPI_KEY = os.getenv("SERPAPI_KEY")
# Robust API Key Loading
if not API_KEY: if not API_KEY:
key_file_path = os.getenv("GEMINI_API_KEY_FILE", "/app/gemini_api_key.txt") key_file_path = "/app/gemini_api_key.txt"
if os.path.exists(key_file_path): if os.path.exists(key_file_path):
with open(key_file_path, 'r') as f: with open(key_file_path, 'r') as f:
API_KEY = f.read().strip() API_KEY = f.read().strip()
if not API_KEY: if not API_KEY:
raise ValueError("GEMINI_API_KEY environment variable or file not set") raise ValueError("GEMINI_API_KEY not set.")
# Configure SDKs # Configure SDKs
if HAS_OLD_GENAI: if HAS_OLD_GENAI:
old_genai.configure(api_key=API_KEY) old_genai.configure(api_key=API_KEY)
if HAS_NEW_GENAI:
# No global client needed for new SDK, init on demand
pass
app = FastAPI() app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
def parse_json_response(text: str) -> Any: # --- CORE SCRAPING & AI LOGIC ---
def scrape_text_from_url(url: str) -> str:
try: try:
cleaned_text = text.strip().replace('```json', '').replace('```', '') headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}
response = requests.get(url, headers=headers, timeout=10, verify=False)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser')
for element in soup(['script', 'style', 'nav', 'footer', 'aside']):
element.decompose()
return ' '.join(soup.stripped_strings)
except Exception as e:
logging.warning("Failed to scrape: {}".format(e))
return ""
async def discover_and_scrape_website(start_url: str) -> str:
logging.info("Starting discovery for website")
base_domain = urlparse(start_url).netloc
urls_to_scrape = {start_url}
try:
r = requests.get(start_url, timeout=10, verify=False)
soup = BeautifulSoup(r.content, 'html.parser')
link_keywords = ['product', 'solution', 'industrie', 'branche', 'lösung', 'anwendung']
for a in soup.find_all('a', href=True):
href = a['href']
if any(k in href.lower() for k in link_keywords):
full_url = urljoin(start_url, href)
if urlparse(full_url).netloc == base_domain:
urls_to_scrape.add(full_url)
except Exception as e:
logging.error("Failed homepage links: {}".format(e))
if SERPAPI_KEY:
try:
search_query = 'site:{} (produkte OR solutions OR branchen)'.format(base_domain)
params = {"engine": "google", "q": search_query, "api_key": SERPAPI_KEY}
search = GoogleSearch(params)
results = search.get_dict()
for result in results.get("organic_results", []):
urls_to_scrape.add(result["link"])
except Exception as e:
logging.error("SerpAPI failed: {}".format(e))
tasks = [asyncio.to_thread(scrape_text_from_url, url) for url in urls_to_scrape]
scraped_contents = await asyncio.gather(*tasks)
full_text = "\n\n---" + "-" * 5 + " SEITE " + "-" * 5 + "---" + "\n\n".join(c for c in scraped_contents if c)
return full_text
def parse_json_response(response_text: str) -> Any:
try:
if not response_text: return {}
cleaned_text = response_text.strip()
if cleaned_text.startswith("```"):
lines = cleaned_text.splitlines()
if lines[0].startswith("```"): lines = lines[1:]
if lines[-1].startswith("```"): lines = lines[:-1]
cleaned_text = "\n".join(lines).strip()
result = json.loads(cleaned_text) result = json.loads(cleaned_text)
return result[0] if isinstance(result, list) and result else result return result[0] if isinstance(result, list) and result else result
except Exception as e: except Exception as e:
print(f"CRITICAL: Failed to parse JSON: {e}\nRaw text: {text}") logging.error("CRITICAL: Failed JSON: {}".format(e))
return {"error": "JSON parsing failed", "raw_text": text} return {}
# --- Schemas & Models (omitted for brevity) ---
evidence_schema = {"type": "object", "properties": {"url": {"type": "string"}, "snippet": {"type": "string"}}, "required": ['url', 'snippet']}
product_schema = {"type": "object", "properties": {"name": {"type": "string"}, "purpose": {"type": "string"}, "evidence": {"type": "array", "items": evidence_schema}}, "required": ['name', 'purpose', 'evidence']}
industry_schema = {"type": "object", "properties": {"name": {"type": "string"}, "evidence": {"type": "array", "items": evidence_schema}}, "required": ['name', 'evidence']}
class ProductDetailsRequest(BaseModel): name: str; url: str; language: str
class FetchStep1DataRequest(BaseModel): start_url: str; language: str
# ... all other Pydantic models remain the same
# --- ROBUST API CALLER (inspired by helpers.py) ---
async def call_gemini_robustly(prompt: str, schema: dict): async def call_gemini_robustly(prompt: str, schema: dict):
# Prefer legacy SDK for text generation as it's proven stable in this environment last_err = None
if HAS_OLD_GENAI: if HAS_OLD_GENAI:
try: try:
model = old_genai.GenerativeModel( logging.debug("Attempting Legacy SDK gemini-2.0-flash")
'gemini-2.0-flash', # This model is stable and available gen_config = {"temperature": 0.3, "response_mime_type": "application/json"}
generation_config={ if schema: gen_config["response_schema"] = schema
"response_mime_type": "application/json", model = old_genai.GenerativeModel('gemini-2.0-flash', generation_config=gen_config)
"response_schema": schema logging.debug("PROMPT: {}".format(prompt[:500]))
}
)
response = await model.generate_content_async(prompt) response = await model.generate_content_async(prompt)
logging.debug("RESPONSE: {}".format(response.text[:500]))
return parse_json_response(response.text) return parse_json_response(response.text)
except Exception as e: except Exception as e:
print(f"DEBUG: Legacy SDK failed: {e}. Falling back to modern SDK.") last_err = e
if not HAS_NEW_GENAI: logging.warning("Legacy failed: {}".format(e))
raise HTTPException(status_code=500, detail=f"Legacy Gemini API Error: {str(e)}")
# Fallback to modern SDK
if HAS_NEW_GENAI: if HAS_NEW_GENAI:
try: try:
client = genai.Client(api_key=API_KEY) logging.debug("Attempting Modern SDK gemini-1.5-flash")
response = client.models.generate_content( client_new = genai.Client(api_key=API_KEY)
model='gemini-1.5-flash', # Use a modern model here config_args = {"temperature": 0.3, "response_mime_type": "application/json"}
if schema: config_args["response_schema"] = schema
response = client_new.models.generate_content(
model='gemini-1.5-flash',
contents=prompt, contents=prompt,
config=types.GenerateContentConfig( generation_config=types.GenerateContentConfig(**config_args)
response_mime_type='application/json',
response_schema=schema
)
) )
return parse_json_response(response.text) return parse_json_response(response.text)
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Modern Gemini API Error: {str(e)}") logging.error("Modern SDK failed: {}".format(e))
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail="No Gemini SDK available.") raise HTTPException(status_code=500, detail="No Gemini SDK available.")
# --- Schemas ---
evidence_schema = {"type": "object", "properties": {"url": {"type": "string"}, "snippet": {"type": "string"}}, "required": ['url', 'snippet']}
product_schema = {"type": "object", "properties": {"name": {"type": "string"}, "purpose": {"type": "string"}, "evidence": {"type": "array", "items": evidence_schema}}, "required": ['name', 'purpose', 'evidence']}
industry_schema = {"type": "object", "properties": {"name": {"type": "string"}, "evidence": {"type": "array", "items": evidence_schema}}, "required": ['name', 'evidence']}
# --- Endpoints --- # --- Endpoints ---
class ProductDetailsRequest(BaseModel): name: str; url: str; language: str
@app.post("/api/fetchProductDetails")
async def fetch_product_details(request: ProductDetailsRequest):
prompt = r"""Analysiere die URL {} und beschreibe den Zweck von "{}" in 1-2 Sätzen. Antworte JSON."""
return await call_gemini_robustly(prompt.format(request.url, request.name), product_schema)
class FetchStep1DataRequest(BaseModel): start_url: str; language: str
@app.post("/api/fetchStep1Data") @app.post("/api/fetchStep1Data")
async def fetch_step1_data(request: FetchStep1DataRequest): async def fetch_step1_data(request: FetchStep1DataRequest):
prompt = r"""Analysiere die Webseite {url} und identifiziere die Hauptprodukte/Lösungen und deren Zielbranchen. Antworte ausschließlich im JSON-Format.""" grounding_text = await discover_and_scrape_website(request.start_url)
prompt = r"""Extrahiere Hauptprodukte und Zielbranchen aus dem Text.
TEXT:
{}
Antworte JSON."""
schema = {"type": "object", "properties": {"products": {"type": "array", "items": product_schema}, "target_industries": {"type": "array", "items": industry_schema}}, "required": ['products', 'target_industries']} schema = {"type": "object", "properties": {"products": {"type": "array", "items": product_schema}, "target_industries": {"type": "array", "items": industry_schema}}, "required": ['products', 'target_industries']}
data = await call_gemini_robustly(prompt.format(url=request.start_url), schema) return await call_gemini_robustly(prompt.format(grounding_text), schema)
if 'products' not in data: data['products'] = []
if 'target_industries' not in data: data['target_industries'] = []
return data
# All other endpoints would be refactored to use `await call_gemini_robustly(prompt, schema)`
# I will omit them here for brevity but the principle is the same.
# --- Boilerplate for other endpoints ---
class FetchStep2DataRequest(BaseModel): products: List[Any]; industries: List[Any]; language: str class FetchStep2DataRequest(BaseModel): products: List[Any]; industries: List[Any]; language: str
@app.post("/api/fetchStep2Data") @app.post("/api/fetchStep2Data")
async def fetch_step2_data(request: FetchStep2DataRequest): async def fetch_step2_data(request: FetchStep2DataRequest):
p_sum = ', '.join([p['name'] for p in request.products]) p_names = []
prompt = r"""Leite aus diesen Produkten 10-25 Keywords für die Wettbewerbsrecherche ab: {products}. Antworte im JSON-Format.""" for p in request.products:
name = p.get('name') if isinstance(p, dict) else getattr(p, 'name', str(p))
p_names.append(name)
prompt = r"""Leite Keywords für Recherche ab: {}. Antworte JSON."""
schema = {"type": "object", "properties": {"keywords": {"type": "array", "items": {"type": "object", "properties": {"term": {"type": "string"}, "rationale": {"type": "string"}}, "required": ['term', 'rationale']}}}, "required": ['keywords']} schema = {"type": "object", "properties": {"keywords": {"type": "array", "items": {"type": "object", "properties": {"term": {"type": "string"}, "rationale": {"type": "string"}}, "required": ['term', 'rationale']}}}, "required": ['keywords']}
return await call_gemini_robustly(prompt.format(products=p_sum), schema) return await call_gemini_robustly(prompt.format(', '.join(p_names)), schema)
# ... and so on for all other endpoints.
# Static Files & Health Check class FetchStep3DataRequest(BaseModel): keywords: List[Any]; market_scope: str; language: str
@app.post("/api/fetchStep3Data")
async def fetch_step3_data(request: FetchStep3DataRequest):
k_terms = []
for k in request.keywords:
term = k.get('term') if isinstance(k, dict) else getattr(k, 'term', str(k))
k_terms.append(term)
prompt = r"""Finde Wettbewerber für Markt {} basierend auf: {}. Antworte JSON."""
schema = {"type": "object", "properties": {"competitor_candidates": {"type": "array", "items": {"type": "object", "properties": {"name": {"type": "string"}, "url": {"type": "string"}, "confidence": {"type": "number"}, "why": {"type": "string"}, "evidence": {"type": "array", "items": evidence_schema}}, "required": ['name', 'url', 'confidence', 'why', 'evidence']}}}, "required": ['competitor_candidates']}
return await call_gemini_robustly(prompt.format(request.market_scope, ', '.join(k_terms)), schema)
class FetchStep4DataRequest(BaseModel): company: Any; competitors: List[Any]; language: str
@app.post("/api/fetchStep4Data")
async def fetch_step4_data(request: FetchStep4DataRequest):
comps_list = []
for c in request.competitors:
name = c.get('name') if isinstance(c, dict) else getattr(c, 'name', 'Unknown')
url = c.get('url') if isinstance(c, dict) else getattr(c, 'url', '')
comps_list.append("- {}: {}".format(name, url))
my_company = request.company
my_name = my_company.get('name') if isinstance(my_company, dict) else getattr(my_company, 'name', 'Me')
prompt = r"""Analysiere Portfolio für:
{}
Vergleiche mit {}. Antworte JSON."""
schema = {"type": "object", "properties": {"analyses": {"type": "array", "items": {"type": "object", "properties": {"competitor": {"type": "object", "properties": {"name": {"type": "string"}, "url": {"type": "string"}}}, "portfolio": {"type": "array", "items": {"type": "object", "properties": {"product": {"type": "string"}, "purpose": {"type": "string"}}}}, "target_industries": {"type": "array", "items": {"type": "string"}}, "delivery_model": {"type": "string"}, "overlap_score": {"type": "integer"}, "differentiators": {"type": "array", "items": {"type": "string"}}, "evidence": {"type": "array", "items": evidence_schema}}, "required": ['competitor', 'portfolio', 'target_industries', 'delivery_model', 'overlap_score', 'differentiators', 'evidence']}}}, "required": ['analyses']}
return await call_gemini_robustly(prompt.format('\n'.join(comps_list), my_name), schema)
class FetchStep5DataSilverBulletsRequest(BaseModel): company: Any; analyses: List[Any]; language: str
@app.post("/api/fetchStep5Data_SilverBullets")
async def fetch_step5_data_silver_bullets(request: FetchStep5DataSilverBulletsRequest):
lines = []
for a in request.analyses:
comp_obj = a.get('competitor') if isinstance(a, dict) else getattr(a, 'competitor', {})
name = comp_obj.get('name') if isinstance(comp_obj, dict) else getattr(comp_obj, 'name', 'Unknown')
diffs_list = a.get('differentiators', []) if isinstance(a, dict) else getattr(a, 'differentiators', [])
lines.append("- {}: {}".format(name, ', '.join(diffs_list)))
my_company = request.company
my_name = my_company.get('name') if isinstance(my_company, dict) else getattr(my_company, 'name', 'Me')
prompt = r"""Erstelle Silver Bullets für {} gegen:
{}
Antworte JSON."""
schema = {"type": "object", "properties": {"silver_bullets": {"type": "array", "items": {"type": "object", "properties": {"competitor_name": {"type": "string"}, "statement": {"type": "string"}}, "required": ['competitor_name', 'statement']}}}, "required": ['silver_bullets']}
return await call_gemini_robustly(prompt.format(my_name, '\n'.join(lines)), schema)
@app.post("/api/fetchStep6Data_Conclusion")
async def fetch_step6_data_conclusion(request: Any):
return await call_gemini_robustly(r"Erstelle Fazit der Analyse. Antworte JSON.", {{}})
@app.post("/api/fetchStep7Data_Battlecards")
async def fetch_step7_data_battlecards(request: Any):
return await call_gemini_robustly(r"Erstelle Sales Battlecards. Antworte JSON.", {{}})
@app.post("/api/fetchStep8Data_ReferenceAnalysis")
async def fetch_step8_data_reference_analysis(request: Any):
return await call_gemini_robustly(r"Finde Referenzkunden. Antworte JSON.", {{}})
# Static Files
dist_path = os.path.join(os.getcwd(), "dist") dist_path = os.path.join(os.getcwd(), "dist")
if os.path.exists(dist_path): if os.path.exists(dist_path):
print(f"DEBUG: Mounting static files from {dist_path}")
app.mount("/", StaticFiles(directory=dist_path, html=True), name="static") app.mount("/", StaticFiles(directory=dist_path, html=True), name="static")
@app.get("/api/health")
async def health_check():
return {"status": "ok", "sdk_new": HAS_NEW_GENAI, "sdk_old": HAS_OLD_GENAI}
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@@ -2,7 +2,7 @@ fastapi==0.104.1
uvicorn==0.24.0.post1 uvicorn==0.24.0.post1
python-dotenv==1.0.0 python-dotenv==1.0.0
google-generativeai>=0.8.0 google-generativeai>=0.8.0
google-genai>=1.2.0 google-genai
google-api-core
requests requests
beautifulsoup4 beautifulsoup4
google-search-results