Files
Brancheneinstufung2/BUILDER_APPS_MIGRATION.md

394 lines
16 KiB
Markdown

# 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.
---
## 1. Vorbereitung & Abhängigkeiten (Common Pitfalls)
Bevor Code kopiert wird, müssen die Grundlagen stimmen.
### 1.1 Package.json Check
Generierte Apps haben oft kein `express`, da sie keinen Server erwarten.
* **Aktion:** Öffne `package.json` der App.
* **Prüfung:** Steht `express` unter `dependencies`?
* **Fix:**
```json
"dependencies": {
...
"express": "^4.18.2",
"cors": "^2.8.5"
}
```
### 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
requests
beautifulsoup4
```
### 1.5 Python Syntax & F-Strings
Multi-Line Prompts können in Docker-Umgebungen zu **sehr hartnäckigen Syntaxfehlern** führen, selbst wenn sie lokal korrekt aussehen.
* **Das Problem:** Der Python-Parser (insbesondere bei `f-strings` in Kombination mit Zahlen/Punkten am Zeilenanfang oder verschachtelten Klammern) kann Multi-Line-Strings (`f"""..."""`) falsch interpretieren, was zu Fehlern wie `SyntaxError: invalid decimal literal` oder `unmatched ')'` führt, auch wenn der Code scheinbar korrekt ist.
* **ULTIMATIVE LÖSUNG (Maximale Robustheit):**
1. **Vermeide `f"""` komplett für komplexe Multi-Line-Prompts.** Definiere stattdessen den Prompt als **Liste von einzelnen String-Zeilen** und füge sie mit `"\n".join(prompt_parts)` zusammen.
2. **Nutze die `.format()` Methode oder f-Strings in EINZEILIGEN Strings** zur Variablen-Injektion. Dies trennt die String-Definition komplett von der Variablen-Interpolation und ist die robusteste Methode.
```python
# Beispiel: Maximal robust
prompt_template_parts = [
"1) Mache dies: {variable_1}",
"2) Mache das: {variable_2}",
]
prompt_template = "\n".join(prompt_template_parts)
prompt = prompt_template.format(variable_1=wert1, variable_2=wert2)
# System-Instruktion muss immer noch vorangestellt werden:
full_prompt = sys_instr + "\n\n" + prompt
```
* **Versionierung für Debugging:** Um sicherzustellen, dass die korrekte Version des Codes läuft, füge Versionsnummern in die Start-Logs des Node.js Servers (`server.cjs`) und des Python Orchestrators (`gtm_architect_orchestrator.py`) ein.
* `server.cjs`: `console.log(`... (Version: ${VERSION})`);`
* `gtm_architect_orchestrator.py`: `print(f"DEBUG: Orchestrator v{__version__} loaded ...")`
* **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`)
Dies ist der Node.js Server im Container. Er muss **robust** gegen Timeouts sein und Pfade dynamisch erkennen (Dev vs. Prod).
**Gold-Standard Template:**
```javascript
const express = require('express');
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const port = 3005; // ANPASSEN!
app.use(express.json({ limit: '50mb' }));
// 1. Statische Dateien: Robustheit für Docker (Flat) vs. Local (Nested)
const distPath = path.join(__dirname, 'dist'); // Docker Standard
const isProduction = fs.existsSync(distPath);
const staticDir = isProduction ? distPath : __dirname;
console.log(`[Init] Serving static files from: ${staticDir}`);
app.use(express.static(staticDir));
// 2. Python Pfad: Robustheit für Sideloading
let pythonScriptPath = path.join(__dirname, 'mein_orchestrator.py'); // ANPASSEN!
if (!fs.existsSync(pythonScriptPath)) {
pythonScriptPath = path.join(__dirname, '../mein_orchestrator.py');
}
// 3. API Routing
app.post('/api/run', (req, res) => {
// ... spawn logic ...
});
// 4. SPA Fallback
app.get('*', (req, res) => {
res.sendFile(path.join(staticDir, 'index.html'));
});
// 5. Timeout-Härtung (CRITICAL!)
const server = app.listen(port, () => {
console.log(`Server listening on ${port}`);
});
server.setTimeout(600000); // 10 Minuten
server.keepAliveTimeout = 610000;
server.headersTimeout = 620000;
```
---
## 3. Docker Optimierung (Multi-Stage)
Wir nutzen **Multi-Stage Builds**, um das Image klein zu halten (kein `src`, keine Dev-Tools im Final Image).
**Gold-Standard Dockerfile:**
```dockerfile
# Stage 1: Frontend Build
FROM node:20-slim AS frontend-builder
WORKDIR /app
COPY mein-app-ordner/package.json ./
RUN npm install
COPY mein-app-ordner/ .
RUN npm run build
# Stage 2: Runtime
FROM python:3.11-slim
WORKDIR /app
# Node.js installieren (für Server Bridge, optimierte Methode)
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ca-certificates && \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
rm -rf /var/lib/apt/lists/*
# Python Deps (aus der app-spezifischen requirements.txt)
COPY mein-app-ordner/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Server & Frontend Artifacts (Flat Structure!)
COPY mein-app-ordner/server.cjs .
COPY mein-app-ordner/package.json .
RUN npm install --omit=dev
COPY --from=frontend-builder /app/dist ./dist
# Python Logic & Shared Libs
COPY mein_orchestrator.py .
COPY helpers.py .
COPY config.py .
COPY market_db_manager.py .
EXPOSE 3005
CMD ["node", "server.cjs"]
```
---
## 4. Docker Compose & Mounts (WICHTIGER PITFALL)
**WARNUNG: Lokale Dateien überschreiben den Container-Code!**
Wenn Sie `volumes` für das Sideloading verwenden (wie unten gezeigt), werden die Dateien aus Ihrem lokalen Verzeichnis direkt in den Container geladen. Das bedeutet: **Wenn Sie nicht `git pull` auf dem Host-System ausführen, bevor Sie den Container neu bauen, wird der Container weiterhin den alten, lokalen Code ausführen.**
**Workflow:**
1. Änderungen im Git-Repository pushen (oder von einem Agent pushen lassen).
2. **`git pull` auf dem Host-System ausführen.** (Dieser Schritt ist entscheidend!)
3. `docker-compose up -d --build <service-name>` ausführen.
Beim Sideloading müssen **alle** Abhängigkeiten gemountet werden, nicht nur das Hauptskript.
**Wichtig:** Der Pfad zu `server.cjs` ändert sich durch die "Flat Structure" im Dockerfile!
```yaml
my-new-app:
# ... build context ...
volumes:
# Logic Sideloading (ALLE Skripte!)
- ./mein_orchestrator.py:/app/mein_orchestrator.py
- ./helpers.py:/app/helpers.py # WICHTIG: Shared Libs
- ./config.py:/app/config.py # WICHTIG: Shared Libs
- ./market_db_manager.py:/app/market_db_manager.py
# Server Sideloading (Ziel ist Root /app/server.cjs!)
- ./mein-app-ordner/server.cjs:/app/server.cjs
# Persistence
- ./mein_projekt.db:/app/mein_projekt.db
environment:
- PYTHONUNBUFFERED=1
- DB_PATH=/app/mein_projekt.db
```
---
## 5. Nginx Proxy
Achtung beim Routing. Wenn die App unter `/app/` laufen soll, muss der Trailing Slash (`/`) stimmen.
```nginx
location /app/ {
proxy_pass http://my-new-app:3005/; # Slash am Ende wichtig!
# ... headers ...
proxy_read_timeout 1200s; # Timeout passend zum Node Server
}
```
---
## 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).
3. **Router:** Falls `react-router` genutzt wird, muss der `basename` gesetzt werden (z.B. `/gtm/`). Bei einfachem State-Routing (wie in den aktuellen Apps) reicht der `base` Config Eintrag.
---
## Checkliste vor dem Commit
- [ ] `express` in `package.json`?
- [ ] `vite.config.ts` hat `base: './'`?
- [ ] `requirements.txt` enthält die korrekten (minimalen) Dependencies?
- [ ] `server.cjs` hat Timeouts (>600s)?
- [ ] `docker-compose.yml` mountet auch `helpers.py` und `config.py`?
- [ ] Leere `.db` Datei auf dem Host erstellt?
- [ ] Dockerfile nutzt Multi-Stage Build?
---
## Appendix A: GTM Architect Fixes & Gemini Migration (Jan 2026)
### A.1 Problemstellung
- **SyntaxError bei großen Prompts:** Python-Parser (3.11) hatte massive Probleme mit f-Strings, die 100+ Zeilen lang waren und Sonderzeichen enthielten.
- **Library Deprecation:** `google.generativeai` hat Support eingestellt? Nein, aber die Fehlermeldung im Log deutete auf einen Konflikt zwischen alten `openai`-Wrappern und neuen Gemini-Paketen hin.
- **Lösung:**
1. **Prompts ausgelagert:** System-Prompts liegen jetzt in `gtm_prompts.json` und werden zur Laufzeit geladen. Kein Code-Parsing mehr notwendig.
2. **Native Gemini Lib:** Statt OpenAI-Wrapper nutzen wir jetzt `google.generativeai` direkt via `helpers.call_gemini_flash`.
3. **Config:** `gtm-architect/Dockerfile` kopiert nun explizit `gtm_prompts.json`.
### 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. 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
```