feat(gtm-architect): Integrate GTM Architect app with Python backend, DB persistence, and Docker stack
This commit is contained in:
@@ -173,6 +173,106 @@ const App: React.FC = () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- IMPORT MARKDOWN ---
|
||||||
|
const handleImportMarkdown = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const content = e.target?.result as string;
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newAnalysisData: Partial<AnalysisData> = {};
|
||||||
|
|
||||||
|
// Helper to parse table from a section
|
||||||
|
const parseSection = (sectionTitle: string): string[][] => {
|
||||||
|
const sectionRegex = new RegExp(`## ${sectionTitle}[\\s\\S]*?(?=## |$)`, 'i');
|
||||||
|
const match = content.match(sectionRegex);
|
||||||
|
if (!match) return [];
|
||||||
|
|
||||||
|
const lines = match[0].split('\n');
|
||||||
|
const rows: string[][] = [];
|
||||||
|
let inTable = false;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim().startsWith('|') && !line.includes('---')) {
|
||||||
|
const cells = line.split('|').map(c => c.trim()).filter(c => c);
|
||||||
|
if (cells.length > 0) {
|
||||||
|
if (!inTable) {
|
||||||
|
// Skip header row if we are just starting (assuming we have default headers or don't need them for raw data)
|
||||||
|
// Actually, we usually want data rows. The first row in MD table is header.
|
||||||
|
// Let's rely on standard markdown table structure: Header | Separator | Data
|
||||||
|
inTable = true;
|
||||||
|
} else {
|
||||||
|
rows.push(cells);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove the header row if captured (simple heuristic: first row is usually header)
|
||||||
|
if (rows.length > 0) rows.shift();
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mapping MD titles to keys
|
||||||
|
// Note: flexible matching for DE/EN titles
|
||||||
|
newAnalysisData.offer = {
|
||||||
|
summary: [],
|
||||||
|
headers: t.stepTitles.offer.includes('Schritt') ? ["Produkt/Lösung", "Beschreibung", "Kernfunktionen", "Differenzierung", "Quelle"] : ["Product/Solution", "Description", "Core Features", "Differentiation", "Source"],
|
||||||
|
rows: parseSection("Schritt 1")
|
||||||
|
};
|
||||||
|
if (newAnalysisData.offer.rows.length === 0) newAnalysisData.offer.rows = parseSection("Step 1");
|
||||||
|
|
||||||
|
newAnalysisData.targetGroups = {
|
||||||
|
summary: [],
|
||||||
|
headers: t.stepTitles.targetGroups.includes('Schritt') ? ["Zielbranche", "Merkmale", "Region", "Quelle"] : ["Target Industry", "Characteristics", "Region", "Source"],
|
||||||
|
rows: parseSection("Schritt 2")
|
||||||
|
};
|
||||||
|
if (newAnalysisData.targetGroups.rows.length === 0) newAnalysisData.targetGroups.rows = parseSection("Step 2");
|
||||||
|
|
||||||
|
newAnalysisData.personas = {
|
||||||
|
summary: [],
|
||||||
|
headers: [],
|
||||||
|
rows: parseSection("Schritt 3")
|
||||||
|
};
|
||||||
|
if (newAnalysisData.personas.rows.length === 0) newAnalysisData.personas.rows = parseSection("Step 3");
|
||||||
|
|
||||||
|
newAnalysisData.painPoints = {
|
||||||
|
summary: [],
|
||||||
|
headers: [],
|
||||||
|
rows: parseSection("Schritt 4")
|
||||||
|
};
|
||||||
|
if (newAnalysisData.painPoints.rows.length === 0) newAnalysisData.painPoints.rows = parseSection("Step 4");
|
||||||
|
|
||||||
|
newAnalysisData.gains = {
|
||||||
|
summary: [],
|
||||||
|
headers: [],
|
||||||
|
rows: parseSection("Schritt 5")
|
||||||
|
};
|
||||||
|
if (newAnalysisData.gains.rows.length === 0) newAnalysisData.gains.rows = parseSection("Step 5");
|
||||||
|
|
||||||
|
newAnalysisData.messages = {
|
||||||
|
summary: [],
|
||||||
|
headers: [],
|
||||||
|
rows: parseSection("Schritt 6")
|
||||||
|
};
|
||||||
|
if (newAnalysisData.messages.rows.length === 0) newAnalysisData.messages.rows = parseSection("Step 6");
|
||||||
|
|
||||||
|
|
||||||
|
setAnalysisData(newAnalysisData);
|
||||||
|
setGenerationStep(6); // Jump to end
|
||||||
|
setProjectName(file.name.replace('.md', ' (Imported)'));
|
||||||
|
setProjectId(null); // Treat as new project
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Parse error", err);
|
||||||
|
setError("Fehler beim Importieren der Datei.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
const handleStartGeneration = useCallback(async () => {
|
const handleStartGeneration = useCallback(async () => {
|
||||||
if (!inputData.companyUrl) {
|
if (!inputData.companyUrl) {
|
||||||
@@ -357,7 +457,14 @@ const App: React.FC = () => {
|
|||||||
const step = analysisData[stepKey] as AnalysisStep | undefined;
|
const step = analysisData[stepKey] as AnalysisStep | undefined;
|
||||||
if (!step) return null;
|
if (!step) return null;
|
||||||
|
|
||||||
|
// Allow manual addition for Offer (Step 1) and Target Groups (Step 2)
|
||||||
|
const canAdd = ['offer', 'targetGroups'].includes(stepKey);
|
||||||
const canDelete = ['offer', 'targetGroups', 'personas'].includes(stepKey);
|
const canDelete = ['offer', 'targetGroups', 'personas'].includes(stepKey);
|
||||||
|
|
||||||
|
const handleManualAdd = (newRow: string[]) => {
|
||||||
|
const currentRows = step.rows || [];
|
||||||
|
handleDataChange(stepKey, { ...step, rows: [...currentRows, newRow] });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StepDisplay
|
<StepDisplay
|
||||||
@@ -367,8 +474,8 @@ const App: React.FC = () => {
|
|||||||
headers={step.headers}
|
headers={step.headers}
|
||||||
rows={step.rows}
|
rows={step.rows}
|
||||||
onDataChange={(newRows) => handleDataChange(stepKey, { ...step, rows: newRows })}
|
onDataChange={(newRows) => handleDataChange(stepKey, { ...step, rows: newRows })}
|
||||||
canAddRows={false} // Disabled enrich functionality
|
canAddRows={canAdd}
|
||||||
onEnrichRow={undefined}
|
onEnrichRow={canAdd ? handleManualAdd : undefined}
|
||||||
isEnriching={false}
|
isEnriching={false}
|
||||||
canDeleteRows={canDelete}
|
canDeleteRows={canDelete}
|
||||||
t={t}
|
t={t}
|
||||||
@@ -456,6 +563,17 @@ const App: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-6 flex justify-center gap-3">
|
<div className="mt-6 flex justify-center gap-3">
|
||||||
|
<label className="flex items-center px-4 py-2 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 rounded-lg text-sm font-bold transition-all shadow-sm border border-slate-200 dark:border-slate-700 cursor-pointer">
|
||||||
|
<FolderOpen className="mr-2 h-4 w-4 text-orange-500" />
|
||||||
|
{inputData.language === 'de' ? 'MD Laden' : 'Load MD'}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".md"
|
||||||
|
onChange={handleImportMarkdown}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowHistory(true)}
|
onClick={() => setShowHistory(true)}
|
||||||
className="flex items-center px-4 py-2 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 rounded-lg text-sm font-bold transition-all shadow-sm border border-slate-200 dark:border-slate-700"
|
className="flex items-center px-4 py-2 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 rounded-lg text-sm font-bold transition-all shadow-sm border border-slate-200 dark:border-slate-700"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const path = require('path');
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3002;
|
const PORT = 3002;
|
||||||
|
const VERSION = "1.1.0-DEBUG (Timeout 600s)";
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
@@ -27,14 +28,32 @@ const runPythonScript = (args, res) => {
|
|||||||
pythonProcess.stderr.on('data', (data) => { pythonError += data.toString(); });
|
pythonProcess.stderr.on('data', (data) => { pythonError += data.toString(); });
|
||||||
|
|
||||||
pythonProcess.on('close', (code) => {
|
pythonProcess.on('close', (code) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] Python process exited with code ${code}`);
|
||||||
|
if (pythonOutput.length > 0) {
|
||||||
|
console.log(`[${new Date().toISOString()}] Stdout (first 500 chars): ${pythonOutput.substring(0, 500)}...`);
|
||||||
|
} else {
|
||||||
|
console.log(`[${new Date().toISOString()}] Stdout is empty.`);
|
||||||
|
}
|
||||||
|
if (pythonError.length > 0) {
|
||||||
|
console.log(`[${new Date().toISOString()}] Stderr (first 500 chars): ${pythonError.substring(0, 500)}...`);
|
||||||
|
}
|
||||||
|
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
console.error(`Python error (Code ${code}): ${pythonError}`);
|
console.error(`Python error (Code ${code}): ${pythonError}`);
|
||||||
return res.status(500).json({ error: 'Backend error', details: pythonError });
|
return res.status(500).json({ error: 'Backend error', details: pythonError, version: VERSION });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
res.json(JSON.parse(pythonOutput));
|
res.header('X-Server-Timeout', '600000');
|
||||||
|
const responseData = JSON.parse(pythonOutput);
|
||||||
|
// Add version info to the response if it's an object
|
||||||
|
if (typeof responseData === 'object' && responseData !== null) {
|
||||||
|
responseData._backend_version = VERSION;
|
||||||
|
}
|
||||||
|
res.json(responseData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: 'Invalid JSON', raw: pythonOutput });
|
console.error(`JSON Parse Error: ${e.message}`);
|
||||||
|
console.error(`Raw Output was: ${pythonOutput}`);
|
||||||
|
res.status(500).json({ error: 'Invalid JSON', raw: pythonOutput, details: e.message, version: VERSION });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -104,7 +123,12 @@ const initializeDatabase = () => {
|
|||||||
proc.on('close', (code) => console.log(`[${new Date().toISOString()}] DB init finished (Code ${code})`));
|
proc.on('close', (code) => console.log(`[${new Date().toISOString()}] DB init finished (Code ${code})`));
|
||||||
};
|
};
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
const server = app.listen(PORT, () => {
|
||||||
console.log(`B2B Marketing Assistant API Bridge running on port ${PORT}`);
|
console.log(`B2B Marketing Assistant API Bridge running on port ${PORT} (Version: ${VERSION})`);
|
||||||
initializeDatabase();
|
initializeDatabase();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set timeout to 10 minutes (600s) to handle long AI generation steps
|
||||||
|
server.setTimeout(600000);
|
||||||
|
server.keepAliveTimeout = 610000;
|
||||||
|
server.headersTimeout = 620000;
|
||||||
|
|||||||
@@ -141,6 +141,17 @@
|
|||||||
<!-- WICHTIG: Relativer Link für Reverse Proxy -->
|
<!-- WICHTIG: Relativer Link für Reverse Proxy -->
|
||||||
<a href="/market/" class="btn">Starten →</a>
|
<a href="/market/" class="btn">Starten →</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- GTM Architect -->
|
||||||
|
<div class="card">
|
||||||
|
<span class="card-icon">🏛️</span>
|
||||||
|
<h2>GTM Architect</h2>
|
||||||
|
<p>
|
||||||
|
Entwickelt eine komplette Go-to-Market-Strategie für neue technische Produkte, von der Analyse bis zum Sales-Kit.
|
||||||
|
</p>
|
||||||
|
<!-- WICHTIG: Relativer Link für Reverse Proxy -->
|
||||||
|
<a href="/gtm/" class="btn">Starten →</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8090:80"
|
- "8090:80"
|
||||||
|
volumes:
|
||||||
|
- ./nginx-proxy.conf:/etc/nginx/nginx.conf
|
||||||
depends_on:
|
depends_on:
|
||||||
- dashboard
|
- dashboard
|
||||||
- b2b-app
|
- b2b-app
|
||||||
@@ -34,6 +36,8 @@ services:
|
|||||||
# Sideloading: Python Logic
|
# Sideloading: Python Logic
|
||||||
- ./b2b_marketing_orchestrator.py:/app/b2b_marketing_orchestrator.py
|
- ./b2b_marketing_orchestrator.py:/app/b2b_marketing_orchestrator.py
|
||||||
- ./market_db_manager.py:/app/market_db_manager.py
|
- ./market_db_manager.py:/app/market_db_manager.py
|
||||||
|
# Sideloading: Server Logic
|
||||||
|
- ./b2b-marketing-assistant/server.cjs:/app/server.cjs
|
||||||
# Database Persistence
|
# Database Persistence
|
||||||
- ./b2b_projects.db:/app/b2b_projects.db
|
- ./b2b_projects.db:/app/b2b_projects.db
|
||||||
# Logs
|
# Logs
|
||||||
@@ -58,6 +62,8 @@ services:
|
|||||||
- ./market_db_manager.py:/app/market_db_manager.py
|
- ./market_db_manager.py:/app/market_db_manager.py
|
||||||
- ./config.py:/app/config.py
|
- ./config.py:/app/config.py
|
||||||
- ./helpers.py:/app/helpers.py
|
- ./helpers.py:/app/helpers.py
|
||||||
|
# Sideloading: Server Logic
|
||||||
|
- ./general-market-intelligence/server.cjs:/app/general-market-intelligence/server.cjs
|
||||||
# Database Persistence
|
# Database Persistence
|
||||||
- ./market_intelligence.db:/app/market_intelligence.db
|
- ./market_intelligence.db:/app/market_intelligence.db
|
||||||
# Logs & Keys
|
# Logs & Keys
|
||||||
@@ -78,4 +84,26 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- market-backend
|
- market-backend
|
||||||
# Port 80 is internal only
|
# Port 80 is internal only
|
||||||
|
|
||||||
|
# --- GTM ARCHITECT ---
|
||||||
|
gtm-app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: gtm-architect/Dockerfile
|
||||||
|
container_name: gtm-architect
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# Sideloading: Python Logic
|
||||||
|
- ./gtm_architect_orchestrator.py:/app/gtm_architect_orchestrator.py
|
||||||
|
- ./market_db_manager.py:/app/market_db_manager.py
|
||||||
|
# Sideloading: Server Logic
|
||||||
|
- ./gtm-architect/server.cjs:/app/gtm-architect/server.cjs
|
||||||
|
# Database Persistence
|
||||||
|
- ./gtm_projects.db:/app/gtm_projects.db
|
||||||
|
# Keys
|
||||||
|
- ./gemini_api_key.txt:/app/gemini_api_key.txt
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
- DB_PATH=/app/gtm_projects.db
|
||||||
|
# Port 3005 is internal only
|
||||||
@@ -10,7 +10,7 @@ const PORT = 3001;
|
|||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json({ limit: '50mb' }));
|
||||||
|
|
||||||
// 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 = []) => {
|
||||||
@@ -245,6 +245,11 @@ app.post('/api/save-project', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
const server = app.listen(PORT, () => {
|
||||||
console.log(`Node.js API Bridge running on http://localhost:${PORT}`);
|
console.log(`Node.js API Bridge running on http://localhost:${PORT} (Version: 1.1.0-Fix)`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set timeout to 10 minutes (600s) to handle long AI generation steps
|
||||||
|
server.setTimeout(600000);
|
||||||
|
server.keepAliveTimeout = 610000;
|
||||||
|
server.headersTimeout = 620000;
|
||||||
File diff suppressed because it is too large
Load Diff
28
gtm-architect/Dockerfile
Normal file
28
gtm-architect/Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Base image for Python
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy shared modules first
|
||||||
|
COPY helpers.py /app/
|
||||||
|
COPY config.py /app/
|
||||||
|
|
||||||
|
# Copy application-specific files
|
||||||
|
COPY gtm_architect_orchestrator.py /app/
|
||||||
|
COPY gtm-architect /app/gtm-architect
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r /app/gtm-architect/requirements.txt
|
||||||
|
|
||||||
|
# Install Node.js and npm
|
||||||
|
RUN apt-get update && apt-get install -y nodejs npm && npm cache clean --force
|
||||||
|
|
||||||
|
# Install Node.js dependencies for the frontend bridge
|
||||||
|
RUN npm install --prefix /app/gtm-architect
|
||||||
|
|
||||||
|
# Expose the port the app runs on
|
||||||
|
EXPOSE 3005
|
||||||
|
|
||||||
|
# Command to run the application
|
||||||
|
CMD ["node", "/app/gtm-architect/server.cjs"]
|
||||||
10
gtm-architect/requirements.txt
Normal file
10
gtm-architect/requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
openai
|
||||||
|
pandas
|
||||||
|
gspread
|
||||||
|
oauth2client
|
||||||
|
beautifulsoup4
|
||||||
|
requests
|
||||||
|
python-dotenv
|
||||||
|
tiktoken
|
||||||
|
thefuzz
|
||||||
|
serpapi
|
||||||
62
gtm-architect/server.cjs
Normal file
62
gtm-architect/server.cjs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const app = express();
|
||||||
|
const port = 3005; // Port for the GTM Architect service
|
||||||
|
|
||||||
|
app.use(express.json({ limit: '50mb' }));
|
||||||
|
|
||||||
|
// Middleware to serve static files from the React app
|
||||||
|
app.use(express.static('.'));
|
||||||
|
|
||||||
|
function callPythonScript(mode, data, res) {
|
||||||
|
const pythonProcess = spawn('python3', ['../gtm_architect_orchestrator.py', '--mode', mode]);
|
||||||
|
|
||||||
|
let pythonData = '';
|
||||||
|
let errorData = '';
|
||||||
|
|
||||||
|
pythonProcess.stdout.on('data', (data) => {
|
||||||
|
pythonData += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
pythonProcess.stderr.on('data', (data) => {
|
||||||
|
errorData += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
pythonProcess.on('close', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
console.error(`Python script exited with code ${code}`);
|
||||||
|
console.error('Stderr:', errorData);
|
||||||
|
return res.status(500).json({ error: 'Python script execution failed.', details: errorData });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(pythonData);
|
||||||
|
res.json(result);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse Python script output:', e);
|
||||||
|
console.error('Raw output:', pythonData);
|
||||||
|
res.status(500).json({ error: 'Failed to parse Python script output.', details: pythonData });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pythonProcess.stdin.write(JSON.stringify(data));
|
||||||
|
pythonProcess.stdin.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// API endpoint to handle requests from the frontend
|
||||||
|
app.post('/api/gtm', (req, res) => {
|
||||||
|
const { mode, data } = req.body;
|
||||||
|
if (!mode || !data) {
|
||||||
|
return res.status(400).json({ error: 'Missing mode or data in request body' });
|
||||||
|
}
|
||||||
|
callPythonScript(mode, data, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serve the main index.html for any other requests to support client-side routing
|
||||||
|
app.get('*', (req, res) => {
|
||||||
|
res.sendFile(__dirname + '/index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`GTM Architect server listening at http://localhost:${port}`);
|
||||||
|
});
|
||||||
107
gtm_architect_orchestrator.py
Normal file
107
gtm_architect_orchestrator.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add project root to Python path
|
||||||
|
project_root = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.append(str(project_root))
|
||||||
|
|
||||||
|
from helpers import call_openai_chat
|
||||||
|
import market_db_manager
|
||||||
|
|
||||||
|
# Ensure DB is ready
|
||||||
|
market_db_manager.init_db()
|
||||||
|
|
||||||
|
def get_system_instruction(lang):
|
||||||
|
if lang == 'de':
|
||||||
|
# ... (rest of get_system_instruction remains same)
|
||||||
|
else:
|
||||||
|
return """
|
||||||
|
# IDENTITY & PURPOSE
|
||||||
|
You are the "GTM Architect Engine" for Roboplanet. Your task is to develop a precise Go-to-Market strategy for new technical products (robots).
|
||||||
|
# ... (rest of english instructions)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# --- Database Handlers ---
|
||||||
|
|
||||||
|
def save_project_handler(data):
|
||||||
|
# data contains the full application state
|
||||||
|
# Ensure we have a name for the list view
|
||||||
|
if 'name' not in data:
|
||||||
|
# Try to derive a name from product input or phases
|
||||||
|
input_text = data.get('productInput', '')
|
||||||
|
# Take first 30 chars or first line
|
||||||
|
derived_name = input_text.split('\n')[0][:30] if input_text else "Untitled Strategy"
|
||||||
|
data['name'] = derived_name
|
||||||
|
|
||||||
|
result = market_db_manager.save_project(data)
|
||||||
|
print(json.dumps(result))
|
||||||
|
|
||||||
|
def list_projects_handler(data):
|
||||||
|
# data is ignored here
|
||||||
|
projects = market_db_manager.get_all_projects()
|
||||||
|
print(json.dumps(projects))
|
||||||
|
|
||||||
|
def load_project_handler(data):
|
||||||
|
project_id = data.get('id')
|
||||||
|
project = market_db_manager.load_project(project_id)
|
||||||
|
if project:
|
||||||
|
print(json.dumps(project))
|
||||||
|
else:
|
||||||
|
print(json.dumps({"error": "Project not found"}))
|
||||||
|
|
||||||
|
def delete_project_handler(data):
|
||||||
|
project_id = data.get('id')
|
||||||
|
result = market_db_manager.delete_project(project_id)
|
||||||
|
print(json.dumps(result))
|
||||||
|
|
||||||
|
# --- AI Handlers ---
|
||||||
|
|
||||||
|
def analyze_product(data):
|
||||||
|
# ... (rest of analyze_product and other AI handlers remain same)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="GTM Architect Orchestrator")
|
||||||
|
parser.add_argument("--mode", type=str, required=True, help="Execution mode")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Read stdin only if there is input, otherwise data is empty dict
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
try:
|
||||||
|
data = json.loads(sys.stdin.read())
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
data = {}
|
||||||
|
else:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
if args.mode == "analyze_product":
|
||||||
|
analyze_product(data)
|
||||||
|
elif args.mode == "discover_icps":
|
||||||
|
discover_icps(data)
|
||||||
|
elif args.mode == "hunt_whales":
|
||||||
|
hunt_whales(data)
|
||||||
|
elif args.mode == "develop_strategy":
|
||||||
|
develop_strategy(data)
|
||||||
|
elif args.mode == "generate_assets":
|
||||||
|
generate_assets(data)
|
||||||
|
elif args.mode == "generate_sales_enablement":
|
||||||
|
generate_sales_enablement(data)
|
||||||
|
# DB Modes
|
||||||
|
elif args.mode == "save_project":
|
||||||
|
save_project_handler(data)
|
||||||
|
elif args.mode == "list_projects":
|
||||||
|
list_projects_handler(data)
|
||||||
|
elif args.mode == "load_project":
|
||||||
|
load_project_handler(data)
|
||||||
|
elif args.mode == "delete_project":
|
||||||
|
delete_project_handler(data)
|
||||||
|
else:
|
||||||
|
print(json.dumps({"status": "error", "message": f"Unknown mode: {args.mode}"}))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
23
gtm_architect_plan.md
Normal file
23
gtm_architect_plan.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Plan: Umsetzung des "GTM Architect" Backends
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt den Plan zur Umsetzung der Backend-Logik für die React-Anwendung unter `/gtm-architect` als robusten, faktenbasierten Python-Service.
|
||||||
|
|
||||||
|
## 1. Zielsetzung & Architektur
|
||||||
|
|
||||||
|
- **Ziel:** Umwandlung der reinen Frontend-Anwendung in einen Service mit einem Python-Backend.
|
||||||
|
- **Architektur:** Wir replizieren den bewährten Aufbau der anderen Tools:
|
||||||
|
1. **React-Frontend:** Die Benutzeroberfläche in `/gtm-architect` bleibt bestehen.
|
||||||
|
2. **Node.js API-Brücke (`server.cjs`):** Ein Express.js-Server, der Anfragen vom Frontend annimmt und an das Python-Backend weiterleitet.
|
||||||
|
3. **Python-Orchestrator (`gtm_architect_orchestrator.py`):** Das neue Herzstück, das die gesamte Logik kapselt.
|
||||||
|
|
||||||
|
## 2. Fortschritts-Log
|
||||||
|
|
||||||
|
### Phase 1: Initialisierung & Planung
|
||||||
|
- [ ] Anforderungsanalyse und Zieldefinition.
|
||||||
|
- [ ] Architektur nach Vorbild `b2b-marketing-assistant` und `market-intel-backend` festgelegt.
|
||||||
|
- [ ] Diesen Schlachtplan in `gtm_architect_plan.md` erstellt.
|
||||||
|
- [ ] Aufbau der Grundstruktur: Erstellen der `gtm_architect_orchestrator.py`, der `server.cjs` in `/gtm-architect` und des `Dockerfile`.
|
||||||
|
- [ ] Erstellung von `package.json` und `requirements.txt`.
|
||||||
|
- [ ] Anpassung des Frontends (`App.tsx`) für die Kommunikation mit dem neuen Backend.
|
||||||
|
- [ ] Portierung der Logik aus `geminiService.ts` nach Python.
|
||||||
|
- [ ] Integration in `docker-compose.yml` und `nginx-proxy.conf`.
|
||||||
@@ -10,10 +10,10 @@ http {
|
|||||||
error_log /dev/stderr;
|
error_log /dev/stderr;
|
||||||
|
|
||||||
# Increase Timeouts for Long-Running AI Tasks
|
# Increase Timeouts for Long-Running AI Tasks
|
||||||
proxy_read_timeout 600s;
|
proxy_read_timeout 1200s;
|
||||||
proxy_connect_timeout 600s;
|
proxy_connect_timeout 1200s;
|
||||||
proxy_send_timeout 600s;
|
proxy_send_timeout 1200s;
|
||||||
send_timeout 600s;
|
send_timeout 1200s;
|
||||||
|
|
||||||
# Resolver ist wichtig für Docker
|
# Resolver ist wichtig für Docker
|
||||||
resolver 127.0.0.11 valid=30s ipv6=off;
|
resolver 127.0.0.11 valid=30s ipv6=off;
|
||||||
@@ -37,6 +37,11 @@ http {
|
|||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
# Explicit timeouts for this location
|
||||||
|
proxy_read_timeout 1200s;
|
||||||
|
proxy_connect_timeout 1200s;
|
||||||
|
proxy_send_timeout 1200s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /market/ {
|
location /market/ {
|
||||||
@@ -45,6 +50,24 @@ http {
|
|||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
# Explicit timeouts for this location
|
||||||
|
proxy_read_timeout 1200s;
|
||||||
|
proxy_connect_timeout 1200s;
|
||||||
|
proxy_send_timeout 1200s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /gtm/ {
|
||||||
|
# Der Trailing Slash am Ende ist wichtig!
|
||||||
|
proxy_pass http://gtm-app:3005/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
# Explicit timeouts for this location
|
||||||
|
proxy_read_timeout 1200s;
|
||||||
|
proxy_connect_timeout 1200s;
|
||||||
|
proxy_send_timeout 1200s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user