Files
Brancheneinstufung2/BUILDER_APPS_MIGRATION.md

16 KiB

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:
    "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:
    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.
    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.

    # 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:

    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.
    # 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:

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:

# 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!

  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.

        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.

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