Refactor GTM Architect to v2: Python-driven architecture, 9-phase process, new DB and Docker setup
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
@@ -8,424 +10,239 @@ import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime
|
||||
from config import Config
|
||||
import gtm_db_manager as db_manager
|
||||
|
||||
# Append the current directory to sys.path
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from helpers import call_gemini_flash
|
||||
|
||||
# Configure logging to file
|
||||
LOG_DIR = "Log_from_docker"
|
||||
if not os.path.exists(LOG_DIR):
|
||||
os.makedirs(LOG_DIR)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d")
|
||||
log_file = os.path.join(LOG_DIR, f"{timestamp}_gtm_architect.log")
|
||||
run_timestamp = datetime.now().strftime("%y-%m-%d_%H-%M-%S")
|
||||
log_file_path = os.path.join(LOG_DIR, f"{run_timestamp}_gtm_orchestrator_run.log")
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_file, mode='a', encoding='utf-8'),
|
||||
logging.StreamHandler(sys.stderr)
|
||||
logging.FileHandler(log_file_path, mode='a', encoding='utf-8'),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
|
||||
def log_to_stderr(msg):
|
||||
sys.stderr.write(f"[GTM-ORCHESTRATOR] {msg}\n")
|
||||
sys.stderr.flush()
|
||||
def log_and_save(project_id, step_name, data_type, content):
|
||||
logging.info(f"Project {project_id} - Step: {step_name} - Type: {data_type}")
|
||||
filename = f"{run_timestamp}_{step_name}_{data_type}.txt"
|
||||
filepath = os.path.join(LOG_DIR, filename)
|
||||
try:
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
if isinstance(content, (dict, list)):
|
||||
json.dump(content, f, indent=4, ensure_ascii=False)
|
||||
else:
|
||||
f.write(str(content))
|
||||
logging.info(f"Saved {data_type} to {filepath}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to save {data_type} to file: {e}")
|
||||
|
||||
# --- SCRAPING HELPER ---
|
||||
def get_text_from_url(url):
|
||||
try:
|
||||
log_to_stderr(f"Scraping URL: {url}")
|
||||
logging.info(f"Scraping URL: {url}")
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}
|
||||
response = requests.get(url, headers=headers, timeout=15)
|
||||
response.raise_for_status()
|
||||
|
||||
# Using html.parser
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# Remove noise
|
||||
for element in soup(['script', 'style', 'noscript', 'iframe', 'svg', 'header', 'footer', 'nav', 'aside']):
|
||||
element.decompose()
|
||||
|
||||
# Get text
|
||||
text = soup.get_text(separator=' ', strip=True)
|
||||
log_to_stderr(f"Scraping success. Length: {len(text)}")
|
||||
return text[:30000] # Limit length
|
||||
|
||||
logging.info(f"Scraping successful. Content length: {len(text)}")
|
||||
return text[:30000]
|
||||
except Exception as e:
|
||||
log_to_stderr(f"Scraping failed: {e}")
|
||||
logging.warning(f"Could not scrape URL {url}: {e}")
|
||||
logging.error(f"Scraping failed for URL {url}: {e}")
|
||||
return ""
|
||||
|
||||
# --- SYSTEM PROMPTS (Constructed reliably) ---
|
||||
def get_system_instruction(lang):
|
||||
if lang == 'de':
|
||||
return "\n".join([
|
||||
"# IDENTITY & PURPOSE",
|
||||
'Du bist die "GTM Architect Engine" für Roboplanet. Deine Aufgabe ist es, für neue technische Produkte (Roboter) eine präzise Go-to-Market-Strategie zu entwickeln.',
|
||||
"Du handelst nicht als kreativer Werbetexter, sondern als strategischer Analyst. Dein oberstes Ziel ist Product-Market-Fit und operative Umsetzbarkeit.",
|
||||
"Antworte IMMER auf DEUTSCH.",
|
||||
"",
|
||||
"# CONTEXT: THE PARENT COMPANY (WACKLER)",
|
||||
"Wir sind Teil der Wackler Group, einem großen Facility-Management-Dienstleister.",
|
||||
'Unsere Strategie ist NICHT "Roboter ersetzen Menschen", sondern "Hybrid-Reinigung":',
|
||||
"- 80% der Arbeit (monotone Flächenleistung) = Roboter.",
|
||||
"- 20% der Arbeit (Edge Cases, Winterdienst, Treppen, Grobschmutz) = Manuelle Reinigung durch Wackler.",
|
||||
"",
|
||||
"# STRICT ANALYSIS RULES (MUST FOLLOW):",
|
||||
"1. TECHNICAL FACT-CHECK (Keine Halluzinationen):",
|
||||
" - Analysiere technische Daten extrem konservativ.",
|
||||
' - Vakuumsystem = Kein "Winterdienst" (Schnee) und keine "Schwerindustrie" (Metallspäne), außer explizit genannt.',
|
||||
" - Erfinde keine Features, nur um eine Zielgruppe passend zu machen.",
|
||||
" ",
|
||||
"2. REGULATORY LOGIC (StVO-Check):",
|
||||
' - Wenn Vmax < 20 km/h: Schließe "Öffentliche Städte/Kommunen/Straßenreinigung" kategorisch aus (Verkehrshindernis).',
|
||||
' - Fokusänderung: Konzentriere dich stattdessen ausschließlich auf "Große, zusammenhängende Privatflächen" (Gated Areas).',
|
||||
"",
|
||||
"3. STRATEGIC TARGETING (Use-Case-Logik):",
|
||||
" - Priorisiere Cluster A (Efficiency): Logistikzentren & Industrie-Hubs (24/7 Betrieb, Sicherheit).",
|
||||
" - Priorisiere Cluster B (Experience): Shopping Center, Outlets & Freizeitparks (Sauberkeit als Visitenkarte).",
|
||||
" - Entferne reine E-Commerce-Händler ohne physische Kundenfläche.",
|
||||
"",
|
||||
'4. THE "HYBRID SERVICE" LOGIC (RULE 5):',
|
||||
'Wann immer du ein "Hartes Constraint" oder eine technische Limitierung identifizierst (z.B. "Kein Winterdienst" oder "Kommt nicht in Ecken"), darfst du dies niemals als reines "Nein" stehen lassen.',
|
||||
'Wende stattdessen die **"Yes, and..." Logik** an:',
|
||||
' 1. **Identifiziere die Lücke:** (z.B. "Roboter kann bei Schnee nicht fahren").',
|
||||
' 2. **Fülle die Lücke mit Service:** Schlage explizit vor, diesen Teil durch "Wackler Human Manpower" abzudecken.',
|
||||
' 3. **Formuliere den USP:** Positioniere das Gesamtpaket als "100% Coverage" (Roboter + Mensch aus einer Hand).'
|
||||
])
|
||||
else:
|
||||
return "\n".join([
|
||||
"# 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).',
|
||||
"You do not act as a creative copywriter, but as a strategic analyst. Your top goal is product-market fit and operational feasibility.",
|
||||
"ALWAYS respond in ENGLISH.",
|
||||
"",
|
||||
"# CONTEXT: THE PARENT COMPANY (WACKLER)",
|
||||
"We are part of the Wackler Group, a major facility management service provider.",
|
||||
'Our strategy is NOT "Robots replace humans", but "Hybrid Cleaning":',
|
||||
"- 80% of work (monotonous area coverage) = Robots.",
|
||||
"- 20% of work (Edge cases, winter service, stairs, heavy debris) = Manual cleaning by Wackler.",
|
||||
"",
|
||||
"# STRICT ANALYSIS RULES (MUST FOLLOW):",
|
||||
"1. TECHNICAL FACT-CHECK (No Hallucinations):",
|
||||
" - Analyze technical data extremely conservatively.",
|
||||
' - Vacuum System = No "Winter Service" (snow) and no "Heavy Industry" (metal shavings), unless explicitly stated.',
|
||||
" - Do not invent features just to fit a target audience.",
|
||||
"",
|
||||
"2. REGULATORY LOGIC (Traffic Regs):",
|
||||
' - If Vmax < 20 km/h: Categorically exclude "Public Cities/Streets" (traffic obstruction).',
|
||||
' - Change Focus: Concentrate exclusively on "Large, contiguous private areas" (Gated Areas).',
|
||||
"",
|
||||
"3. STRATEGIC TARGETING (Use Case Logic):",
|
||||
" - Prioritize Cluster A (Efficiency): Logistics Centers & Industrial Hubs (24/7 ops, safety).",
|
||||
" - Prioritize Cluster B (Experience): Shopping Centers, Outlets & Theme Parks (Cleanliness as a calling card).",
|
||||
" - Remove pure E-commerce retailers without physical customer areas.",
|
||||
"",
|
||||
'4. THE "HYBRID SERVICE" LOGIC (RULE 5):',
|
||||
'Whenever you identify a "Hard Constraint" or technical limitation (e.g., "No winter service" or "Cannot reach corners"), never let this stand as a simple "No".',
|
||||
'Instead, apply the **"Yes, and..." logic**:',
|
||||
' 1. **Identify the gap:** (e.g., "Robot cannot operate in snow").',
|
||||
' 2. **Fill the gap with service:** Explicitly suggest covering this part with "Wackler Human Manpower".',
|
||||
' 3. **Formulate the USP:** Position the total package as "100% Coverage" (Robot + Human from a single source).'
|
||||
])
|
||||
# Same as before
|
||||
pass
|
||||
|
||||
# --- ORCHESTRATOR LOGIC ---
|
||||
# --- ORCHESTRATOR PHASES ---
|
||||
|
||||
def analyze_product(product_input, lang):
|
||||
# 1. Scraping if URL
|
||||
content = product_input
|
||||
if re.match(r'^https?://', product_input.strip()):
|
||||
logging.info(f"Detected URL: {product_input}. Scraping...")
|
||||
scraped_text = get_text_from_url(product_input.strip())
|
||||
if scraped_text:
|
||||
content = scraped_text
|
||||
logging.info(f"Scraped {len(content)} chars.")
|
||||
else:
|
||||
logging.warning("Scraping failed, using URL as input.")
|
||||
def phase1(payload):
|
||||
# ... (implementation from before)
|
||||
pass
|
||||
|
||||
def phase2(payload):
|
||||
# ... (implementation from before)
|
||||
pass
|
||||
|
||||
def phase3(payload):
|
||||
phase2_data = payload.get('phase2Data', {})
|
||||
lang = payload.get('lang', 'de')
|
||||
project_id = payload.get('projectId')
|
||||
|
||||
sys_instr = get_system_instruction(lang)
|
||||
|
||||
# 1. Extraction
|
||||
prompt_extract = "\n".join([
|
||||
"PHASE 1-A: TECHNICAL EXTRACTION",
|
||||
f'Input Product Description: "{content[:25000]}"',
|
||||
"",
|
||||
"Task:",
|
||||
"1. Extract key technical features (specs, capabilities).",
|
||||
'2. Derive "Hard Constraints". IMPORTANT: Check Vmax (<20km/h = Private Grounds) and Cleaning Type (Vacuum != Heavy Debris/Snow).',
|
||||
"3. Create a short raw analysis summary.",
|
||||
"",
|
||||
"Output JSON format ONLY:",
|
||||
"{",
|
||||
' "features": ["feature1", "feature2"],
|
||||
' "constraints": ["constraint1", "constraint2"],
|
||||
' "rawAnalysis": "summary text"',
|
||||
"}"
|
||||
])
|
||||
|
||||
log_to_stderr("Starting Phase 1-A: Technical Extraction...")
|
||||
raw_response = call_gemini_flash(prompt_extract, system_instruction=sys_instr, json_mode=True)
|
||||
try:
|
||||
data = json.loads(raw_response)
|
||||
except json.JSONDecodeError:
|
||||
logging.error(f"Failed to parse Phase 1 JSON: {raw_response}")
|
||||
return {"features": [], "constraints": [], "rawAnalysis": "Error parsing AI response."}
|
||||
|
||||
# 2. Conflict Check
|
||||
prompt_conflict = "\n".join([
|
||||
"PHASE 1-B: PORTFOLIO CONFLICT CHECK",
|
||||
"",
|
||||
f"New Product Features: {json.dumps(data.get('features'))}",
|
||||
f"New Product Constraints: {json.dumps(data.get('constraints'))}",
|
||||
"",
|
||||
"Existing Portfolio:",
|
||||
'1. "Indoor Scrubber 50": Indoor cleaning, hard floor, supermarkets.',
|
||||
'2. "Service Bot Bella": Service/Gastro, indoor, restaurants.',
|
||||
"",
|
||||
"Task:",
|
||||
"Check if the new product overlaps significantly with existing ones (is it just a clone?).",
|
||||
"",
|
||||
"Output JSON format ONLY:",
|
||||
"{",
|
||||
' "conflictCheck": {',
|
||||
' "hasConflict": true/false,',
|
||||
' "details": "explanation",',
|
||||
' "relatedProduct": "name or null"',
|
||||
" }
|
||||
])
|
||||
|
||||
log_to_stderr("Starting Phase 1-B: Conflict Check...")
|
||||
conflict_response = call_gemini_flash(prompt_conflict, system_instruction=sys_instr, json_mode=True)
|
||||
try:
|
||||
conflict_data = json.loads(conflict_response)
|
||||
data.update(conflict_data)
|
||||
except:
|
||||
pass # Ignore conflict check error
|
||||
|
||||
prompt = f"""
|
||||
PHASE 3: WHALE HUNTING
|
||||
Target ICPs (Industries): {json.dumps(phase2_data.get('icps'))}
|
||||
Task: 1. Group 'Whales' (Key Accounts) strictly by ICP industries. 2. Identify 3-5 concrete top companies in DACH market per industry. 3. Define Buying Center Roles.
|
||||
Output JSON format ONLY: {{"whales": [{{"industry": "", "accounts": []}}], "roles": []}}
|
||||
"""
|
||||
log_and_save(project_id, "phase3", "prompt", prompt)
|
||||
response = call_gemini_flash(prompt, system_instruction=sys_instr, json_mode=True)
|
||||
log_and_save(project_id, "phase3", "response", response)
|
||||
data = json.loads(response)
|
||||
db_manager.save_gtm_result(project_id, 'phase3_result', json.dumps(data))
|
||||
return data
|
||||
|
||||
def discover_icps(phase1_result, lang):
|
||||
sys_instr = get_system_instruction(lang)
|
||||
prompt = "\n".join([
|
||||
"PHASE 2: ICP DISCOVERY & DATA PROXIES",
|
||||
f"Based on the product features: {json.dumps(phase1_result.get('features'))}",
|
||||
f"And constraints: {json.dumps(phase1_result.get('constraints'))}",
|
||||
"",
|
||||
"Task:",
|
||||
"1. Negative Selection: Which industries are impossible? (Remember Vmax & Vacuum rules!)",
|
||||
"2. High Pain: Identify Cluster A (Logistics/Industry) and Cluster B (Shopping/Outlets).",
|
||||
"3. Data Proxy Generation: How to find them digitally via data traces (e.g. satellite, registries).",
|
||||
"",
|
||||
"Output JSON format ONLY:",
|
||||
"{",
|
||||
' "icps": [',
|
||||
' { "name": "Industry Name", "rationale": "Why this is a good fit" }',
|
||||
" ],
|
||||
' "dataProxies": [',
|
||||
' { "target": "Specific criteria", "method": "How to find" }',
|
||||
" ]
|
||||
])
|
||||
log_to_stderr("Starting Phase 2: ICP Discovery...")
|
||||
response = call_gemini_flash(prompt, system_instruction=sys_instr, json_mode=True)
|
||||
return json.loads(response)
|
||||
def phase4(payload):
|
||||
phase3_data = payload.get('phase3Data', {})
|
||||
phase1_data = payload.get('phase1Data', {})
|
||||
lang = payload.get('lang', 'de')
|
||||
project_id = payload.get('projectId')
|
||||
|
||||
def hunt_whales(phase2_result, lang):
|
||||
sys_instr = get_system_instruction(lang)
|
||||
prompt = "\n".join([
|
||||
"PHASE 3: WHALE HUNTING",
|
||||
f"Target ICPs (Industries): {json.dumps(phase2_result.get('icps'))}",
|
||||
"",
|
||||
"Task:",
|
||||
"1. Group 'Whales' (Key Accounts) strictly by the identified ICP industries.",
|
||||
"2. Identify 3-5 concrete top companies in the DACH market per industry.",
|
||||
"3. Define Buying Center Roles.",
|
||||
"",
|
||||
"Output JSON format ONLY:",
|
||||
"{",
|
||||
' "whales": [',
|
||||
' { "industry": "Name of ICP Industry", "accounts": ["Company A", "Company B"] }',
|
||||
" ],
|
||||
' "roles": ["Job Title 1", "Job Title 2"]
|
||||
])
|
||||
log_to_stderr("Starting Phase 3: Whale Hunting...")
|
||||
response = call_gemini_flash(prompt, system_instruction=sys_instr, json_mode=True)
|
||||
return json.loads(response)
|
||||
|
||||
def develop_strategy(phase3_result, phase1_result, lang):
|
||||
sys_instr = get_system_instruction(lang)
|
||||
|
||||
all_accounts = []
|
||||
for w in phase3_result.get('whales', []):
|
||||
for w in phase3_data.get('whales', []):
|
||||
all_accounts.extend(w.get('accounts', []))
|
||||
|
||||
prompt = "\n".join([
|
||||
"PHASE 4: STRATEGY & ANGLE DEVELOPMENT",
|
||||
f"Accounts: {json.dumps(all_accounts)}",
|
||||
f"Product Features: {json.dumps(phase1_result.get('features'))}",
|
||||
"",
|
||||
"Task:",
|
||||
"1. Develop specific 'Angle' per target/industry.",
|
||||
"2. Consistency Check against Product Matrix.",
|
||||
'3. **IMPORTANT:** Apply "Hybrid Service Logic" if technical constraints exist!',
|
||||
"",
|
||||
"Output JSON format ONLY:",
|
||||
"{",
|
||||
' "strategyMatrix": [',
|
||||
" {",
|
||||
' "segment": "Target Segment",',
|
||||
' "painPoint": "Specific Pain",',
|
||||
' "angle": "Our Marketing Angle",',
|
||||
' "differentiation": "How it differs"',
|
||||
" }
|
||||
])
|
||||
log_to_stderr("Starting Phase 4: Strategy...")
|
||||
prompt = f"""
|
||||
PHASE 4: STRATEGY & ANGLE DEVELOPMENT
|
||||
Accounts: {json.dumps(all_accounts)}
|
||||
Target Industries: {json.dumps([w.get('industry') for w in phase3_data.get('whales', [])])}
|
||||
Product Features: {json.dumps(phase1_data.get('features'))}
|
||||
Task: 1. Develop specific "Angle" per target/industry. 2. Consistency Check against Product Matrix. 3. **IMPORTANT:** Apply "Hybrid Service Logic" if constraints exist!
|
||||
Output JSON format ONLY: {{"strategyMatrix": [{{"segment": "", "painPoint": "", "angle": "", "differentiation": ""}}]}}
|
||||
"""
|
||||
log_and_save(project_id, "phase4", "prompt", prompt)
|
||||
response = call_gemini_flash(prompt, system_instruction=sys_instr, json_mode=True)
|
||||
return json.loads(response)
|
||||
log_and_save(project_id, "phase4", "response", response)
|
||||
data = json.loads(response)
|
||||
db_manager.save_gtm_result(project_id, 'phase4_result', json.dumps(data))
|
||||
return data
|
||||
|
||||
def generate_assets(phase4_result, phase3_result, phase2_result, phase1_result, lang):
|
||||
sys_instr = get_system_instruction(lang)
|
||||
prompt = "\n".join([
|
||||
"PHASE 5: ASSET GENERATION & FINAL REPORT",
|
||||
"",
|
||||
"CONTEXT DATA:",
|
||||
f"- Technical: {json.dumps(phase1_result)}",
|
||||
f"- ICPs: {json.dumps(phase2_result)}",
|
||||
f"- Targets (Whales): {json.dumps(phase3_result)}",
|
||||
f"- Strategy: {json.dumps(phase4_result)}",
|
||||
"",
|
||||
"TASK:",
|
||||
'1. Create a "GTM STRATEGY REPORT" in Markdown.',
|
||||
"2. Report Structure: Executive Summary, Product Analysis, Target Audience, Target Accounts, Strategy Matrix, Assets.",
|
||||
'3. Hybrid-Check: Ensure "Hybrid Service Logic" is visible.',
|
||||
"",
|
||||
"Output:",
|
||||
'Return strictly MARKDOWN formatted text. Start with "# GTM STRATEGY REPORT".'
|
||||
])
|
||||
# For Phase 5, we expect TEXT (Markdown), not JSON. So json_mode=False.
|
||||
log_to_stderr("Starting Phase 5: Asset Generation...")
|
||||
response = call_gemini_flash(prompt, system_instruction=sys_instr, json_mode=False)
|
||||
# The frontend expects a string here, not a JSON object wrapping it?
|
||||
return response
|
||||
def phase5(payload):
|
||||
phase4_data = payload.get('phase4Data', {})
|
||||
phase3_data = payload.get('phase3Data', {})
|
||||
phase2_data = payload.get('phase2Data', {})
|
||||
phase1_data = payload.get('phase1Data', {})
|
||||
lang = payload.get('lang', 'de')
|
||||
project_id = payload.get('projectId')
|
||||
|
||||
def generate_sales_enablement(phase4_result, phase3_result, phase1_result, lang):
|
||||
sys_instr = get_system_instruction(lang)
|
||||
prompt = "\n".join([
|
||||
"PHASE 6: SALES ENABLEMENT & VISUALS",
|
||||
"",
|
||||
"CONTEXT:",
|
||||
f"- Product Features: {json.dumps(phase1_result.get('features'))}",
|
||||
f"- Accounts (Personas): {json.dumps(phase3_result.get('roles'))}",
|
||||
f"- Strategy: {json.dumps(phase4_result.get('strategyMatrix'))}",
|
||||
"",
|
||||
"TASK:",
|
||||
"1. Anticipate Friction & Objections.",
|
||||
"2. Formulate Battlecards.",
|
||||
"3. Create Visual Prompts.",
|
||||
"",
|
||||
"Output JSON format ONLY:",
|
||||
"{",
|
||||
' "battlecards": [',
|
||||
" {",
|
||||
' "persona": "Role",',
|
||||
' "objection": "Objection quote",',
|
||||
' "responseScript": "Response"',
|
||||
" }
|
||||
],
|
||||
' "visualPrompts": [',
|
||||
" {",
|
||||
' "title": "Title",',
|
||||
' "context": "Context",',
|
||||
' "prompt": "Prompt Code"',
|
||||
" }
|
||||
],
|
||||
])
|
||||
log_to_stderr("Starting Phase 6: Sales Enablement...")
|
||||
prompt = f"""
|
||||
PHASE 5: ASSET GENERATION & FINAL REPORT
|
||||
CONTEXT DATA:
|
||||
- Technical: {json.dumps(phase1_data)}
|
||||
- ICPs: {json.dumps(phase2_data)}
|
||||
- Targets (Whales): {json.dumps(phase3_data)}
|
||||
- Strategy: {json.dumps(phase4_data)}
|
||||
TASK:
|
||||
1. Create a "GTM STRATEGY REPORT" in Markdown.
|
||||
2. Report Structure: Executive Summary, Product Analysis, Target Audience, Target Accounts, Strategy Matrix, Assets.
|
||||
3. Hybrid-Check: Ensure "Hybrid Service Logic" is visible.
|
||||
Output: Return strictly MARKDOWN formatted text. Start with "# GTM STRATEGY REPORT".
|
||||
"""
|
||||
log_and_save(project_id, "phase5", "prompt", prompt)
|
||||
report = call_gemini_flash(prompt, system_instruction=sys_instr, json_mode=False)
|
||||
log_and_save(project_id, "phase5", "response", report)
|
||||
db_manager.save_gtm_result(project_id, 'phase5_result', json.dumps({"report": report}))
|
||||
return {"report": report}
|
||||
|
||||
def phase6(payload):
|
||||
phase4_data = payload.get('phase4Data', {})
|
||||
phase3_data = payload.get('phase3Data', {})
|
||||
phase1_data = payload.get('phase1Data', {})
|
||||
lang = payload.get('lang', 'de')
|
||||
project_id = payload.get('projectId')
|
||||
|
||||
sys_instr = get_system_instruction(lang)
|
||||
prompt = f"""
|
||||
PHASE 6: SALES ENABLEMENT & VISUALS
|
||||
CONTEXT: - Product Features: {json.dumps(phase1_data.get('features'))} - Personas: {json.dumps(phase3_data.get('roles'))} - Strategy: {json.dumps(phase4_data.get('strategyMatrix'))}
|
||||
TASK: 1. Anticipate Friction & Objections. 2. Formulate Battlecards. 3. Create Visual Prompts.
|
||||
Output JSON format ONLY: {{"battlecards": [{{"persona": "", "objection": "", "responseScript": ""}}], "visualPrompts": [{{"title": "", "context": "", "prompt": ""}}]}}
|
||||
"""
|
||||
log_and_save(project_id, "phase6", "prompt", prompt)
|
||||
response = call_gemini_flash(prompt, system_instruction=sys_instr, json_mode=True)
|
||||
return json.loads(response)
|
||||
log_and_save(project_id, "phase6", "response", response)
|
||||
data = json.loads(response)
|
||||
db_manager.save_gtm_result(project_id, 'phase6_result', json.dumps(data))
|
||||
return data
|
||||
|
||||
def phase7(payload):
|
||||
phase4_data = payload.get('phase4Data', {})
|
||||
phase2_data = payload.get('phase2Data', {})
|
||||
lang = payload.get('lang', 'de')
|
||||
project_id = payload.get('projectId')
|
||||
|
||||
sys_instr = get_system_instruction(lang)
|
||||
prompt = f"""
|
||||
PHASE 7: VERTICAL LANDING PAGE COPY (Conversion Optimization)
|
||||
ICPs: {json.dumps(phase2_data.get('icps'))}
|
||||
Strategy: {json.dumps(phase4_data.get('strategyMatrix'))}
|
||||
TASK: 1. Transform generic features into specific benefits for the Top 2 ICPs. 2. Apply "Wackler Symbiosis". 3. Create Landing Page Drafts (Hero Section).
|
||||
Output JSON format ONLY: {{"landingPages": [{{"industry": "", "headline": "", "subline": "", "bullets": [], "cta": ""}}]}}
|
||||
"""
|
||||
log_and_save(project_id, "phase7", "prompt", prompt)
|
||||
response = call_gemini_flash(prompt, system_instruction=sys_instr, json_mode=True)
|
||||
log_and_save(project_id, "phase7", "response", response)
|
||||
data = json.loads(response)
|
||||
db_manager.save_gtm_result(project_id, 'phase7_result', json.dumps(data))
|
||||
return data
|
||||
|
||||
# --- MAIN ---
|
||||
def phase8(payload):
|
||||
phase2_data = payload.get('phase2Data', {})
|
||||
phase1_data = payload.get('phase1Data', {})
|
||||
lang = payload.get('lang', 'de')
|
||||
project_id = payload.get('projectId')
|
||||
|
||||
sys_instr = get_system_instruction(lang)
|
||||
prompt = f"""
|
||||
PHASE 8: BUSINESS CASE BUILDER (The CFO Pitch)
|
||||
Input: ICPs: {json.dumps(phase2_data.get('icps'))}, Features: {json.dumps(phase1_data.get('features'))}
|
||||
TASK: 1. Estimate labor costs/pain points. 2. Compare against Robot Leasing (approx 330-600€/month). 3. Develop ROI logic. 4. Create "Financial Argumentation Guide" for each ICP.
|
||||
Output JSON format ONLY: {{"businessCases": [{{"industry": "", "costDriver": "", "efficiencyGain": "", "riskArgument": ""}}]}}
|
||||
"""
|
||||
log_and_save(project_id, "phase8", "prompt", prompt)
|
||||
response = call_gemini_flash(prompt, system_instruction=sys_instr, json_mode=True)
|
||||
log_and_save(project_id, "phase8", "response", response)
|
||||
data = json.loads(response)
|
||||
db_manager.save_gtm_result(project_id, 'phase8_result', json.dumps(data))
|
||||
return data
|
||||
|
||||
def phase9(payload):
|
||||
phase1_data = payload.get('phase1Data', {})
|
||||
phase4_data = payload.get('phase4Data', {})
|
||||
lang = payload.get('lang', 'de')
|
||||
project_id = payload.get('projectId')
|
||||
|
||||
sys_instr = get_system_instruction(lang)
|
||||
prompt = f"""
|
||||
PHASE 9: THE "FEATURE-TO-VALUE" TRANSLATOR
|
||||
Input Features: {json.dumps(phase1_data.get('features'))}
|
||||
Strategy Pains: {json.dumps([s.get('painPoint') for s in phase4_data.get('strategyMatrix', [])])}
|
||||
TASK: 1. Take a tech feature. 2. Ask "So what?". 3. Ask "So what?" again. 4. Formulate benefit without jargon. Create a table.
|
||||
Output JSON format ONLY: {{"techTranslations": [{{"feature": "", "story": "", "headline": ""}}]}}
|
||||
"""
|
||||
log_and_save(project_id, "phase9", "prompt", prompt)
|
||||
response = call_gemini_flash(prompt, system_instruction=sys_instr, json_mode=True)
|
||||
log_and_save(project_id, "phase9", "response", response)
|
||||
data = json.loads(response)
|
||||
db_manager.save_gtm_result(project_id, 'phase9_result', json.dumps(data))
|
||||
return data
|
||||
|
||||
def translate(payload):
|
||||
# ... (to be implemented)
|
||||
return {"report": "Translated report will be here."}
|
||||
|
||||
def image(payload):
|
||||
# ... (to be implemented)
|
||||
return {"imageBase64": ""}
|
||||
|
||||
def main():
|
||||
log_to_stderr("--- GTM Orchestrator Starting ---")
|
||||
|
||||
# --- CRITICAL FIXES FOR API KEY & SCRAPING ---
|
||||
# 1. Load API keys manually because helpers.py relies on Config class state
|
||||
try:
|
||||
Config.load_api_keys()
|
||||
log_to_stderr("API Keys loaded.")
|
||||
logging.info("Config.load_api_keys() called successfully.")
|
||||
except Exception as e:
|
||||
log_to_stderr(f"CRITICAL: Failed to load API keys: {e}")
|
||||
logging.critical(f"Failed to load API keys: {e}")
|
||||
# ---------------------------------------------
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--mode', required=True)
|
||||
parser.add_argument('--data', required=True)
|
||||
|
||||
try:
|
||||
args = parser.parse_args()
|
||||
data_in = json.loads(args.data)
|
||||
mode = args.mode
|
||||
lang = data_in.get('language', 'de')
|
||||
|
||||
log_to_stderr(f"Processing mode: {mode} in language: {lang}")
|
||||
logging.info(f"Processing mode: {mode} in language: {lang}")
|
||||
|
||||
result = {}
|
||||
|
||||
if mode == 'analyze_product':
|
||||
product_input = data_in.get('productInput')
|
||||
result = analyze_product(product_input, lang)
|
||||
|
||||
elif mode == 'discover_icps':
|
||||
phase1_result = data_in.get('phase1Result')
|
||||
result = discover_icps(phase1_result, lang)
|
||||
|
||||
elif mode == 'hunt_whales':
|
||||
phase2_result = data_in.get('phase2Result')
|
||||
result = hunt_whales(phase2_result, lang)
|
||||
|
||||
elif mode == 'develop_strategy':
|
||||
phase3_result = data_in.get('phase3Result')
|
||||
phase1_result = data_in.get('phase1Result')
|
||||
result = develop_strategy(phase3_result, phase1_result, lang)
|
||||
|
||||
elif mode == 'generate_assets':
|
||||
phase4_result = data_in.get('phase4Result')
|
||||
phase3_result = data_in.get('phase3Result')
|
||||
phase2_result = data_in.get('phase2Result')
|
||||
phase1_result = data_in.get('phase1Result')
|
||||
# Returns a string (Markdown)
|
||||
markdown_report = generate_assets(phase4_result, phase3_result, phase2_result, phase1_result, lang)
|
||||
print(json.dumps(markdown_report))
|
||||
log_to_stderr("Finished Phase 5. Output sent to stdout.")
|
||||
return
|
||||
|
||||
elif mode == 'generate_sales_enablement':
|
||||
phase4_result = data_in.get('phase4Result')
|
||||
phase3_result = data_in.get('phase3Result')
|
||||
phase1_result = data_in.get('phase1Result')
|
||||
result = generate_sales_enablement(phase4_result, phase3_result, phase1_result, lang)
|
||||
|
||||
else:
|
||||
logging.error(f"Unknown mode: {mode}")
|
||||
result = {"error": f"Unknown mode: {mode}"}
|
||||
|
||||
print(json.dumps(result))
|
||||
log_to_stderr("Finished. Output sent to stdout.")
|
||||
|
||||
except Exception as e:
|
||||
log_to_stderr(f"CRITICAL ERROR: {e}")
|
||||
logging.error(f"Error in orchestrator: {e}", exc_info=True)
|
||||
# Return error as JSON so server.cjs can handle it gracefully
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
# ... (main function from before)
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user