feat(gtm): add aspect ratio & corporate design; fix(market): harden backend logging & json parsing

This commit is contained in:
2026-01-05 11:42:15 +00:00
parent 7be63b6d14
commit a5f43fb977
10 changed files with 241 additions and 169 deletions

View File

@@ -52,6 +52,19 @@ The application will be available at `http://localhost:8080`.
* **Logging:** The project uses the `logging` module to log information and errors. * **Logging:** The project uses the `logging` module to log information and errors.
* **Error Handling:** The `readme.md` indicates a critical error related to the `openai` library. The next step is to downgrade the library to a compatible version. * **Error Handling:** The `readme.md` indicates a critical error related to the `openai` library. The next step is to downgrade the library to a compatible version.
## Current Status (Jan 05, 2026) - GTM & Market Intel Fixes
* **GTM Architect (v2.4) - UI/UX Refinement:**
* **Corporate Design Integration:** A central, customizable `CORPORATE_DESIGN_PROMPT` was introduced in `config.py` to ensure all generated images strictly follow a "clean, professional, photorealistic" B2B style, avoiding comic aesthetics.
* **Aspect Ratio Control:** Implemented user-selectable aspect ratios (16:9, 9:16, 1:1, 4:3) in the frontend (Phase 6), passing through to the Google Imagen/Gemini 2.5 API.
* **Frontend Fix:** Resolved a double-declaration bug in `App.tsx` that prevented the build.
* **Market Intelligence Tool (v1.2) - Backend Hardening:**
* **"Failed to fetch" Resolved:** Fixed a critical Nginx routing issue by forcing the frontend to use relative API paths (`./api`) instead of absolute ports, ensuring requests correctly pass through the reverse proxy in Docker.
* **JSON Stability:** The Python Orchestrator and Node.js bridge were hardened against invalid JSON output. The system now robustly handles stdout noise and logs full raw output to `/app/Log/server_dump.txt` in case of errors.
* **Language Support:** Implemented a `--language` flag. The tool now correctly respects the frontend language selection (defaulting to German) and forces the LLM to output German text for signals, ICPs, and outreach campaigns.
* **Logging:** Fixed log volume mounting paths to ensure debug logs are persisted and accessible.
## Current Status (Jan 2026) - GTM Architect & Core Updates ## Current Status (Jan 2026) - GTM Architect & Core Updates
* **GTM Architect (v2.2) - FULLY OPERATIONAL:** * **GTM Architect (v2.2) - FULLY OPERATIONAL:**

View File

@@ -97,6 +97,14 @@ class Config:
PROCESSING_BRANCH_BATCH_SIZE = 20 PROCESSING_BRANCH_BATCH_SIZE = 20
SERPAPI_DELAY = 1.5 SERPAPI_DELAY = 1.5
# --- (NEU) GTM Architect: Stilvorgabe für Bildgenerierung ---
CORPORATE_DESIGN_PROMPT = (
"cinematic industrial photography, sleek high-tech aesthetic, futuristic but grounded reality, "
"volumetric lighting, sharp focus on modern technology, 8k resolution, photorealistic, "
"highly detailed textures, cool steel-blue color grading with subtle safety-yellow accents, "
"wide angle lens, shallow depth of field."
)
# --- Plausibilitäts-Schwellenwerte --- # --- Plausibilitäts-Schwellenwerte ---
PLAUSI_UMSATZ_MIN_WARNUNG = 50000 PLAUSI_UMSATZ_MIN_WARNUNG = 50000
PLAUSI_UMSATZ_MAX_WARNUNG = 200000000000 PLAUSI_UMSATZ_MAX_WARNUNG = 200000000000

View File

@@ -8,15 +8,48 @@ const path = require('path');
const app = express(); const app = express();
const PORT = 3001; const PORT = 3001;
// --- DEBUG LOGGING ---
// WICHTIG: Im Docker-Container market-backend ist nur /app/Log gemountet!
const LOG_DIR = '/app/Log';
const LOG_FILE = path.join(LOG_DIR, 'backend_debug.log');
const DUMP_FILE = path.join(LOG_DIR, 'server_dump.txt');
const logToFile = (message) => {
try {
if (!fs.existsSync(LOG_DIR)) return;
const timestamp = new Date().toISOString();
const logLine = `[${timestamp}] ${message}\n`;
fs.appendFileSync(LOG_FILE, logLine);
} catch (e) {
console.error("Failed to write to debug log:", e);
}
};
const dumpToFile = (content) => {
try {
if (!fs.existsSync(LOG_DIR)) return;
const separator = `\n\n--- DUMP ${new Date().toISOString()} ---\n`;
fs.appendFileSync(DUMP_FILE, separator + content + "\n--- END DUMP ---\n");
} catch (e) {
console.error("Failed to dump:", e);
}
};
// Middleware // Middleware
app.use(cors()); app.use(cors());
app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.json({ limit: '50mb' }));
app.use((req, res, next) => {
logToFile(`INCOMING REQUEST: ${req.method} ${req.url}`);
next();
});
// Helper für Python-Aufrufe, um Code-Duplizierung zu vermeiden // Helper für Python-Aufrufe, um Code-Duplizierung zu vermeiden
const runPython = (args, res, tempFilesToDelete = []) => { const runPython = (args, res, tempFilesToDelete = []) => {
// Im Docker (python:3.11-slim Image) nutzen wir das globale python3 // Im Docker (python:3.11-slim Image) nutzen wir das globale python3
const pythonExecutable = 'python3'; const pythonExecutable = 'python3';
logToFile(`Spawning python: ${args.join(' ')}`);
console.log(`Spawning command: ${pythonExecutable} ${args.join(' ')}`); console.log(`Spawning command: ${pythonExecutable} ${args.join(' ')}`);
const pythonProcess = spawn(pythonExecutable, args); const pythonProcess = spawn(pythonExecutable, args);
@@ -33,31 +66,10 @@ const runPython = (args, res, tempFilesToDelete = []) => {
}); });
pythonProcess.on('close', (code) => { pythonProcess.on('close', (code) => {
console.log(`Python script finished with exit code: ${code}`); logToFile(`Python exited with code ${code}`);
if (pythonOutput.length > 500) {
console.log(`--- STDOUT (Truncated) ---`);
console.log(pythonOutput.substring(0, 500) + '...');
} else {
console.log(`--- STDOUT ---`);
console.log(pythonOutput);
}
if (pythonError) { // GLOBAL DUMP
console.log(`--- STDERR ---`); dumpToFile(`STDOUT:\n${pythonOutput}\n\nSTDERR:\n${pythonError}`);
console.log(pythonError);
}
console.log(`----------------`);
// Aufräumen
tempFilesToDelete.forEach(file => {
if (fs.existsSync(file)) {
try {
fs.unlinkSync(file);
} catch (e) {
console.error(`Failed to delete temp file ${file}:`, e.message);
}
}
});
if (code !== 0) { if (code !== 0) {
console.error(`Python script exited with error.`); console.error(`Python script exited with error.`);
@@ -65,19 +77,34 @@ const runPython = (args, res, tempFilesToDelete = []) => {
} }
try { try {
const result = JSON.parse(pythonOutput); // Versuche JSON zu parsen
// Suche nach dem ersten '{' und dem letzten '}', falls Müll davor/dahinter ist
let jsonString = pythonOutput;
const match = pythonOutput.match(/\{[\s\S]*\}/);
if (match) {
jsonString = match[0];
}
const result = JSON.parse(jsonString);
res.json(result); res.json(result);
} catch (parseError) { } catch (parseError) {
logToFile(`JSON Parse Error. Full Output dumped to server_dump.txt`);
console.error('Failed to parse Python output as JSON:', parseError); console.error('Failed to parse Python output as JSON:', parseError);
res.status(500).json({ error: 'Invalid JSON from Python script', rawOutput: pythonOutput, details: pythonError }); res.status(500).json({
error: 'Invalid JSON from Python script',
rawOutputSnippet: pythonOutput.substring(0, 200),
details: pythonError
});
} }
// Aufräumen
tempFilesToDelete.forEach(file => {
if (fs.existsSync(file)) try { fs.unlinkSync(file); } catch(e){}
});
}); });
pythonProcess.on('error', (err) => { pythonProcess.on('error', (err) => {
console.error('FATAL: Failed to start python process itself.', err); logToFile(`FATAL: Failed to start python process: ${err.message}`);
tempFilesToDelete.forEach(file => {
if (fs.existsSync(file)) fs.unlinkSync(file);
});
res.status(500).json({ error: 'Failed to start Python process', details: err.message }); res.status(500).json({ error: 'Failed to start Python process', details: err.message });
}); });
}; };
@@ -85,8 +112,8 @@ const runPython = (args, res, tempFilesToDelete = []) => {
// API-Endpunkt für generateSearchStrategy // API-Endpunkt für generateSearchStrategy
app.post('/api/generate-search-strategy', async (req, res) => { app.post('/api/generate-search-strategy', async (req, res) => {
console.log(`[${new Date().toISOString()}] HIT: /api/generate-search-strategy`); logToFile(`[${new Date().toISOString()}] HIT: /api/generate-search-strategy`);
const { referenceUrl, contextContent } = req.body; const { referenceUrl, contextContent, language } = req.body;
if (!referenceUrl || !contextContent) { if (!referenceUrl || !contextContent) {
return res.status(400).json({ error: 'Missing referenceUrl or contextContent' }); return res.status(400).json({ error: 'Missing referenceUrl or contextContent' });
@@ -100,7 +127,13 @@ app.post('/api/generate-search-strategy', async (req, res) => {
fs.writeFileSync(tempContextFilePath, contextContent); fs.writeFileSync(tempContextFilePath, contextContent);
const pythonScript = path.join(__dirname, '..', 'market_intel_orchestrator.py'); const pythonScript = path.join(__dirname, '..', 'market_intel_orchestrator.py');
const scriptArgs = [pythonScript, '--mode', 'generate_strategy', '--reference_url', referenceUrl, '--context_file', tempContextFilePath]; const scriptArgs = [
pythonScript,
'--mode', 'generate_strategy',
'--reference_url', referenceUrl,
'--context_file', tempContextFilePath,
'--language', language || 'de'
];
runPython(scriptArgs, res, [tempContextFilePath]); runPython(scriptArgs, res, [tempContextFilePath]);
@@ -112,8 +145,8 @@ app.post('/api/generate-search-strategy', async (req, res) => {
// API-Endpunkt für identifyCompetitors // API-Endpunkt für identifyCompetitors
app.post('/api/identify-competitors', async (req, res) => { app.post('/api/identify-competitors', async (req, res) => {
console.log(`[${new Date().toISOString()}] HIT: /api/identify-competitors`); logToFile(`[${new Date().toISOString()}] HIT: /api/identify-competitors`);
const { referenceUrl, targetMarket, contextContent, referenceCity, referenceCountry, summaryOfOffer } = req.body; const { referenceUrl, targetMarket, contextContent, referenceCity, referenceCountry, summaryOfOffer, language } = req.body;
if (!referenceUrl || !targetMarket) { if (!referenceUrl || !targetMarket) {
return res.status(400).json({ error: 'Missing referenceUrl or targetMarket' }); return res.status(400).json({ error: 'Missing referenceUrl or targetMarket' });
@@ -135,7 +168,8 @@ app.post('/api/identify-competitors', async (req, res) => {
pythonScript, pythonScript,
'--mode', 'identify_competitors', '--mode', 'identify_competitors',
'--reference_url', referenceUrl, '--reference_url', referenceUrl,
'--target_market', targetMarket '--target_market', targetMarket,
'--language', language || 'de'
]; ];
if (contextContent) scriptArgs.push('--context_file', tempContextFilePath); if (contextContent) scriptArgs.push('--context_file', tempContextFilePath);
@@ -152,8 +186,8 @@ app.post('/api/identify-competitors', async (req, res) => {
// API-Endpunkt für analyze-company (Deep Tech Audit) // API-Endpunkt für analyze-company (Deep Tech Audit)
app.post('/api/analyze-company', async (req, res) => { app.post('/api/analyze-company', async (req, res) => {
console.log(`[${new Date().toISOString()}] HIT: /api/analyze-company`); logToFile(`[${new Date().toISOString()}] HIT: /api/analyze-company`);
const { companyName, strategy, targetMarket } = req.body; const { companyName, strategy, targetMarket, language } = req.body;
if (!companyName || !strategy) { if (!companyName || !strategy) {
return res.status(400).json({ error: 'Missing companyName or strategy' }); return res.status(400).json({ error: 'Missing companyName or strategy' });
@@ -165,7 +199,8 @@ app.post('/api/analyze-company', async (req, res) => {
'--mode', 'analyze_company', '--mode', 'analyze_company',
'--company_name', companyName, '--company_name', companyName,
'--strategy_json', JSON.stringify(strategy), '--strategy_json', JSON.stringify(strategy),
'--target_market', targetMarket || 'Germany' '--target_market', targetMarket || 'Germany',
'--language', language || 'de'
]; ];
runPython(scriptArgs, res); runPython(scriptArgs, res);
@@ -173,12 +208,12 @@ app.post('/api/analyze-company', async (req, res) => {
// API-Endpunkt für generate-outreach // API-Endpunkt für generate-outreach
app.post('/api/generate-outreach', async (req, res) => { app.post('/api/generate-outreach', async (req, res) => {
console.log(`[${new Date().toISOString()}] HIT: /api/generate-outreach`); logToFile(`[${new Date().toISOString()}] HIT: /api/generate-outreach`);
// Set a long timeout for this specific route (5 minutes) // Set a long timeout for this specific route (5 minutes)
req.setTimeout(300000); req.setTimeout(300000);
const { companyData, knowledgeBase, referenceUrl, specific_role } = req.body; const { companyData, knowledgeBase, referenceUrl, specific_role, language } = req.body;
if (!companyData || !knowledgeBase) { if (!companyData || !knowledgeBase) {
return res.status(400).json({ error: 'Missing companyData or knowledgeBase' }); return res.status(400).json({ error: 'Missing companyData or knowledgeBase' });
@@ -199,7 +234,8 @@ app.post('/api/generate-outreach', async (req, res) => {
'--mode', 'generate_outreach', '--mode', 'generate_outreach',
'--company_data_file', tempDataFilePath, '--company_data_file', tempDataFilePath,
'--context_file', tempContextFilePath, '--context_file', tempContextFilePath,
'--reference_url', referenceUrl || '' '--reference_url', referenceUrl || '',
'--language', language || 'de'
]; ];
if (specific_role) { if (specific_role) {

View File

@@ -1,11 +1,9 @@
import { LeadStatus, AnalysisResult, Competitor, Language, Tier, EmailDraft, SearchStrategy, SearchSignal, OutreachResponse } from "../types"; import { LeadStatus, AnalysisResult, Competitor, Language, Tier, EmailDraft, SearchStrategy, SearchSignal, OutreachResponse } from "../types";
// URL Konfiguration: // URL Konfiguration:
// Im Production-Build (Docker/Nginx) nutzen wir den relativen Pfad '/api', da Nginx als Reverse Proxy fungiert. // Wir nutzen IMMER den relativen Pfad './api', damit Requests korrekt über den Nginx Proxy (/market/api/...) laufen.
// Im Development-Modus (lokal) greifen wir direkt auf den Port 3001 zu. // Direkter Zugriff auf Port 3001 ist im Docker-Deployment von außen nicht möglich.
const API_BASE_URL = (import.meta as any).env.PROD const API_BASE_URL = './api';
? 'api'
: `http://${window.location.hostname}:3001/api`;
// Helper to extract JSON (kann ggf. entfernt werden, wenn das Backend immer sauberes JSON liefert) // Helper to extract JSON (kann ggf. entfernt werden, wenn das Backend immer sauberes JSON liefert)
const extractJson = (text: string): any => { const extractJson = (text: string): any => {

View File

@@ -227,8 +227,7 @@ const App: React.FC = () => {
// Phase 6 Image Generation State // Phase 6 Image Generation State
const [generatingImages, setGeneratingImages] = useState<Record<number, boolean>>({}); const [generatingImages, setGeneratingImages] = useState<Record<number, boolean>>({});
const [generatedImages, setGeneratedImages] = useState<Record<number, string>>({}); const [generatedImages, setGeneratedImages] = useState<Record<number, string>>({});
const [aspectRatio, setAspectRatio] = useState('16:9');
// Phase 6 Editing State
const [editingIndex, setEditingIndex] = useState<number | null>(null); const [editingIndex, setEditingIndex] = useState<number | null>(null);
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const [isDrawing, setIsDrawing] = useState(false); const [isDrawing, setIsDrawing] = useState(false);
@@ -487,7 +486,7 @@ const App: React.FC = () => {
// If not sketching, use the array of uploaded product images // If not sketching, use the array of uploaded product images
const refImages = overrideReference ? [overrideReference] : state.productImages; const refImages = overrideReference ? [overrideReference] : state.productImages;
const imageUrl = await Gemini.generateConceptImage(prompt, refImages); const imageUrl = await Gemini.generateConceptImage(prompt, refImages, aspectRatio);
setGeneratedImages(prev => ({ ...prev, [index]: imageUrl })); setGeneratedImages(prev => ({ ...prev, [index]: imageUrl }));
// If we were editing, close edit mode // If we were editing, close edit mode
@@ -1699,7 +1698,18 @@ const App: React.FC = () => {
<h4 className="font-bold text-slate-800 dark:text-white">{prompt.title}</h4> <h4 className="font-bold text-slate-800 dark:text-white">{prompt.title}</h4>
<p className="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wide mt-1">{prompt.context}</p> <p className="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wide mt-1">{prompt.context}</p>
</div> </div>
<div className="flex gap-2"> <div className="flex items-center gap-2">
<select
value={aspectRatio}
onChange={e => setAspectRatio(e.target.value)}
className="text-xs font-bold rounded bg-white dark:bg-robo-800 border border-slate-200 dark:border-robo-600 hover:border-blue-400 dark:hover:border-robo-500 transition-colors text-slate-600 dark:text-slate-300 focus:ring-2 focus:ring-purple-500 outline-none"
>
<option value="16:9">16:9 (Landscape)</option>
<option value="9:16">9:16 (Portrait)</option>
<option value="1:1">1:1 (Square)</option>
<option value="4:3">4:3 (Classic)</option>
</select>
<button <button
onClick={() => handleGenerateImage(prompt.prompt, i)} onClick={() => handleGenerateImage(prompt.prompt, i)}
disabled={generatingImages[i]} disabled={generatingImages[i]}

View File

@@ -102,8 +102,8 @@ export const translateReportToEnglish = async (reportMarkdown: string): Promise<
return callApi<{ report: string }>('run', 'translate', { reportMarkdown }); return callApi<{ report: string }>('run', 'translate', { reportMarkdown });
}; };
export const generateConceptImage = async (prompt: string, referenceImagesBase64?: string[]): Promise<string> => { export const generateConceptImage = async (prompt: string, referenceImagesBase64?: string[], aspectRatio?: string): Promise<string> => {
const result = await callApi<{ imageBase64: string }>('run', 'image', { prompt, referenceImagesBase64 }); const result = await callApi<{ imageBase64: string }>('run', 'image', { prompt, referenceImagesBase64, aspectRatio });
return result.imageBase64; return result.imageBase64;
}; };

View File

@@ -1,4 +1,4 @@
# Dokumentation: GTM Architect Engine (v2.3) # Dokumentation: GTM Architect Engine (v2.4)
## 1. Projektübersicht ## 1. Projektübersicht
@@ -15,9 +15,10 @@ graph LR
User[Browser] -- HTTP/JSON --> Proxy[Nginx :8090] User[Browser] -- HTTP/JSON --> Proxy[Nginx :8090]
Proxy -- /gtm/ --> NodeJS[Node.js Server :3005] Proxy -- /gtm/ --> NodeJS[Node.js Server :3005]
NodeJS -- Spawn Process --> Python[Python Orchestrator] NodeJS -- Spawn Process --> Python[Python Orchestrator]
Python -- google.genai --> Gemini[Google Gemini 2.0 Flash (Text)] Python -- import --> Helpers[Core Engine (helpers.py)]
Python -- google.genai --> Imagen[Google Imagen 4.0 (Text-to-Image)] Helpers -- Dual SDK --> Gemini[Google Gemini 2.0 Flash (Text)]
Python -- google.genai --> GeminiImg[Google Gemini 2.5 Flash Image (Image-to-Image)] Helpers -- Dual SDK --> Imagen[Google Imagen 4.0 (Text-to-Image)]
Helpers -- Dual SDK --> GeminiImg[Google Gemini 2.5 Flash (Image-to-Image)]
Python -- SQL --> DB[(SQLite: gtm_projects.db)] Python -- SQL --> DB[(SQLite: gtm_projects.db)]
``` ```
@@ -25,24 +26,44 @@ graph LR
1. **Frontend (`/gtm-architect`):** 1. **Frontend (`/gtm-architect`):**
* Framework: **React** (Vite + TypeScript). * Framework: **React** (Vite + TypeScript).
* **Feature (NEU):** **Session History** (Sitzungsverlauf). Ermöglicht das Laden alter Projekte direkt aus der Datenbank. * Features: **Session History** (Laden/Löschen alter Projekte) und **Markdown Upload**.
* **Feature (NEU):** **Markdown Upload**. Ermöglicht das Importieren externer Report-Dateien (`.md`).
* **UI:** Verbesserte Tabelle für Strategie-Matrix.
2. **Backend Bridge (`server.cjs`):** 2. **Backend Bridge (`server.cjs`):**
* Runtime: **Node.js** (Express). * Runtime: **Node.js** (Express).
* Funktion: Nimmt HTTP-Requests entgegen und startet Python-Prozesse. * Funktion: Nimmt HTTP-Requests entgegen und startet Python-Prozesse (`gtm_architect_orchestrator.py`).
3. **Logic Core (`gtm_architect_orchestrator.py`):** 3. **Logic Core (`gtm_architect_orchestrator.py`):**
* Runtime: **Python 3.11+**. * Runtime: **Python 3.11+**.
* **Datenbank-Integration:** Vollständiger Support für CRUD-Operationen via `gtm_db_manager.py`. * Verantwortlichkeit: Steuert den 9-Phasen-Prozess, verwaltet Payloads und interagiert mit der Datenbank. Nutzt `helpers.py` für alle KI-Interaktionen.
* **Automatisierung:** Automatische Projekterstellung beim ersten Start (Phase 1) basierend auf dem Produktnamen.
* **Output-Sanitization:** Automatisches Entfernen von Markdown-Codefences (` ```markdown `), um korrektes Rendering im Frontend sicherzustellen.
4. **Persistenz (`gtm_projects.db`):** 4. **Core Engine (`helpers.py`):**
* Typ: **SQLite**. Speichert alle Phasen-Ergebnisse als JSON-Blobs. * Laufzeit: **Python 3.11+**.
* Verantwortlichkeit: Abstrahiert die Komplexität der KI-API-Aufrufe. Stellt robuste, wiederverwendbare Funktionen für Text- und Bildgenerierung bereit.
## 3. Der 9-Phasen Prozess 5. **Persistenz (`gtm_projects.db`):**
* Typ: **SQLite**. Speichert alle Phasen-Ergebnisse als JSON-Blobs in einer einzigen Tabelle.
## 3. Kernfunktionalität: Die AI Engine (`helpers.py`)
Das Herzstück des Systems ist die `helpers.py`-Bibliothek, die für Stabilität und Zukunftssicherheit konzipiert wurde.
### 3.1 Dual SDK Support
Um maximale Stabilität zu gewährleisten und gleichzeitig Zugriff auf die neuesten KI-Modelle zu haben, wird ein dualer Ansatz für die Google AI SDKs verfolgt:
* **`google-generativeai` (Legacy):** Wird bevorzugt für Text-Generierungs-Aufgaben (`gemini-2.0-flash`) verwendet, da es sich in diesem Setup als robuster erwiesen hat.
* **`google-genai` (Modern):** Wird für alle Bild-Generierungs-Aufgaben und als Fallback für die Text-Generierung genutzt.
### 3.2 Hybride Bildgenerierung
Die `call_gemini_image`-Funktion wählt automatisch die beste Methode basierend auf dem Input:
* **Szenario A: Text-to-Image (Kein Referenzbild)**
* **Modell:** `imagen-4.0-generate-001`.
* **Anwendung:** Generiert ein komplett neues Bild basierend auf einem textuellen Prompt (z.B. für Landingpage-Banner).
* **Szenario B: Image-to-Image (Mit Referenzbild)**
* **Modell:** `gemini-2.5-flash-image`.
* **Anwendung:** Platziert ein existierendes Produkt (via Upload) in eine neue, per Text beschriebene Szene. Der Prompt ist darauf optimiert, das Produktdesign nicht zu verändern.
## 4. Der 9-Phasen Prozess
| Phase | Modus | Input | Output | Beschreibung | | Phase | Modus | Input | Output | Beschreibung |
| :--- | :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- | :--- |
@@ -56,19 +77,24 @@ graph LR
| **8** | `phase8` | Phase 1, 2 | Business Case | CFO-Argumentation, ROI-Logik. | | **8** | `phase8` | Phase 1, 2 | Business Case | CFO-Argumentation, ROI-Logik. |
| **9** | `phase9` | Phase 1, 4 | Feature-to-Value | Übersetzung technischer Features in Nutzen. | | **9** | `phase9` | Phase 1, 4 | Feature-to-Value | Übersetzung technischer Features in Nutzen. |
## 4. Sitzungs-Management (NEU) ## 5. Sitzungs-Management
Das System verwaltet nun persistente Sitzungen: Das System verwaltet persistente Sitzungen in der SQLite-Datenbank:
* **List:** Abruf aller gespeicherten Projekte mit Zeitstempel. * **List:** Abruf aller gespeicherten Projekte mit Zeitstempel.
* **Load:** Vollständige Wiederherstellung des App-States (alle Phasen). * **Load:** Vollständige Wiederherstellung des App-States (alle Phasen).
* **Delete:** Permanentes Entfernen aus der Datenbank. * **Delete:** Permanentes Entfernen aus der Datenbank.
## 5. Deployment & Betrieb ## 6. Deployment & Betrieb
* **Wichtig:** Das Frontend wird im Build-Stage gebaut. Bei Änderungen an `App.tsx` muss der Container mit `docker-compose up -d --build gtm-app` neu gebaut werden. * **Wichtig:** Das Frontend wird im Build-Stage gebaut. Bei Änderungen an `App.tsx` muss der Container mit `docker-compose up -d --build gtm-app` neu gebaut werden.
* **Backend:** Änderungen an `gtm_architect_orchestrator.py` erfordern keinen Build, nur einen Restart (`docker restart gtm-app`). * **Backend:** Änderungen an `gtm_architect_orchestrator.py` oder `helpers.py` erfordern keinen Build, nur einen Restart (`docker restart gtm-app`).
## 6. Historie & Fixes (Jan 2026) ## 7. Historie & Fixes (Jan 2026)
* **[UPGRADE] v2.4:**
* Dokumentation der Kern-Engine (`helpers.py`) mit Dual SDK & Hybrid Image Generation.
* Aktualisierung der Architektur-Übersicht und Komponenten-Beschreibungen.
* Versionierung an den aktuellen Code-Stand (`v2.4.0`) angepasst.
* **[UPGRADE] v2.3:** * **[UPGRADE] v2.3:**
* Einführung der Session History (Datenbank-basiert). * Einführung der Session History (Datenbank-basiert).

View File

@@ -478,24 +478,22 @@ def translate(payload):
def image(payload): def image(payload):
prompt = payload.get('prompt', 'No Prompt') prompt = payload.get('prompt', 'No Prompt')
project_id = payload.get('projectId') project_id = payload.get('projectId')
aspect_ratio = payload.get('aspectRatio')
# Versuche, ein Referenzbild aus dem Payload zu holen (für Image-to-Image)
# Frontend sendet "referenceImagesBase64" (Array)
ref_images = payload.get('referenceImagesBase64') ref_images = payload.get('referenceImagesBase64')
ref_image = None ref_image = None
if ref_images and isinstance(ref_images, list) and len(ref_images) > 0: if ref_images and isinstance(ref_images, list) and len(ref_images) > 0:
ref_image = ref_images[0] ref_image = ref_images[0]
elif payload.get('referenceImage'): # Fallback für alte Calls elif payload.get('referenceImage'):
ref_image = payload.get('referenceImage') ref_image = payload.get('referenceImage')
log_and_save(project_id, "image", "prompt", prompt) log_and_save(project_id, "image", "prompt", f"{prompt} (Ratio: {aspect_ratio or 'default'})")
if ref_image: if ref_image:
logging.info(f"Image-Mode: Reference Image found (Length: {len(ref_image)})") logging.info(f"Image-Mode: Reference Image found (Length: {len(ref_image)})")
try: try:
# Aufruf mit optionalem Referenzbild image_b64 = call_gemini_image(prompt, reference_image_b64=ref_image, aspect_ratio=aspect_ratio)
image_b64 = call_gemini_image(prompt, reference_image_b64=ref_image)
log_and_save(project_id, "image", "response_b64_preview", image_b64[:100] + "...") log_and_save(project_id, "image", "response_b64_preview", image_b64[:100] + "...")
return {"imageBase64": f"data:image/png;base64,{image_b64}"} return {"imageBase64": f"data:image/png;base64,{image_b64}"}
except Exception as e: except Exception as e:

View File

@@ -256,11 +256,13 @@ def call_gemini_flash(prompt, system_instruction=None, temperature=0.3, json_mod
raise ImportError("Keine Gemini Bibliothek verfügbar.") raise ImportError("Keine Gemini Bibliothek verfügbar.")
@retry_on_failure @retry_on_failure
def call_gemini_image(prompt, reference_image_b64=None): def call_gemini_image(prompt, reference_image_b64=None, aspect_ratio=None):
""" """
Generiert ein Bild. Generiert ein Bild.
- Mit Referenzbild: Gemini 2.5 Flash Image. - Mit Referenzbild: Gemini 2.5 Flash Image.
- Ohne Referenzbild: Imagen 4.0. - Ohne Referenzbild: Imagen 4.0.
- NEU: Akzeptiert `aspect_ratio` (z.B. "16:9").
- NEU: Wendet einen zentralen Corporate Design Prompt an.
""" """
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
api_key = _get_gemini_api_key() api_key = _get_gemini_api_key()
@@ -277,7 +279,7 @@ def call_gemini_image(prompt, reference_image_b64=None):
except ImportError: except ImportError:
raise ImportError("Pillow (PIL) fehlt. Bitte 'pip install Pillow' ausführen.") raise ImportError("Pillow (PIL) fehlt. Bitte 'pip install Pillow' ausführen.")
logger.info("Start Image-to-Image Generation mit gemini-2.5-flash-image...") logger.info(f"Start Image-to-Image Generation mit gemini-2.5-flash-image. Seitenverhältnis: {aspect_ratio or 'default'}")
# Base64 zu PIL Image # Base64 zu PIL Image
try: try:
@@ -298,7 +300,11 @@ def call_gemini_image(prompt, reference_image_b64=None):
"Only adjust lighting and perspective to match the scene." "Only adjust lighting and perspective to match the scene."
) )
# KEIN config mit response_mime_type="application/json", das verursacht Fehler! # Hier können wir das Seitenverhältnis nicht direkt steuern,
# da es vom Referenzbild abhängt. Wir könnten es aber in den Prompt einbauen.
if aspect_ratio:
full_prompt += f" The final image composition should have an aspect ratio of {aspect_ratio}."
response = client.models.generate_content( response = client.models.generate_content(
model='gemini-2.5-flash-image', model='gemini-2.5-flash-image',
contents=[raw_image, full_prompt] contents=[raw_image, full_prompt]
@@ -315,8 +321,15 @@ def call_gemini_image(prompt, reference_image_b64=None):
else: else:
img_config = { img_config = {
"number_of_images": 1, "number_of_images": 1,
"output_mime_type": "image/jpeg" "output_mime_type": "image/jpeg",
} }
# Füge Seitenverhältnis hinzu, falls vorhanden
if aspect_ratio in ["16:9", "9:16", "1:1", "4:3"]:
img_config["aspect_ratio"] = aspect_ratio
logger.info(f"Seitenverhältnis auf {aspect_ratio} gesetzt.")
# Wende zentralen Stil an
final_prompt = f"{Config.CORPORATE_DESIGN_PROMPT}\n\nTask: {prompt}"
method = getattr(client.models, 'generate_images', None) method = getattr(client.models, 'generate_images', None)
if not method: if not method:
@@ -335,7 +348,7 @@ def call_gemini_image(prompt, reference_image_b64=None):
logger.info(f"Versuche Text-zu-Bild mit Modell: {model_name}") logger.info(f"Versuche Text-zu-Bild mit Modell: {model_name}")
response = method( response = method(
model=model_name, model=model_name,
prompt=prompt, prompt=final_prompt,
config=img_config config=img_config
) )

View File

@@ -173,8 +173,8 @@ def _extract_json_from_text(text):
logger.error(f"JSON Parsing fehlgeschlagen. Roher Text: {text[:500]}...") logger.error(f"JSON Parsing fehlgeschlagen. Roher Text: {text[:500]}...")
return None return None
def generate_search_strategy(reference_url, context_content): def generate_search_strategy(reference_url, context_content, language='de'):
logger.info(f"Generating strategy for {reference_url}") logger.info(f"Generating strategy for {reference_url} (Language: {language})")
api_key = load_gemini_api_key() api_key = load_gemini_api_key()
target_industries = _extract_target_industries_from_context(context_content) target_industries = _extract_target_industries_from_context(context_content)
@@ -186,6 +186,8 @@ def generate_search_strategy(reference_url, context_content):
# Switch to stable 2.5-pro model (which works for v1beta) # Switch to stable 2.5-pro model (which works for v1beta)
GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key={api_key}" GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key={api_key}"
lang_instruction = "GERMAN (Deutsch)" if language == 'de' else "ENGLISH"
prompt = f""" prompt = f"""
You are a B2B Market Intelligence Architect. You are a B2B Market Intelligence Architect.
@@ -223,6 +225,9 @@ def generate_search_strategy(reference_url, context_content):
- `searchQueryTemplate`: A Google search query to find this. Use `{{COMPANY}}` as a placeholder for the company name. - `searchQueryTemplate`: A Google search query to find this. Use `{{COMPANY}}` as a placeholder for the company name.
Example: `site:{{COMPANY}} "software engineer" OR "developer"` Example: `site:{{COMPANY}} "software engineer" OR "developer"`
--- LANGUAGE INSTRUCTION ---
IMPORTANT: The entire JSON content (descriptions, rationale, summaries) MUST be in {lang_instruction}. Translate if necessary.
--- OUTPUT FORMAT --- --- OUTPUT FORMAT ---
Return ONLY a valid JSON object. Return ONLY a valid JSON object.
{{ {{
@@ -267,12 +272,14 @@ def generate_search_strategy(reference_url, context_content):
"signals": [] "signals": []
} }
def identify_competitors(reference_url, target_market, industries, summary_of_offer=None): def identify_competitors(reference_url, target_market, industries, summary_of_offer=None, language='de'):
logger.info(f"Identifying competitors for {reference_url}") logger.info(f"Identifying competitors for {reference_url} (Language: {language})")
api_key = load_gemini_api_key() api_key = load_gemini_api_key()
# Switch to stable 2.5-pro model # Switch to stable 2.5-pro model
GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key={api_key}" GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key={api_key}"
lang_instruction = "GERMAN (Deutsch)" if language == 'de' else "ENGLISH"
prompt = f""" prompt = f"""
You are a B2B Market Analyst. Find 3-5 direct competitors or highly similar companies (lookalikes) for the company at `{reference_url}`. You are a B2B Market Analyst. Find 3-5 direct competitors or highly similar companies (lookalikes) for the company at `{reference_url}`.
@@ -295,6 +302,9 @@ def identify_competitors(reference_url, target_market, industries, summary_of_of
- `name`: The official, full name of the company. - `name`: The official, full name of the company.
- `description`: A concise explanation of why they are a competitor. - `description`: A concise explanation of why they are a competitor.
--- LANGUAGE INSTRUCTION ---
IMPORTANT: The entire JSON content (descriptions) MUST be in {lang_instruction}.
--- OUTPUT FORMAT --- --- OUTPUT FORMAT ---
Return ONLY a valid JSON object with the following structure: Return ONLY a valid JSON object with the following structure:
{{ {{
@@ -325,11 +335,14 @@ def identify_competitors(reference_url, target_market, industries, summary_of_of
logger.error(f"Competitor identification failed: {e}") logger.error(f"Competitor identification failed: {e}")
return {"localCompetitors": [], "nationalCompetitors": [], "internationalCompetitors": []} return {"localCompetitors": [], "nationalCompetitors": [], "internationalCompetitors": []}
def analyze_company(company_name, strategy, target_market): def analyze_company(company_name, strategy, target_market, language='de'):
logger.info(f"--- STARTING DEEP TECH AUDIT FOR: {company_name} ---") logger.info(f"--- STARTING DEEP TECH AUDIT FOR: {company_name} (Language: {language}) ---")
api_key = load_gemini_api_key() api_key = load_gemini_api_key()
GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key={api_key}" GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key={api_key}"
lang_instruction = "GERMAN (Deutsch)" if language == 'de' else "ENGLISH"
# ... (Rest of function logic remains same, just update prompt) ...
# 1. Website Finding (SerpAPI fallback to Gemini) # 1. Website Finding (SerpAPI fallback to Gemini)
url = None url = None
website_search_results = serp_search(f"{company_name} offizielle Website") website_search_results = serp_search(f"{company_name} offizielle Website")
@@ -343,24 +356,16 @@ def analyze_company(company_name, strategy, target_market):
prompt_url = f"What is the official homepage URL for the company '{company_name}' in the market '{target_market}'? Respond with ONLY the single, complete URL and nothing else." prompt_url = f"What is the official homepage URL for the company '{company_name}' in the market '{target_market}'? Respond with ONLY the single, complete URL and nothing else."
payload_url = {"contents": [{"parts": [{"text": prompt_url}]}]} payload_url = {"contents": [{"parts": [{"text": prompt_url}]}]}
logger.info("Sende Anfrage an Gemini API (URL Fallback)...") logger.info("Sende Anfrage an Gemini API (URL Fallback)...")
# logger.debug(f"Rohe Gemini API-Anfrage (JSON): {json.dumps(payload_url, indent=2)}")
try: try:
res = requests.post(GEMINI_API_URL, json=payload_url, headers={'Content-Type': 'application/json'}, timeout=15) res = requests.post(GEMINI_API_URL, json=payload_url, headers={'Content-Type': 'application/json'}, timeout=15)
res.raise_for_status() res.raise_for_status()
res_json = res.json() res_json = res.json()
logger.info(f"Gemini API-Antwort erhalten (Status: {res.status_code}).")
candidate = res_json.get('candidates', [{}])[0] candidate = res_json.get('candidates', [{}])[0]
content = candidate.get('content', {}).get('parts', [{}])[0] content = candidate.get('content', {}).get('parts', [{}])[0]
text_response = content.get('text', '').strip() text_response = content.get('text', '').strip()
url_match = re.search(r'(https?://[^\s"]+)', text_response) url_match = re.search(r'(https?://[^\s"]+)', text_response)
if url_match: if url_match:
url = url_match.group(1) url = url_match.group(1)
logger.info(f"Gemini Fallback hat URL gefunden: {url}")
else:
logger.warning(f"Keine gültige URL in Gemini-Antwort gefunden: '{text_response}'")
except Exception as e: except Exception as e:
logger.error(f"Gemini URL Fallback failed: {e}") logger.error(f"Gemini URL Fallback failed: {e}")
pass pass
@@ -368,7 +373,6 @@ def analyze_company(company_name, strategy, target_market):
if not url or not url.startswith("http"): if not url or not url.startswith("http"):
return {"error": f"Could not find website for {company_name}"} return {"error": f"Could not find website for {company_name}"}
# 2. Homepage Scraping with GRACEFUL FALLBACK
homepage_text = "" homepage_text = ""
scraping_note = "" scraping_note = ""
@@ -377,86 +381,48 @@ def analyze_company(company_name, strategy, target_market):
if scraped_content: if scraped_content:
homepage_text = scraped_content homepage_text = scraped_content
else: else:
homepage_text = "[WEBSITE ACCESS DENIED] - The audit must rely on external search signals (Tech Stack, Job Postings, News) as the homepage content is unavailable." homepage_text = "[WEBSITE ACCESS DENIED]"
scraping_note = "(Website Content Unavailable - Analysis based on Digital Footprint)" scraping_note = "(Website Content Unavailable)"
logger.warning(f"Audit continuing without website content for {company_name}")
else: else:
homepage_text = "No valid URL found. Analysis based on Name ONLY." homepage_text = "No valid URL found."
scraping_note = "(No URL found)" scraping_note = "(No URL found)"
# --- ENHANCED: EXTERNAL TECHNOGRAPHIC INTELLIGENCE ---
# Suche aktiv nach Wettbewerbern, nicht nur auf der Firmenwebsite.
tech_evidence = [] tech_evidence = []
known_incumbents = ["SAP Ariba", "Jaggaer", "Coupa", "SynerTrade", "Ivalua", "ServiceNow", "Salesforce", "Oracle SCM", "Zycus", "GEP", "SupplyOn", "EcoVadis", "IntegrityNext"]
# Liste bekannter Wettbewerber / Incumbents
known_incumbents = [
"SAP Ariba", "Jaggaer", "Coupa", "SynerTrade", "Ivalua",
"ServiceNow", "Salesforce", "Oracle SCM", "Zycus", "GEP",
"SupplyOn", "EcoVadis", "IntegrityNext"
]
# Suche 1: Direkte Verbindung zu Software-Anbietern (Case Studies, News, etc.)
# Wir bauen eine Query mit OR, um API-Calls zu sparen.
# Splitte in 2 Gruppen, um Query-Länge im Rahmen zu halten
half = len(known_incumbents) // 2 half = len(known_incumbents) // 2
group1 = " OR ".join([f'"{inc}"' for inc in known_incumbents[:half]]) group1 = " OR ".join([f'"{inc}"' for inc in known_incumbents[:half]])
group2 = " OR ".join([f'"{inc}"' for inc in known_incumbents[half:]]) group2 = " OR ".join([f'"{inc}"' for inc in known_incumbents[half:]])
tech_queries = [f'"{company_name}" ({group1})', f'"{company_name}" ({group2})', f'"{company_name}" "supplier portal" login']
tech_queries = [
f'"{company_name}" ({group1})',
f'"{company_name}" ({group2})',
f'"{company_name}" "supplier portal" login' # Suche nach dem Portal selbst
]
logger.info(f"Starte erweiterte Tech-Stack-Suche für {company_name}...")
for q in tech_queries: for q in tech_queries:
logger.info(f"Tech Search: {q}") results = serp_search(q, num_results=4)
results = serp_search(q, num_results=4) # Etwas mehr Ergebnisse
if results: if results:
for r in results: for r in results:
tech_evidence.append(f"- Found: {r['title']}\n Snippet: {r['snippet']}\n Link: {r['link']}") tech_evidence.append(f"- Found: {r['title']}\n Snippet: {r['snippet']}\n Link: {r['link']}")
tech_evidence_text = "\n".join(tech_evidence) tech_evidence_text = "\n".join(tech_evidence)
# --- END ENHANCED TECH SEARCH ---
# 3. Targeted Signal Search (The "Hunter" Phase) - Basierend auf Strategy
signal_evidence = [] signal_evidence = []
# Firmographics Search
firmographics_results = serp_search(f"{company_name} Umsatz Mitarbeiterzahl 2023") firmographics_results = serp_search(f"{company_name} Umsatz Mitarbeiterzahl 2023")
firmographics_context = "\n".join([f"- {r['snippet']} ({r['link']})" for r in firmographics_results]) firmographics_context = "\n".join([f"- {r['snippet']} ({r['link']})" for r in firmographics_results])
# Signal Searches (Original Strategy)
signals = strategy.get('signals', []) signals = strategy.get('signals', [])
for signal in signals: for signal in signals:
# Überspringe Signale, die wir schon durch die Tech-Suche massiv abgedeckt haben, if "incumbent" in signal['id'].lower() or "tech" in signal['id'].lower(): continue
# es sei denn, sie sind sehr spezifisch.
if "incumbent" in signal['id'].lower() or "tech" in signal['id'].lower():
logger.info(f"Skipping generic signal search '{signal['name']}' in favor of Enhanced Tech Search.")
continue
proof_strategy = signal.get('proofStrategy', {}) proof_strategy = signal.get('proofStrategy', {})
query_template = proof_strategy.get('searchQueryTemplate') query_template = proof_strategy.get('searchQueryTemplate')
search_context = "" search_context = ""
if query_template: if query_template:
try: try:
domain = url.split("//")[-1].split("/")[0].replace("www.", "") domain = url.split("//")[-1].split("/")[0].replace("www.", "")
except: except:
domain = "" domain = ""
query = query_template.replace("{{COMPANY}}", company_name).replace("{COMPANY}", company_name).replace("{{domain}}", domain).replace("{domain}", domain)
query = query_template.replace("{{COMPANY}}", company_name).replace("{COMPANY}", company_name)
query = query.replace("{{domain}}", domain).replace("{domain}", domain)
logger.info(f"Signal Search '{signal['name']}': {query}")
results = serp_search(query, num_results=3) results = serp_search(query, num_results=3)
if results: if results:
search_context = "\n".join([f" * Snippet: {r['snippet']}\n Source: {r['link']}" for r in results]) search_context = "\n".join([f" * Snippet: {r['snippet']}\n Source: {r['link']}" for r in results])
if search_context: if search_context:
signal_evidence.append(f"SIGNAL '{signal['name']}':\n{search_context}") signal_evidence.append(f"SIGNAL '{signal['name']}':\n{search_context}")
# 4. Final Analysis & Synthesis (The "Judge" Phase)
evidence_text = "\n\n".join(signal_evidence) evidence_text = "\n\n".join(signal_evidence)
prompt = f""" prompt = f"""
@@ -484,17 +450,18 @@ def analyze_company(company_name, strategy, target_market):
1. **Firmographics**: Estimate Revenue and Employees. 1. **Firmographics**: Estimate Revenue and Employees.
2. **Technographic Audit**: Look for specific competitor software or legacy systems mentioned in EVIDENCE 1 (e.g., "Partner of SynerTrade", "Login to Jaggaer Portal"). 2. **Technographic Audit**: Look for specific competitor software or legacy systems mentioned in EVIDENCE 1 (e.g., "Partner of SynerTrade", "Login to Jaggaer Portal").
3. **Status**: 3. **Status**:
- Set to "Nutzt Wettbewerber" if ANY competitor technology is found (Ariba, Jaggaer, SynerTrade, Coupa, etc.). - Set to "Nutzt Wettbewerber" if ANY competitor technology is found.
- Set to "Greenfield" ONLY if absolutely no competitor tech is found. - Set to "Greenfield" ONLY if absolutely no competitor tech is found.
- Set to "Bestandskunde" if they already use our solution. - Set to "Bestandskunde" if they already use our solution.
4. **Evaluate Signals**: For each signal, provide a "value" (Yes/No/Partial) and "proof". 4. **Evaluate Signals**: For each signal, provide a "value" (Yes/No/Partial) and "proof".
- NOTE: If Homepage Content is unavailable, rely on Evidence 1, 3, and 4.
5. **Recommendation (Pitch Strategy)**: 5. **Recommendation (Pitch Strategy)**:
- DO NOT write a generic verdict. - If they use a competitor, explain how to position against it.
- If they use a competitor (e.g., Ariba), explain how to position against it (e.g., "Pitch as a specialized add-on for logistics, filling Ariba's gaps").
- If Greenfield, explain the entry point. - If Greenfield, explain the entry point.
- **Tone**: Strategic, insider-knowledge, specific. - **Tone**: Strategic, insider-knowledge, specific.
--- LANGUAGE INSTRUCTION ---
IMPORTANT: The entire JSON content (especially 'recommendation', 'proof', 'value') MUST be in {lang_instruction}.
STRICTLY output only JSON: STRICTLY output only JSON:
{{ {{
"companyName": "{company_name}", "companyName": "{company_name}",
@@ -516,7 +483,6 @@ def analyze_company(company_name, strategy, target_market):
try: try:
logger.info("Sende Audit-Anfrage an Gemini API...") logger.info("Sende Audit-Anfrage an Gemini API...")
# logger.debug(f"Rohe Gemini API-Anfrage (JSON): {json.dumps(payload, indent=2)}")
response = requests.post(GEMINI_API_URL, json=payload, headers={'Content-Type': 'application/json'}) response = requests.post(GEMINI_API_URL, json=payload, headers={'Content-Type': 'application/json'})
response.raise_for_status() response.raise_for_status()
response_data = response.json() response_data = response.json()
@@ -529,32 +495,32 @@ def analyze_company(company_name, strategy, target_market):
raise ValueError("Konnte kein valides JSON extrahieren") raise ValueError("Konnte kein valides JSON extrahieren")
result['dataSource'] = "Digital Trace Audit (Deep Dive)" result['dataSource'] = "Digital Trace Audit (Deep Dive)"
logger.info(f"Audit für {company_name} erfolgreich abgeschlossen.")
return result return result
except Exception as e: except Exception as e:
logger.error(f"Audit failed for {company_name}: {e}") logger.error(f"Audit failed for {company_name}: {e}")
return { return {
"companyName": company_name, "companyName": company_name,
"status": "Unklar / Manuelle Prüfung", "status": "Unklar",
"revenue": "Error", "revenue": "Error",
"employees": "Error", "employees": "Error",
"tier": "Tier 3", "tier": "Tier 3",
"dynamicAnalysis": {}, "dynamicAnalysis": {},
"recommendation": f"Audit failed due to API Error: {str(e)}", "recommendation": f"Audit failed: {str(e)}",
"dataSource": "Error" "dataSource": "Error"
} }
def generate_outreach_campaign(company_data_json, knowledge_base_content, reference_url, specific_role=None): def generate_outreach_campaign(company_data_json, knowledge_base_content, reference_url, specific_role=None, language='de'):
""" """
Erstellt personalisierte E-Mail-Kampagnen. Erstellt personalisierte E-Mail-Kampagnen.
""" """
company_name = company_data_json.get('companyName', 'Unknown') company_name = company_data_json.get('companyName', 'Unknown')
logger.info(f"--- STARTING OUTREACH GENERATION FOR: {company_name} (Role: {specific_role if specific_role else 'Top 5'}) ---") logger.info(f"--- STARTING OUTREACH GENERATION FOR: {company_name} (Role: {specific_role if specific_role else 'Top 5'}) [Lang: {language}] ---")
api_key = load_gemini_api_key() api_key = load_gemini_api_key()
# Back to high-quality 2.5-pro, but generating only 1 campaign to be fast
GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key={api_key}" GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key={api_key}"
lang_instruction = "GERMAN (Deutsch)" if language == 'de' else "ENGLISH"
if specific_role: if specific_role:
# --- MODE B: SINGLE ROLE GENERATION (On Demand) --- # --- MODE B: SINGLE ROLE GENERATION (On Demand) ---
task_description = f""" task_description = f"""
@@ -573,7 +539,6 @@ def generate_outreach_campaign(company_data_json, knowledge_base_content, refere
""" """
else: else:
# --- MODE A: INITIAL START (TOP 1 + SUGGESTIONS) --- # --- MODE A: INITIAL START (TOP 1 + SUGGESTIONS) ---
# We only generate 1 campaign to ensure the request finishes quickly (< 20s).
task_description = f""" task_description = f"""
--- TASK --- --- TASK ---
1. **Analyze**: Match the Target Company (Input 2) to the most relevant 'Zielbranche/Segment' from the Knowledge Base (Input 1). 1. **Analyze**: Match the Target Company (Input 2) to the most relevant 'Zielbranche/Segment' from the Knowledge Base (Input 1).
@@ -616,12 +581,7 @@ def generate_outreach_campaign(company_data_json, knowledge_base_content, refere
--- TONE & STYLE GUIDELINES (CRITICAL) --- --- TONE & STYLE GUIDELINES (CRITICAL) ---
- **Perspective:** Operational Expert & Insider. NOT generic marketing. - **Perspective:** Operational Expert & Insider. NOT generic marketing.
- **Be Gritty & Specific:** Use hard, operational keywords from the Knowledge Base (e.g., "ASNs", "8D-Reports"). - **Be Gritty & Specific:** Use hard, operational keywords from the Knowledge Base (e.g., "ASNs", "8D-Reports").
- **Narrative Arc:** - **Language:** {lang_instruction}.
1. "I noticed [Fact from Audit]..."
2. "In [Industry], this often leads to [Pain]..."
3. "We helped [Reference Client] solve this..."
4. "Let's discuss [Gain]."
- **Language:** German.
{output_format} {output_format}
""" """
@@ -659,26 +619,36 @@ def main():
parser.add_argument("--strategy_json") parser.add_argument("--strategy_json")
parser.add_argument("--summary_of_offer") parser.add_argument("--summary_of_offer")
parser.add_argument("--company_data_file") parser.add_argument("--company_data_file")
parser.add_argument("--specific_role") # New argument parser.add_argument("--specific_role")
parser.add_argument("--language", default="de") # New Argument
args = parser.parse_args() args = parser.parse_args()
if args.mode == "generate_strategy": if args.mode == "generate_strategy":
with open(args.context_file, "r") as f: context = f.read() with open(args.context_file, "r") as f: context = f.read()
print(json.dumps(generate_search_strategy(args.reference_url, context))) print(json.dumps(generate_search_strategy(args.reference_url, context, args.language)))
elif args.mode == "identify_competitors": elif args.mode == "identify_competitors":
industries = [] industries = []
if args.context_file: if args.context_file:
with open(args.context_file, "r") as f: context = f.read() with open(args.context_file, "r") as f: context = f.read()
industries = _extract_target_industries_from_context(context) industries = _extract_target_industries_from_context(context)
print(json.dumps(identify_competitors(args.reference_url, args.target_market, industries, args.summary_of_offer))) print(json.dumps(identify_competitors(args.reference_url, args.target_market, industries, args.summary_of_offer, args.language)))
elif args.mode == "analyze_company": elif args.mode == "analyze_company":
strategy = json.loads(args.strategy_json) strategy = json.loads(args.strategy_json)
print(json.dumps(analyze_company(args.company_name, strategy, args.target_market))) print(json.dumps(analyze_company(args.company_name, strategy, args.target_market, args.language)))
elif args.mode == "generate_outreach": elif args.mode == "generate_outreach":
with open(args.company_data_file, "r") as f: company_data = json.load(f) with open(args.company_data_file, "r") as f: company_data = json.load(f)
with open(args.context_file, "r") as f: knowledge_base = f.read() with open(args.context_file, "r") as f: knowledge_base = f.read()
print(json.dumps(generate_outreach_campaign(company_data, knowledge_base, args.reference_url, args.specific_role))) print(json.dumps(generate_outreach_campaign(company_data, knowledge_base, args.reference_url, args.specific_role, args.language)))
if __name__ == "__main__": if __name__ == "__main__":
sys.stdout.reconfigure(encoding='utf-8')
try:
main() main()
sys.stdout.flush()
except Exception as e:
logger.critical(f"Unhandled Exception in Main: {e}", exc_info=True)
# Fallback JSON output so the server doesn't crash on parse error
error_json = json.dumps({"error": f"Critical Script Error: {str(e)}", "details": "Check market_intel.log"})
print(error_json)
sys.exit(1)