feat(gtm): add aspect ratio & corporate design; fix(market): harden backend logging & json parsing
This commit is contained in:
13
GEMINI.md
13
GEMINI.md
@@ -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:**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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]}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
23
helpers.py
23
helpers.py
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user