From bfb96118b006e0a907625df6d4ed61178319eb7f Mon Sep 17 00:00:00 2001 From: Floke Date: Tue, 24 Feb 2026 07:13:49 +0000 Subject: [PATCH] =?UTF-8?q?[2ff88f42]=20einf=C3=BCgen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit einfügen --- .dev_session/SESSION_INFO | 2 +- GEMINI.md | 11 +++++++ MIGRATION_PLAN.md | 13 ++++++++ check_erding_openers.py | 16 ++++++++++ check_klinikum_erding.py | 16 ++++++++++ check_matrix_indoor.py | 23 ++++++++++++++ check_matrix_results.py | 24 +++++++++++++++ company-explorer/backend/database.py | 4 +++ .../backend/scripts/generate_matrix.py | 26 ++++++++++++---- .../backend/scripts/sync_notion_personas.py | 21 +++++++++++-- inspect_persona_db.py | 24 +++++++++++++++ inspect_persona_db_v2.py | 30 +++++++++++++++++++ list_industries_db.py | 12 ++++++++ migrate_personas_v2.py | 30 +++++++++++++++++++ verify_db.py | 13 ++++++++ 15 files changed, 255 insertions(+), 10 deletions(-) create mode 100644 check_erding_openers.py create mode 100644 check_klinikum_erding.py create mode 100644 check_matrix_indoor.py create mode 100644 check_matrix_results.py create mode 100644 inspect_persona_db.py create mode 100644 inspect_persona_db_v2.py create mode 100644 list_industries_db.py create mode 100644 migrate_personas_v2.py create mode 100644 verify_db.py diff --git a/.dev_session/SESSION_INFO b/.dev_session/SESSION_INFO index 9371c096..fc2c992c 100644 --- a/.dev_session/SESSION_INFO +++ b/.dev_session/SESSION_INFO @@ -1 +1 @@ -{"task_id": "31188f42-8544-80f0-b21a-c6beaa9ea3a1", "token": "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", "session_start_time": "2026-02-24T06:47:22.751414"} \ No newline at end of file +{"task_id": "2ff88f42-8544-8050-8245-c3bb852058f4", "token": "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", "session_start_time": "2026-02-24T07:13:35.422817"} \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md index 0efe1aae..d27fe91d 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -105,6 +105,17 @@ The system architecture has evolved from a CLI-based toolset to a modern web app * **Problem:** Users didn't see when a background job finished. * **Solution:** Implementing a polling mechanism (`setInterval`) tied to a `isProcessing` state is superior to static timeouts for long-running AI tasks. +7. **Hyper-Personalized Marketing Engine (v3.2) - "Deep Persona Injection":** + * **Problem:** Marketing texts were too generic and didn't reflect the specific psychological or operative profile of the different target roles (e.g., CFO vs. Facility Manager). + * **Solution (Deep Sync & Prompt Hardening):** + 1. **Extended Schema:** Added `description`, `convincing_arguments`, and `kpis` to the `Persona` database model to store richer profile data. + 2. **Notion Master Sync:** Updated the synchronization logic to pull these deep insights directly from the Notion "Personas / Roles" database. + 3. **Role-Centric Prompts:** The `MarketingMatrix` generator was re-engineered to inject the persona's "Mindset" and "KPIs" into the prompt. + * **Example (Healthcare):** + - **Infrastructure Lead:** Focuses now on "IT Security", "DSGVO Compliance", and "WLAN integration". + - **Economic Buyer (CFO):** Focuses on "ROI Amortization", "Reduction of Overtime", and "Flexible Financing (RaaS)". + * **Verification:** Verified that the transition from a company-specific **Opener** (e.g., observing staff shortages at Klinikum Erding) to the **Role-specific Intro** (e.g., pitching transport robots to reduce walking distances for nursing directors) is seamless and logical. + ## Metric Parser - Regression Tests To ensure the stability and accuracy of the metric extraction logic, a dedicated test suite (`/company-explorer/backend/tests/test_metric_parser.py`) has been created. It covers the following critical, real-world bug fixes: diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md index ddbc9fb1..cea41f31 100644 --- a/MIGRATION_PLAN.md +++ b/MIGRATION_PLAN.md @@ -339,6 +339,19 @@ PERSÖNLICHE HERAUSFORDERUNGEN: {persona_pains} **Konzept:** Strikte Trennung zwischen `[Primary Product]` und `[Secondary Product]` zur Vermeidung logischer Brüche. +### 17.9 Deep Persona Injection (Update Feb 24, 2026) + +**Ziel:** Maximale Relevanz durch Einbezug psychografischer und operativer Rollen-Details ("Voll ins Zentrum"). + +**Die Erweiterung:** +- **Vollständiger Daten-Sync:** Übernahme von `Beschreibung/Denkweise`, `Was ihn überzeugt` und `KPIs` aus der Notion "Personas / Roles" Datenbank in das lokale Schema. +- **Rollenspezifische Tonalität:** Die KI nutzt diese Details, um den "Ton" der jeweiligen Persona perfekt zu treffen (z.B. technischer Fokus beim Infrastruktur-Leiter vs. betriebswirtschaftlicher Fokus beim CFO). + +**Beispiel-Kaskade (Klinikum Erding):** +1. **Opener:** "Klinikum Erding trägt maßgeblich zur regionalen Versorgung bei... Dokumentation lückenloser Hygiene stellt eine operative Herausforderung dar." +2. **Matrix-Anschluss (Infrastruktur):** "...minimieren Ausfallzeiten um 80-90% durch proaktives Monitoring... planbare Wartung und Transparenz durch feste **SLAs**." (Direkter Bezug auf hinterlegte Überzeugungsargumente). +3. **Matrix-Anschluss (Wirtschaftlich):** "...Reduktion operativer Personalkosten um 10-25%... wirkt sich direkt auf **ROI** und **Amortisationszeit** aus." (Direkter Bezug auf hinterlegte KPIs). + --- ## 18. Next Steps & Todos (Post-Migration) diff --git a/check_erding_openers.py b/check_erding_openers.py new file mode 100644 index 00000000..9e531aca --- /dev/null +++ b/check_erding_openers.py @@ -0,0 +1,16 @@ +import sqlite3 + +DB_PATH = "/app/companies_v3_fixed_2.db" +conn = sqlite3.connect(DB_PATH) +cursor = conn.cursor() + +cursor.execute("SELECT name, ai_opener, ai_opener_secondary, industry_ai FROM companies WHERE name LIKE '%Erding%'") +row = cursor.fetchone() +if row: + print(f"Company: {row[0]}") + print(f"Industry: {row[3]}") + print(f"Opener Primary: {row[1]}") + print(f"Opener Secondary: {row[2]}") +else: + print("Company not found.") +conn.close() diff --git a/check_klinikum_erding.py b/check_klinikum_erding.py new file mode 100644 index 00000000..4d37cffe --- /dev/null +++ b/check_klinikum_erding.py @@ -0,0 +1,16 @@ +import sqlite3 + +DB_PATH = "/app/companies_v3_fixed_2.db" +conn = sqlite3.connect(DB_PATH) +cursor = conn.cursor() + +cursor.execute("SELECT name, ai_opener, ai_opener_secondary, industry_ai FROM companies WHERE name LIKE '%Klinikum Landkreis Erding%'") +row = cursor.fetchone() +if row: + print(f"Company: {row[0]}") + print(f"Industry: {row[3]}") + print(f"Opener Primary: {row[1]}") + print(f"Opener Secondary: {row[2]}") +else: + print("Company not found.") +conn.close() diff --git a/check_matrix_indoor.py b/check_matrix_indoor.py new file mode 100644 index 00000000..b8e1f3e4 --- /dev/null +++ b/check_matrix_indoor.py @@ -0,0 +1,23 @@ +import sqlite3 + +DB_PATH = "/app/companies_v3_fixed_2.db" +conn = sqlite3.connect(DB_PATH) +cursor = conn.cursor() + +query = """ +SELECT i.name, p.name, m.subject, m.intro, m.social_proof +FROM marketing_matrix m +JOIN industries i ON m.industry_id = i.id +JOIN personas p ON m.persona_id = p.id +WHERE i.name = 'Leisure - Indoor Active' +""" + +cursor.execute(query) +rows = cursor.fetchall() +for row in rows: + print(f"Industry: {row[0]} | Persona: {row[1]}") + print(f" Subject: {row[2]}") + print(f" Intro: {row[3]}") + print(f" Social Proof: {row[4]}") + print("-" * 50) +conn.close() diff --git a/check_matrix_results.py b/check_matrix_results.py new file mode 100644 index 00000000..c407559e --- /dev/null +++ b/check_matrix_results.py @@ -0,0 +1,24 @@ +import sqlite3 +import json + +DB_PATH = "/app/companies_v3_fixed_2.db" +conn = sqlite3.connect(DB_PATH) +cursor = conn.cursor() + +query = """ +SELECT i.name, p.name, m.subject, m.intro, m.social_proof +FROM marketing_matrix m +JOIN industries i ON m.industry_id = i.id +JOIN personas p ON m.persona_id = p.id +WHERE i.name = 'Healthcare - Hospital' +""" + +cursor.execute(query) +rows = cursor.fetchall() +for row in rows: + print(f"Industry: {row[0]} | Persona: {row[1]}") + print(f" Subject: {row[2]}") + print(f" Intro: {row[3]}") + print(f" Social Proof: {row[4]}") + print("-" * 50) +conn.close() diff --git a/company-explorer/backend/database.py b/company-explorer/backend/database.py index 7c60d212..41af8890 100644 --- a/company-explorer/backend/database.py +++ b/company-explorer/backend/database.py @@ -205,8 +205,12 @@ class Persona(Base): id = Column(Integer, primary_key=True, index=True) name = Column(String, unique=True, index=True) # Matches the 'role' string in JobRolePattern + description = Column(Text, nullable=True) # NEW: Role description / how they think pains = Column(Text, nullable=True) # JSON list or multiline string gains = Column(Text, nullable=True) # JSON list or multiline string + convincing_arguments = Column(Text, nullable=True) # NEW: What convinces them + typical_positions = Column(Text, nullable=True) # NEW: Typical titles + kpis = Column(Text, nullable=True) # NEW: Relevant KPIs created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) diff --git a/company-explorer/backend/scripts/generate_matrix.py b/company-explorer/backend/scripts/generate_matrix.py index b0d562f4..da5928cc 100644 --- a/company-explorer/backend/scripts/generate_matrix.py +++ b/company-explorer/backend/scripts/generate_matrix.py @@ -93,9 +93,16 @@ def generate_prompt(industry: Industry, persona: Persona) -> str: persona_pains = [persona.pains] if persona.pains else [] persona_gains = [persona.gains] if persona.gains else [] + # Advanced Persona Context + persona_context = f""" +BESCHREIBUNG/DENKWEISE: {persona.description or 'Nicht definiert'} +WAS DIESE PERSON ÜBERZEUGT: {persona.convincing_arguments or 'Nicht definiert'} +RELEVANTE KPIs: {persona.kpis or 'Nicht definiert'} +""" + prompt = f""" -Du bist ein kompetenter Lösungsberater und brillanter Texter. -AUFGABE: Erstelle 3 Textblöcke (Subject, Introduction_Textonly, Industry_References_Textonly) für eine E-Mail an einen Entscheider. +Du bist ein kompetenter Lösungsberater und brillanter Texter für B2B-Marketing. +AUFGABE: Erstelle 3 hoch-personalisierte Textblöcke (Subject, Introduction_Textonly, Industry_References_Textonly) für eine E-Mail an einen Entscheider. --- KONTEXT --- ZIELBRANCHE: {industry.name} @@ -106,20 +113,27 @@ FOKUS-PRODUKT (LÖSUNG): {product_context} ANSPRECHPARTNER (ROLLE): {persona.name} -PERSÖNLICHE HERAUSFORDERUNGEN DES ANSPRECHPARTNERS (PAIN POINTS): +{persona_context} + +SPEZIFISCHE HERAUSFORDERUNGEN (PAIN POINTS) DER ROLLE: {chr(10).join(['- ' + str(p) for p in persona_pains])} +SPEZIFISCHE NUTZEN (GAINS) DER ROLLE: +{chr(10).join(['- ' + str(g) for g in persona_gains])} + --- DEINE AUFGABE --- +Deine Texte müssen "voll ins Zentrum" der Rolle treffen. Vermeide oberflächliche Floskeln. Nutze die Details zur Denkweise, den KPIs und den Überzeugungsargumenten, um eine tiefgreifende Relevanz zu erzeugen. + 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. 2. **Introduction_Textonly:** Formuliere einen prägnanten Einleitungstext (max. 2 Sätze). - **WICHTIG:** Gehe davon aus, dass die spezifische Herausforderung des Kunden bereits im Satz davor [Opener] genannt wurde. **Wiederhole die Herausforderung NICHT.** - - **Satz 1 (Die Lösung & der Gain):** Beginne direkt mit der Lösung. Nenne die im Kontext `FOKUS-PRODUKT` definierte **Produktkategorie** (z.B. "automatisierte Reinigungsroboter") und verbinde sie mit dem zentralen Nutzen (Gain) aus den `BRANCHEN-HERAUSFORDERUNGEN`. Beispiel: "Genau hier setzen unsere automatisierten Reinigungsroboter an, indem sie eine lückenlose und auditsichere Hygiene gewährleisten." - - **Satz 2 (Die Relevanz):** Stelle die Relevanz für die Zielperson her, indem du einen ihrer `PERSÖNLICHE HERAUSFORDERUNGEN` adressierst. Beispiel: "Für Sie als Infrastruktur-Verantwortlicher bedeutet dies vor allem eine reibungslose Integration in bestehende Abläufe, ohne den Betrieb zu stören." + - **Satz 1 (Die Lösung & der Gain):** Beginne direkt mit der Lösung. Nenne die im Kontext `FOKUS-PRODUKT` definierte **Produktkategorie** (z.B. "automatisierte Reinigungsroboter") und verbinde sie mit einem Nutzen, der für diese Rolle (siehe `WAS DIESE PERSON ÜBERZEUGT` und `GAINS`) besonders kritisch ist. + - **Satz 2 (Die Relevanz):** Stelle die Relevanz für die Zielperson her, indem du eine ihrer `PERSÖNLICHE HERAUSFORDERUNGEN` oder `KPIs` adressierst. Beispiel: "Für Sie als [Rolle] bedeutet dies vor allem [Nutzen bezogen auf KPI oder Pain]." 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.' + - **Satz 2 (Rollen-Relevanz):** Schaffe den direkten Nutzen für die Zielperson. Nutze dabei die Informationen aus `BESCHREIBUNG/DENKWEISE`, um den Ton perfekt zu treffen. --- BEISPIEL FÜR EINEN PERFEKTEN OUTPUT --- {{ diff --git a/company-explorer/backend/scripts/sync_notion_personas.py b/company-explorer/backend/scripts/sync_notion_personas.py index a88d854f..ab6f5799 100644 --- a/company-explorer/backend/scripts/sync_notion_personas.py +++ b/company-explorer/backend/scripts/sync_notion_personas.py @@ -16,13 +16,14 @@ logger = logging.getLogger(__name__) NOTION_TOKEN_FILE = "/app/notion_token.txt" # Sector & Persona Master DB -PERSONAS_DB_ID = "2e288f42-8544-8113-b878-ec99c8a02a6b" +PERSONAS_DB_ID = "30588f42-8544-80c3-8919-e22d74d945ea" VALID_ARCHETYPES = { "Wirtschaftlicher Entscheider", "Operativer Entscheider", "Infrastruktur-Verantwortlicher", - "Innovations-Treiber" + "Innovations-Treiber", + "Influencer" } def load_notion_token(): @@ -65,6 +66,10 @@ def extract_title(prop): if not prop: return "" return "".join([t.get("plain_text", "") for t in prop.get("title", [])]) +def extract_rich_text(prop): + if not prop: return "" + return "".join([t.get("plain_text", "") for t in prop.get("rich_text", [])]) + def extract_rich_text_to_list(prop): """ Extracts rich text and converts bullet points/newlines into a list of strings. @@ -94,7 +99,8 @@ def sync_personas(token, session): for page in pages: props = page.get("properties", {}) - name = extract_title(props.get("Name")) + # The title property is 'Role' in the new DB, not 'Name' + name = extract_title(props.get("Role")) if name not in VALID_ARCHETYPES: logger.debug(f"Skipping '{name}' (Not a target Archetype)") @@ -105,6 +111,11 @@ def sync_personas(token, session): pains_list = extract_rich_text_to_list(props.get("Pains")) gains_list = extract_rich_text_to_list(props.get("Gains")) + description = extract_rich_text(props.get("Rollenbeschreibung")) + convincing_arguments = extract_rich_text(props.get("Was ihn überzeugt")) + typical_positions = extract_rich_text(props.get("Typische Positionen")) + kpis = extract_rich_text(props.get("KPIs")) + # Upsert Logic persona = session.query(Persona).filter(Persona.name == name).first() if not persona: @@ -116,6 +127,10 @@ def sync_personas(token, session): persona.pains = json.dumps(pains_list, ensure_ascii=False) persona.gains = json.dumps(gains_list, ensure_ascii=False) + persona.description = description + persona.convincing_arguments = convincing_arguments + persona.typical_positions = typical_positions + persona.kpis = kpis count += 1 diff --git a/inspect_persona_db.py b/inspect_persona_db.py new file mode 100644 index 00000000..d0d9cc3d --- /dev/null +++ b/inspect_persona_db.py @@ -0,0 +1,24 @@ +import sys +import os +import requests +import json + +NOTION_TOKEN_FILE = "/app/notion_token.txt" +PERSONAS_DB_ID = "2e288f42-8544-8113-b878-ec99c8a02a6b" + +def load_notion_token(): + with open(NOTION_TOKEN_FILE, "r") as f: + return f.read().strip() + +def query_notion_db(token, db_id): + url = f"https://api.notion.com/v1/databases/{db_id}/query" + headers = { + "Authorization": f"Bearer {token}", + "Notion-Version": "2022-06-28" + } + response = requests.post(url, headers=headers) + return response.json() + +token = load_notion_token() +data = query_notion_db(token, PERSONAS_DB_ID) +print(json.dumps(data.get("results", [])[0], indent=2)) \ No newline at end of file diff --git a/inspect_persona_db_v2.py b/inspect_persona_db_v2.py new file mode 100644 index 00000000..f954e69e --- /dev/null +++ b/inspect_persona_db_v2.py @@ -0,0 +1,30 @@ +import sys +import os +import requests +import json + +NOTION_TOKEN_FILE = "/app/notion_token.txt" +PERSONAS_DB_ID = "30588f42-8544-80c3-8919-e22d74d945ea" + +def load_notion_token(): + with open(NOTION_TOKEN_FILE, "r") as f: + return f.read().strip() + +def query_notion_db(token, db_id): + url = f"https://api.notion.com/v1/databases/{db_id}/query" + headers = { + "Authorization": f"Bearer {token}", + "Notion-Version": "2022-06-28" + } + response = requests.post(url, headers=headers) + return response.json() + +token = load_notion_token() +data = query_notion_db(token, PERSONAS_DB_ID) +results = data.get("results", []) +for res in results: + props = res.get("properties", {}) + role = "".join([t.get("plain_text", "") for t in props.get("Role", {}).get("title", [])]) + print(f"Role: {role}") + print(json.dumps(props, indent=2)) + print("-" * 40) \ No newline at end of file diff --git a/list_industries_db.py b/list_industries_db.py new file mode 100644 index 00000000..bf8102cd --- /dev/null +++ b/list_industries_db.py @@ -0,0 +1,12 @@ +import sqlite3 + +DB_PATH = "/app/companies_v3_fixed_2.db" +conn = sqlite3.connect(DB_PATH) +cursor = conn.cursor() + +cursor.execute("SELECT name FROM industries") +industries = cursor.fetchall() +print("Available Industries:") +for ind in industries: + print(f"- {ind[0]}") +conn.close() diff --git a/migrate_personas_v2.py b/migrate_personas_v2.py new file mode 100644 index 00000000..c0906eec --- /dev/null +++ b/migrate_personas_v2.py @@ -0,0 +1,30 @@ +import sqlite3 +import os + +DB_PATH = "/app/companies_v3_fixed_2.db" + +def migrate_personas(): + print(f"Adding new columns to 'personas' table in {DB_PATH}...") + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + columns_to_add = [ + ("description", "TEXT"), + ("convincing_arguments", "TEXT"), + ("typical_positions", "TEXT"), + ("kpis", "TEXT") + ] + + for col_name, col_type in columns_to_add: + try: + cursor.execute(f"ALTER TABLE personas ADD COLUMN {col_name} {col_type}") + print(f" Added column: {col_name}") + except sqlite3.OperationalError: + print(f" Column {col_name} already exists.") + + conn.commit() + conn.close() + print("Migration complete.") + +if __name__ == "__main__": + migrate_personas() diff --git a/verify_db.py b/verify_db.py new file mode 100644 index 00000000..975f483e --- /dev/null +++ b/verify_db.py @@ -0,0 +1,13 @@ +import sqlite3 + +DB_PATH = "/app/companies_v3_fixed_2.db" +conn = sqlite3.connect(DB_PATH) +cursor = conn.cursor() +cursor.execute("SELECT name, description, convincing_arguments FROM personas") +rows = cursor.fetchall() +for row in rows: + print(f"Persona: {row[0]}") + print(f" Description: {row[1][:100]}...") + print(f" Convincing: {row[2][:100]}...") + print("-" * 20) +conn.close()