[2ff88f42] Implement Ops-Secondary Logic & Matrix Gen v3.2

This commit is contained in:
2026-02-23 10:45:12 +00:00
parent 0a125480c1
commit d0d75b9eeb
2 changed files with 114 additions and 39 deletions

View File

@@ -2,7 +2,7 @@ import sys
import os import os
import json import json
import argparse import argparse
from typing import List import re
import google.generativeai as genai import google.generativeai as genai
# Setup Environment # Setup Environment
@@ -14,30 +14,76 @@ from backend.config import settings
# --- Configuration --- # --- Configuration ---
MODEL_NAME = "gemini-2.0-flash" # High quality copy MODEL_NAME = "gemini-2.0-flash" # High quality copy
def extract_segment(text: str, marker: str) -> str:
"""
Extracts a text block starting with [marker].
Example: [Primary Product: Cleaning] ... [Secondary Product: Service]
"""
if not text: return ""
# Split by square brackets that look like headers [Text: ...]
# We look for the marker inside the header
# Simplified Regex: Capture everything inside brackets as ONE group
pattern = r'\[(.*?)\]'
segments = re.split(pattern, text)
# segments[0] is text before first bracket
# segments[1] is content of first bracket (header)
# segments[2] is content after first bracket (body)
# ...
best_match = ""
for i in range(1, len(segments), 2):
header = segments[i]
content = segments[i+1]
# print(f"DEBUG: Checking Header: '{header}' for Marker: '{marker}'") # Uncomment for deep debug
if marker.lower() in header.lower():
return content.strip()
# Fallback: If no markers found, return full text (legacy support)
if "Primary Product" not in text and "Secondary Product" not in text:
return text
return ""
def generate_prompt(industry: Industry, persona: Persona) -> str: def generate_prompt(industry: Industry, persona: Persona) -> str:
""" """
Builds the prompt for the AI to generate the marketing texts. Builds the prompt for the AI to generate the marketing texts.
Combines Industry context with Persona specific pains/gains and Product Category. Combines Industry context with Persona specific pains/gains and Product Category.
""" """
# 1. Determine Product Context # 1. Determine Product Focus Strategy
# We focus on the primary category for the general matrix, # Default: Primary
# but we inform the AI about the secondary option if applicable. target_scope = "Primary Product"
primary_cat = industry.primary_category target_category = industry.primary_category
product_context = f"{primary_cat.name}: {primary_cat.description}" if primary_cat else "Intelligente Robotik-Lösungen"
# 2. Extract specific segments from industry pains/gains # Special Rule: "Operativer Entscheider" gets Secondary Product IF ops_focus_secondary is True
def extract_segment(text, marker): # Logic: A Nursing Director (Ops) doesn't care about floor cleaning (Facility),
if not text: return "" # but cares about Service Robots (Secondary).
import re if persona.name == "Operativer Entscheider" and industry.ops_focus_secondary:
segments = re.split(r'\[(.*?)\]', text) target_scope = "Secondary Product"
for i in range(1, len(segments), 2): target_category = industry.secondary_category
if marker.lower() in segments[i].lower(): print(f" -> STRATEGY SWITCH: Using {target_scope} for {persona.name}")
return segments[i+1].strip()
return text
industry_pains = extract_segment(industry.pains, "Primary Product") # Fallback if secondary was requested but not defined
industry_gains = extract_segment(industry.gains, "Primary Product") if not target_category:
target_category = industry.primary_category
target_scope = "Primary Product" # Fallback to primary if secondary category object is missing
product_context = f"{target_category.name}: {target_category.description}" if target_category else "Intelligente Robotik-Lösungen"
# 2. Extract specific segments from industry pains/gains based on scope
industry_pains = extract_segment(industry.pains, target_scope)
industry_gains = extract_segment(industry.gains, target_scope)
# Fallback: If specific scope is empty (e.g. no Secondary Pains defined), try Primary
if not industry_pains and target_scope == "Secondary Product":
print(f" -> WARNING: No specific Pains found for {target_scope}. Fallback to Primary.")
industry_pains = extract_segment(industry.pains, "Primary Product")
industry_gains = extract_segment(industry.gains, "Primary Product")
# 3. Handle Persona Data # 3. Handle Persona Data
try: try:
@@ -48,32 +94,38 @@ def generate_prompt(industry: Industry, persona: Persona) -> str:
persona_gains = [persona.gains] if persona.gains else [] persona_gains = [persona.gains] if persona.gains else []
prompt = f""" prompt = f"""
Du bist ein scharfsinniger B2B-Strategieberater und exzellenter Copywriter. Du bist ein kompetenter Lösungsberater und brillanter Texter.
Deine Aufgabe: Erstelle hochpräzise, "scharfe" Marketing-Textbausteine für einen Outreach an Entscheider. AUFGABE: Erstelle 3 Textblöcke (Subject, Introduction_Textonly, Industry_References_Textonly) für eine E-Mail an einen Entscheider.
--- STRATEGISCHER RAHMEN --- --- KONTEXT ---
ZIELUNTERNEHMEN (Branche): {industry.name} ZIELBRANCHE: {industry.name}
BRANCHEN-KONTEXT: {industry.description or 'Keine spezifische Beschreibung'} BRANCHEN-HERAUSFORDERUNGEN (PAIN POINTS):
BRANCEHN-HERAUSFORDERUNGEN: {industry_pains} {industry_pains}
ANGESTREBTE MEHRWERTE: {industry_gains}
ZIELPERSON (Rolle): {persona.name} FOKUS-PRODUKT (LÖSUNG):
PERSÖNLICHER DRUCK (Pains der Rolle):
{chr(10).join(['- ' + p for p in persona_pains])}
GEWÜNSCHTE ERFOLGE (Gains der Rolle):
{chr(10).join(['- ' + g for g in persona_gains])}
ANGEBOTENE LÖSUNG (Produkt-Fokus):
{product_context} {product_context}
--- DEIN AUFTRAG --- ANSPRECHPARTNER (ROLLE): {persona.name}
Erstelle ein JSON-Objekt mit 3 Textbausteinen, die den persönlichen Druck des Empfängers mit den strategischen Notwendigkeiten seiner Branche und der technologischen Lösung verknüpfen. PERSÖNLICHE HERAUSFORDERUNGEN DES ANSPRECHPARTNERS (PAIN POINTS):
Tonalität: Wertschätzend, auf Augenhöhe, scharfsinnig, absolut NICHT marktschreierisch. {chr(10).join(['- ' + str(p) for p in persona_pains])}
1. "subject": Eine Betreffzeile (Max 6 Wörter), die den Finger direkt in eine Wunde (Pain) legt oder ein hohes Ziel (Gain) verspricht. --- DEINE AUFGABE ---
2. "intro": Einleitung (2-3 Sätze). Verbinde die spezifische Branchen-Herausforderung mit der persönlichen Verantwortung des Empfängers. Er muss sich sofort verstanden fühlen. 1. **Subject:** Formuliere eine kurze Betreffzeile (max. 6 Wörter). Richte sie **direkt an einem der persönlichen Pain Points** des Ansprechpartners oder dem zentralen Branchen-Pain. Sei scharfsinnig, nicht werblich.
3. "social_proof": Ein Beweissatz, der zeigt, dass diese Lösung in der Branche {industry.name} bereits reale Probleme (z.B. Personalmangel, Dokumentationsdruck) gelöst hat. Nenne keine konkreten Firmennamen, aber quantifizierbare Effekte.
2. **Introduction_Textonly:** Formuliere einen Einleitungstext (2-3 Sätze).
- **Satz 1 (Die Brücke):** Knüpfe an die (uns unbekannte) operative Herausforderung an. Beschreibe subtil den Nutzen einer Lösung, ohne das Produkt (Roboter) plump zu nennen.
- **Satz 2 (Die Relevanz):** Schaffe die Relevanz für die Zielperson, indem du das Thema mit einem ihrer persönlichen Pain Points verknüpfst (z.B. "Für Sie als {persona.name} ist dabei entscheidend...").
3. **Industry_References_Textonly:** Formuliere einen **strategischen Referenz-Block (ca. 2-3 Sätze)** nach folgendem Muster:
- **Satz 1 (Social Proof):** Beginne direkt mit dem Nutzen, den vergleichbare Unternehmen in der Branche {industry.name} bereits erzielen. (Erfinde keine Firmennamen, sprich von "Führenden Einrichtungen" oder "Vergleichbaren Häusern").
- **Satz 2 (Rollen-Relevanz):** Schaffe den direkten Nutzen für die Zielperson. Formuliere z.B. 'Dieser Wissensvorsprung hilft uns, Ihre [persönlicher Pain Point der Rolle] besonders effizient zu lösen.'
--- BEISPIEL FÜR EINEN PERFEKTEN OUTPUT ---
{{
"Subject": "Kostenkontrolle im Service",
"Introduction_Textonly": "Genau bei der Optimierung dieser Serviceprozesse können erhebliche Effizienzgewinne erzielt werden. Für Sie als Finanzleiter ist dabei die Sicherstellung der Profitabilität bei gleichzeitiger Kostentransparenz von zentraler Bedeutung.",
"Industry_References_Textonly": "Vergleichbare Unternehmen profitieren bereits massiv von automatisierten Prozessen. Unsere Erfahrung zeigt, dass die grundlegenden Herausforderungen in der Einsatzplanung oft branchenübergreifend ähnlich sind. Dieser Wissensvorsprung hilft uns, Ihre Ziele bei der Kostenkontrolle und Profitabilitätssteigerung besonders effizient zu unterstützen."
}}
--- FORMAT --- --- FORMAT ---
Antworte NUR mit einem validen JSON-Objekt. Antworte NUR mit einem validen JSON-Objekt.
@@ -88,7 +140,7 @@ Format:
def mock_call(prompt: str): def mock_call(prompt: str):
"""Simulates an API call for dry runs.""" """Simulates an API call for dry runs."""
print(f"\n--- [MOCK] GENERATING PROMPT ---\n{prompt[:300]}...\n--------------------------------") print(f"\n--- [MOCK] GENERATING PROMPT ---\n{prompt[:800]}...\n--------------------------------")
return { return {
"subject": "[MOCK] Effizienzsteigerung in der Produktion", "subject": "[MOCK] Effizienzsteigerung in der Produktion",
"intro": "[MOCK] Als Produktionsleiter wissen Sie, wie teuer Stillstand ist. Unsere Roboter helfen.", "intro": "[MOCK] Als Produktionsleiter wissen Sie, wie teuer Stillstand ist. Unsere Roboter helfen.",
@@ -149,10 +201,14 @@ def run_matrix_generation(dry_run: bool = True, force: bool = False, specific_in
print(f"Found {len(industries)} Industries and {len(personas)} Personas.") print(f"Found {len(industries)} Industries and {len(personas)} Personas.")
print(f"Mode: {'DRY RUN (No API calls, no DB writes)' if dry_run else 'LIVE - GEMINI GENERATION'}") print(f"Mode: {'DRY RUN (No API calls, no DB writes)' if dry_run else 'LIVE - GEMINI GENERATION'}")
# Pre-load categories to avoid lazy load issues if detached
# (SQLAlchemy session is open, so should be fine, but good practice)
total_combinations = len(industries) * len(personas) total_combinations = len(industries) * len(personas)
processed = 0 processed = 0
for ind in industries: for ind in industries:
print(f"\n>>> Processing Industry: {ind.name} (Ops Secondary: {ind.ops_focus_secondary})")
for pers in personas: for pers in personas:
processed += 1 processed += 1
print(f"[{processed}/{total_combinations}] Check: {ind.name} x {pers.name}") print(f"[{processed}/{total_combinations}] Check: {ind.name} x {pers.name}")

View File

@@ -67,6 +67,7 @@ def extract_select(prop):
return prop.get("select", {}).get("name", "") if prop.get("select") else "" return prop.get("select", {}).get("name", "") if prop.get("select") else ""
def extract_number(prop): def extract_number(prop):
if not prop: return None
return prop.get("number") return prop.get("number")
def sync_categories(token, session): def sync_categories(token, session):
@@ -135,6 +136,11 @@ def sync_industries(token, session):
industry.name = name industry.name = name
industry.description = extract_rich_text(props.get("Definition")) industry.description = extract_rich_text(props.get("Definition"))
# New: Map Pains & Gains explicitly
industry.pains = extract_rich_text(props.get("Pains"))
industry.gains = extract_rich_text(props.get("Gains"))
industry.notes = extract_rich_text(props.get("Notes"))
status = extract_select(props.get("Status")) status = extract_select(props.get("Status"))
industry.status_notion = status industry.status_notion = status
industry.is_focus = (status == "P1 Focus Industry") industry.is_focus = (status == "P1 Focus Industry")
@@ -147,6 +153,9 @@ def sync_industries(token, session):
industry.scraper_search_term = extract_select(props.get("Scraper Search Term")) # <-- FIXED HERE industry.scraper_search_term = extract_select(props.get("Scraper Search Term")) # <-- FIXED HERE
industry.scraper_keywords = extract_rich_text(props.get("Scraper Keywords")) industry.scraper_keywords = extract_rich_text(props.get("Scraper Keywords"))
industry.standardization_logic = extract_rich_text(props.get("Standardization Logic")) industry.standardization_logic = extract_rich_text(props.get("Standardization Logic"))
# New Field: Ops Focus Secondary (Checkbox)
industry.ops_focus_secondary = props.get("Ops Focus: Secondary", {}).get("checkbox", False)
# Relation: Primary Product Category # Relation: Primary Product Category
relation = props.get("Primary Product Category", {}).get("relation", []) relation = props.get("Primary Product Category", {}).get("relation", [])
@@ -157,6 +166,16 @@ def sync_industries(token, session):
industry.primary_category_id = cat.id industry.primary_category_id = cat.id
else: else:
logger.warning(f"Related category {related_id} not found for industry {name}") logger.warning(f"Related category {related_id} not found for industry {name}")
# Relation: Secondary Product Category
relation_sec = props.get("Secondary Product", {}).get("relation", [])
if relation_sec:
related_id = relation_sec[0]["id"]
cat = session.query(RoboticsCategory).filter(RoboticsCategory.notion_id == related_id).first()
if cat:
industry.secondary_category_id = cat.id
else:
logger.warning(f"Related Secondary category {related_id} not found for industry {name}")
count += 1 count += 1