feat: Build complete POC for Butler model (client, matrix, daemon)
This commit is contained in:
152
connector-superoffice/build_matrix.py
Normal file
152
connector-superoffice/build_matrix.py
Normal file
@@ -0,0 +1,152 @@
|
||||
import sqlite3
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
DB_FILE = "marketing_matrix.db"
|
||||
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
||||
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
|
||||
NOTION_DB_INDUSTRIES = "2ec88f4285448014ab38ea664b4c2b81"
|
||||
|
||||
# --- MAPPINGS ---
|
||||
# SuperOffice ID -> Notion Vertical Name
|
||||
VERTICAL_MAP = {
|
||||
23: "Logistics - Warehouse"
|
||||
}
|
||||
|
||||
# SuperOffice ID -> Persona Name & Pains (aus unserer Definition)
|
||||
ROLE_MAP = {
|
||||
19: {"name": "Operativer Entscheider", "pains": "Zuverlässigkeit, einfache Bedienbarkeit, Personaleinsatz-Optimierung, minimale Störungen"},
|
||||
20: {"name": "Infrastruktur-Verantwortlicher", "pains": "Technische Machbarkeit, IT-Sicherheit, Integration, Brandschutz"},
|
||||
21: {"name": "Wirtschaftlicher Entscheider", "pains": "ROI, Amortisationszeit, Kostenstruktur, Einsparpotenziale"},
|
||||
22: {"name": "Innovations-Treiber", "pains": "Wettbewerbsfähigkeit, Modernisierung, Employer Branding, Kundenerlebnis"}
|
||||
}
|
||||
|
||||
# --- DATABASE SETUP ---
|
||||
def init_db():
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
c = conn.cursor()
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS text_blocks
|
||||
(vertical_id INTEGER, role_id INTEGER,
|
||||
subject TEXT, intro TEXT, social_proof TEXT,
|
||||
PRIMARY KEY (vertical_id, role_id))''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("✅ Database initialized.")
|
||||
|
||||
# --- NOTION FETCHER ---
|
||||
def get_vertical_pains_gains(vertical_name):
|
||||
url = f"https://api.notion.com/v1/databases/{NOTION_DB_INDUSTRIES}/query"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {NOTION_API_KEY}",
|
||||
"Notion-Version": "2022-06-28",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {
|
||||
"filter": {
|
||||
"property": "Vertical",
|
||||
"title": {"contains": vertical_name}
|
||||
}
|
||||
}
|
||||
resp = requests.post(url, headers=headers, json=payload)
|
||||
if resp.status_code == 200:
|
||||
results = resp.json().get("results", [])
|
||||
if results:
|
||||
props = results[0]['properties']
|
||||
pains = props.get('Pains', {}).get('rich_text', [])
|
||||
gains = props.get('Gains', {}).get('rich_text', [])
|
||||
return {
|
||||
"pains": pains[0]['plain_text'] if pains else "",
|
||||
"gains": gains[0]['plain_text'] if gains else ""
|
||||
}
|
||||
print(f"⚠️ Warning: No data found for {vertical_name}")
|
||||
return {"pains": "N/A", "gains": "N/A"}
|
||||
|
||||
# --- GEMINI GENERATOR ---
|
||||
def generate_text(vertical_name, v_data, role_id, role_data):
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={GEMINI_API_KEY}"
|
||||
|
||||
prompt = f"""
|
||||
Du bist ein B2B-Copywriter. Erstelle 3 Textbausteine für eine Cold-Outreach E-Mail.
|
||||
|
||||
KONTEXT:
|
||||
Branche: {vertical_name}
|
||||
Branchen-Pains: {v_data['pains']}
|
||||
Lösung-Gains: {v_data['gains']}
|
||||
|
||||
Rolle: {role_data['name']}
|
||||
Rollen-Pains: {role_data['pains']}
|
||||
|
||||
AUFGABE:
|
||||
1. Subject: Betreffzeile (max 40 Zeichen!). Knackig, Pain-bezogen.
|
||||
2. Intro: Einleitungssatz (max 40 Zeichen!). Brücke Pain -> Lösung.
|
||||
3. SocialProof: Referenzsatz (max 40 Zeichen!). "Wir arbeiten mit X..."
|
||||
|
||||
FORMAT (JSON):
|
||||
{{ "Subject": "...", "Intro": "...", "SocialProof": "..." }}
|
||||
"""
|
||||
|
||||
payload = {
|
||||
"contents": [{"parts": [{"text": prompt}]}],
|
||||
"generationConfig": {"responseMimeType": "application/json"}
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.post(url, json=payload)
|
||||
if resp.status_code == 200:
|
||||
return json.loads(resp.json()['candidates'][0]['content']['parts'][0]['text'])
|
||||
except Exception as e:
|
||||
print(f"Gemini Error: {e}")
|
||||
return None
|
||||
|
||||
# --- MAIN ---
|
||||
def run_matrix():
|
||||
init_db()
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
c = conn.cursor()
|
||||
|
||||
# Iterate Verticals
|
||||
for v_id, v_name in VERTICAL_MAP.items():
|
||||
print(f"\nProcessing Vertical: {v_name} (ID: {v_id})")
|
||||
v_data = get_vertical_pains_gains(v_name)
|
||||
|
||||
# Iterate Roles
|
||||
for r_id, r_data in ROLE_MAP.items():
|
||||
print(f" > Generating for Role: {r_data['name']} (ID: {r_id})...", end="", flush=True)
|
||||
|
||||
# Check if exists (optional: skip if exists)
|
||||
# ...
|
||||
|
||||
text_block = generate_text(v_name, v_data, r_id, r_data)
|
||||
|
||||
if text_block:
|
||||
# Robustness: Handle list return from Gemini
|
||||
if isinstance(text_block, list):
|
||||
if len(text_block) > 0 and isinstance(text_block[0], dict):
|
||||
text_block = text_block[0] # Take first item if list of dicts
|
||||
else:
|
||||
print(" ❌ Failed (Unexpected JSON format: List without dicts).")
|
||||
continue
|
||||
|
||||
# Cut to 40 chars hard limit (Safety)
|
||||
subj = text_block.get("Subject", "")[:40]
|
||||
intro = text_block.get("Intro", "Intro")[:40] # Fallback key name check
|
||||
if "Introduction_Textonly" in text_block: intro = text_block["Introduction_Textonly"][:40]
|
||||
proof = text_block.get("SocialProof", "")[:40]
|
||||
if "Industry_References_Textonly" in text_block: proof = text_block["Industry_References_Textonly"][:40]
|
||||
|
||||
c.execute("INSERT OR REPLACE INTO text_blocks VALUES (?, ?, ?, ?, ?)",
|
||||
(v_id, r_id, subj, intro, proof))
|
||||
conn.commit()
|
||||
print(" ✅ Done.")
|
||||
else:
|
||||
print(" ❌ Failed.")
|
||||
|
||||
conn.close()
|
||||
print("\nMatrix generation complete.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_matrix()
|
||||
Reference in New Issue
Block a user