From e1d115e0bab89ff10ce5783d56fa487dfccbaeb5 Mon Sep 17 00:00:00 2001 From: Floke Date: Sun, 11 Jan 2026 11:57:43 +0000 Subject: [PATCH] feat(notion): Implement relational Competitive Radar import - Added import_relational_radar.py for bidirectional database structure in Notion. - Added refresh_references.py to populate analysis data with grounded facts via scraping. - Updated documentation for Competitive Radar v2.0. --- MIGRATION_REPORT_COMPETITOR_ANALYSIS.md | 17 +- Notion_Dashboard.md | 11 +- import_competitors_to_notion.py | 200 +++++++++++++++++++++ import_relational_radar.py | 230 ++++++++++++++++++++++++ refresh_references.py | 35 ++++ 5 files changed, 482 insertions(+), 11 deletions(-) create mode 100644 import_competitors_to_notion.py create mode 100644 import_relational_radar.py create mode 100644 refresh_references.py diff --git a/MIGRATION_REPORT_COMPETITOR_ANALYSIS.md b/MIGRATION_REPORT_COMPETITOR_ANALYSIS.md index add4ecf7..f9d957cf 100644 --- a/MIGRATION_REPORT_COMPETITOR_ANALYSIS.md +++ b/MIGRATION_REPORT_COMPETITOR_ANALYSIS.md @@ -69,12 +69,15 @@ Die App ist unter `/ca/` voll funktionsfähig und verfügt nun über eine "Groun * **Map-Reduce:** Statt eines Riesen-Prompts werden Konkurrenten parallel einzeln analysiert. Das skaliert linear. * **Logging:** Ein spezieller `log_debug` Helper schreibt direkt in `/app/Log_from_docker`, um Python-Logging-Probleme zu umgehen. -### Lessons Learned für die Ewigkeit +### 📊 Relationaler Notion Import (Competitive Radar v2.0) +Um die Analyse-Ergebnisse optimal nutzbar zu machen, wurde ein bidirektionaler Import-Prozess nach Notion implementiert (`import_relational_radar.py`). + +* **Architektur:** Statt Textblöcken werden drei vernetzte Datenbanken erstellt: + 1. **📦 Companies (Hub):** Stammdaten, USPs, Portfolio. + 2. **💣 Landmines (Satellite):** Einzelfragen und Angriffsvektoren, verknüpft mit der Company. + 3. **🏆 References (Satellite):** Konkrete Kundenprojekte, verknüpft mit der Company. +* **Dual-Way Relations:** Dank `dual_property` Konfiguration sind die Verknüpfungen in Notion sofort in beide Richtungen navigierbar (z.B. sieht man auf der Company-Seite sofort alle zugehörigen Landmines). +* **Daten-Qualität:** Durch die Map-Reduce Analyse und das gezielte Reference-Scraping werden nun echte Fakten statt KI-Halluzinationen importiert. -1. **F-STRINGS SIND VERBOTEN** für Prompts und komplexe Listen-Operationen. -2. **TRIPLE RAW QUOTES (`r"""..."""`)** sind der einzige sichere Weg für Strings in Docker-Umgebungen. -3. **DUAL SDK STRATEGY:** Legacy SDK für Stabilität (`gemini-2.0-flash`), Modern SDK für Spezial-Features. -4. **MAP-REDUCE:** Bei Listen > 3 Elementen niemals das LLM bitten, "alle auf einmal" zu bearbeiten. Immer zerlegen (Map) und aggregieren (Reduce). -5. **SCHEMA FIRST:** Frontend (`types.ts`) und Backend (`Pydantic`) müssen *vorher* abgeglichen werden. `422` bedeutet fast immer Schema-Mismatch. --- -*Dokumentation aktualisiert am 11.01.2026 nach erfolgreicher Skalierung auf 9+ Konkurrenten.* +*Dokumentation aktualisiert am 11.01.2026 nach Implementierung des relationalen Competitive Radars.* diff --git a/Notion_Dashboard.md b/Notion_Dashboard.md index fde20571..8cde4f86 100644 --- a/Notion_Dashboard.md +++ b/Notion_Dashboard.md @@ -39,10 +39,11 @@ Die Schaltstelle für die hyper-personalisierte Ansprache. * **Logik:** Trennung in **Satz 1** (Individueller Hook basierend auf der aktuellen Website-Analyse des Zielkunden) und **Satz 2** (Relationaler Lösungsbaustein basierend auf Branche + Produkt). * **Voice-Ready:** Vorbereitung von Skripten für den zukünftigen Voice-KI-Einsatz im Vertrieb und Support. -### 3.4 Competitive Radar (Market Intelligence) -Automatisierte Überwachung der Marktbegleiter. -* **Funktion:** Kontinuierliches Scraping von Wettbewerber-News und Blogposts. -* **Kill-Argumente:** Direkte Gegenüberstellung technischer Specs zur Erstellung von Battlecards für den Sales-Außendienst. +### 3.4 Competitive Radar (Market Intelligence v2.0) +Automatisierte Überwachung der Marktbegleiter mit Fokus auf "Grounded Truth". +* **Funktion:** Kontinuierliches Scraping von Wettbewerber-Webseiten, gezielte Suche nach Referenzkunden und Case Studies. +* **Kill-Argumente & Landmines:** Erstellung von strukturierten Battlecards und spezifischen "Landmine Questions" für den Sales-Außendienst. +* **Relationaler Ansatz:** Trennung in drei verknüpfte Datenbanken (Firmen, Landmines, Referenzen) für maximale Filterbarkeit und Übersicht. ### 3.5 Enrichment Factory & RevOps Datenanreicherung der CRM-Accounts. @@ -90,6 +91,8 @@ Um die relationale Integrität zu wahren, sind folgende Datenbanken in Notion zw * **Product Master** $\leftrightarrow$ **Sector Master** (Welcher Roboter passt in welchen Markt?) * **Messaging Matrix** $\leftrightarrow$ **Product Master** (Welche Lösung gehört zum Text?) * **Messaging Matrix** $\leftrightarrow$ **Sector Master** (Welcher Schmerz gehört zu welcher Branche?) +* **Competitive Radar (Companies)** $\leftrightarrow$ **Competitive Radar (Landmines)** (Welche Angriffsfragen gehören zu welchem Wettbewerber?) +* **Competitive Radar (Companies)** $\leftrightarrow$ **Competitive Radar (References)** (Welche Kundenprojekte hat der Wettbewerber realisiert?) * **The Brain** $\leftrightarrow$ **Product Master** (Welches Support-Wissen gehört zu welcher Hardware?) * **GTM Workspace** $\leftrightarrow$ **Product Master** (Welche Kampagne bewirbt welches Gerät?) * **Feature-to-Value Translator** $\leftrightarrow$ **Product Master** (Welcher Nutzen gehört zu welchem Feature?) diff --git a/import_competitors_to_notion.py b/import_competitors_to_notion.py new file mode 100644 index 00000000..139dde81 --- /dev/null +++ b/import_competitors_to_notion.py @@ -0,0 +1,200 @@ +import json +import os +import requests +import sys + +# Configuration +JSON_FILE = 'analysis_robo-planet.de.json' +TOKEN_FILE = 'notion_token.txt' +# Root Page ID from notion_integration.md +PARENT_PAGE_ID = "2e088f42-8544-8024-8289-deb383da3818" +DB_TITLE = "Competitive Radar 🎯" + +def load_json_data(filepath): + try: + with open(filepath, 'r') as f: + return json.load(f) + except Exception as e: + print(f"Error loading JSON: {e}") + sys.exit(1) + +def load_notion_token(filepath): + try: + with open(filepath, 'r') as f: + return f.read().strip() + except Exception as e: + print(f"Error loading token: {e}") + sys.exit(1) + +def create_competitor_database(token, parent_page_id): + url = "https://api.notion.com/v1/databases" + headers = { + "Authorization": f"Bearer {token}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" + } + + payload = { + "parent": {"type": "page_id", "page_id": parent_page_id}, + "title": [{"type": "text", "text": {"content": DB_TITLE}}], + "properties": { + "Competitor Name": {"title": {}}, + "Website": {"url": {}}, + "Target Industries": {"multi_select": {}}, + "USPs / Differentiators": {"rich_text": {}}, + "Silver Bullet": {"rich_text": {}}, + "Landmines": {"rich_text": {}}, + "Strengths vs Weaknesses": {"rich_text": {}}, + "Portfolio": {"rich_text": {}}, + "Known References": {"rich_text": {}} + } + } + + print(f"Creating database '{DB_TITLE}'...") + response = requests.post(url, headers=headers, json=payload) + + if response.status_code != 200: + print(f"Error creating database: {response.status_code}") + print(response.text) + sys.exit(1) + + db_data = response.json() + print(f"Database created successfully! ID: {db_data['id']}") + return db_data['id'] + +def format_list_as_bullets(items): + """Converts a python list of strings into a Notion rich_text text block with bullets.""" + if not items: + return "" + text_content = "" + for item in items: + text_content += f"• {item}\n" + return text_content.strip() + +def get_competitor_data(data, comp_name): + """Aggregates data from different sections of the JSON for a single competitor.""" + + # Init structure + comp_data = { + "name": comp_name, + "url": "", + "industries": [], + "differentiators": [], + "portfolio": [], + "silver_bullet": "", + "landmines": [], + "strengths_weaknesses": [], + "references": [] + } + + # 1. Basic Info & Portfolio (from 'analyses') + for analysis in data.get('analyses', []): + c = analysis.get('competitor', {}) + if c.get('name') == comp_name: + comp_data['url'] = c.get('url', '') + comp_data['industries'] = analysis.get('target_industries', []) + comp_data['differentiators'] = analysis.get('differentiators', []) + + # Format Portfolio + for prod in analysis.get('portfolio', []): + p_name = prod.get('product', '') + p_purpose = prod.get('purpose', '') + comp_data['portfolio'].append(f"{p_name}: {p_purpose}") + break + + # 2. Battlecards + for card in data.get('battlecards', []): + if card.get('competitor_name') == comp_name: + comp_data['silver_bullet'] = card.get('silver_bullet', '') + comp_data['landmines'] = card.get('landmine_questions', []) + comp_data['strengths_weaknesses'] = card.get('strengths_vs_weaknesses', []) + break + + # 3. References + for ref_entry in data.get('reference_analysis', []): + if ref_entry.get('competitor_name') == comp_name: + for ref in ref_entry.get('references', []): + r_name = ref.get('name', 'Unknown') + r_ind = ref.get('industry', '') + entry = r_name + if r_ind: + entry += f" ({r_ind})" + comp_data['references'].append(entry) + break + + return comp_data + +def add_competitor_entry(token, db_id, c_data): + url = "https://api.notion.com/v1/pages" + headers = { + "Authorization": f"Bearer {token}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" + } + + # Prepare properties + props = { + "Competitor Name": {"title": [{"text": {"content": c_data['name']}}]}, + "USPs / Differentiators": {"rich_text": [{"text": {"content": format_list_as_bullets(c_data['differentiators'])}}]}, + "Silver Bullet": {"rich_text": [{"text": {"content": c_data['silver_bullet']}}]}, + "Landmines": {"rich_text": [{"text": {"content": format_list_as_bullets(c_data['landmines'])}}]}, + "Strengths vs Weaknesses": {"rich_text": [{"text": {"content": format_list_as_bullets(c_data['strengths_weaknesses'])}}]}, + "Portfolio": {"rich_text": [{"text": {"content": format_list_as_bullets(c_data['portfolio'])}}]}, + "Known References": {"rich_text": [{"text": {"content": format_list_as_bullets(c_data['references'])}}]} + } + + if c_data['url']: + props["Website"] = {"url": c_data['url']} + + # Multi-select for industries + # Note: Notion options are auto-created, but we must ensure no commas or weird chars break it + ms_options = [] + for ind in c_data['industries']: + # Simple cleanup + clean_ind = ind.replace(',', '') + ms_options.append({"name": clean_ind}) + + props["Target Industries"] = {"multi_select": ms_options} + + payload = { + "parent": {"database_id": db_id}, + "properties": props + } + + try: + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + print(f" - Added: {c_data['name']}") + except requests.exceptions.HTTPError as e: + print(f" - Failed to add {c_data['name']}: {e}") + # print(response.text) + +def main(): + token = load_notion_token(TOKEN_FILE) + data = load_json_data(JSON_FILE) + + # 1. Create DB + db_id = create_competitor_database(token, PARENT_PAGE_ID) + + # 2. Collect List of Competitors + # We use the shortlist or candidates list to drive the iteration + competitor_list = data.get('competitors_shortlist', []) + if not competitor_list: + competitor_list = data.get('competitor_candidates', []) + + print(f"Importing {len(competitor_list)} competitors...") + + for comp in competitor_list: + c_name = comp.get('name') + if not c_name: continue + + # Aggregate Data + c_data = get_competitor_data(data, c_name) + + # Push to Notion + add_competitor_entry(token, db_id, c_data) + + print("Import complete.") + +if __name__ == "__main__": + main() diff --git a/import_relational_radar.py b/import_relational_radar.py new file mode 100644 index 00000000..f2874390 --- /dev/null +++ b/import_relational_radar.py @@ -0,0 +1,230 @@ +import json +import os +import requests +import sys + +# Configuration +JSON_FILE = 'analysis_robo-planet.de.json' +TOKEN_FILE = 'notion_token.txt' +PARENT_PAGE_ID = "2e088f42-8544-8024-8289-deb383da3818" + +# Database Titles +DB_TITLE_HUB = "📦 Competitive Radar (Companies)" +DB_TITLE_LANDMINES = "💣 Competitive Radar (Landmines & Intel)" +DB_TITLE_REFS = "🏆 Competitive Radar (References)" + +def load_json_data(filepath): + try: + with open(filepath, 'r') as f: + return json.load(f) + except Exception as e: + print(f"Error loading JSON: {e}") + sys.exit(1) + +def load_notion_token(filepath): + try: + with open(filepath, 'r') as f: + return f.read().strip() + except Exception as e: + print(f"Error loading token: {e}") + sys.exit(1) + +def create_database(token, parent_page_id, title, properties): + url = "https://api.notion.com/v1/databases" + headers = { + "Authorization": f"Bearer {token}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" + } + + payload = { + "parent": {"type": "page_id", "page_id": parent_page_id}, + "title": [{"type": "text", "text": {"content": title}}], + "properties": properties + } + + response = requests.post(url, headers=headers, json=payload) + if response.status_code != 200: + print(f"Error creating DB '{title}': {response.status_code}") + print(response.text) + sys.exit(1) + + db_data = response.json() + print(f"✅ Created DB '{title}' (ID: {db_data['id']})") + return db_data['id'] + +def create_page(token, db_id, properties): + url = "https://api.notion.com/v1/pages" + headers = { + "Authorization": f"Bearer {token}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" + } + + payload = { + "parent": {"database_id": db_id}, + "properties": properties + } + + response = requests.post(url, headers=headers, json=payload) + if response.status_code != 200: + print(f"Error creating page: {response.status_code}") + # print(response.text) + return None + return response.json()['id'] + +def format_list_as_bullets(items): + if not items: return "" + return "\n".join([f"• {item}" for item in items]) + +def main(): + token = load_notion_token(TOKEN_FILE) + data = load_json_data(JSON_FILE) + + print("🚀 Starting Relational Import...") + + # --- STEP 1: Define & Create Competitors Hub DB --- + props_hub = { + "Name": {"title": {}}, + "Website": {"url": {}}, + "Target Industries": {"multi_select": {}}, + "Portfolio Summary": {"rich_text": {}}, + "Silver Bullet": {"rich_text": {}}, + "USPs": {"rich_text": {}} + } + hub_db_id = create_database(token, PARENT_PAGE_ID, DB_TITLE_HUB, props_hub) + + # --- STEP 2: Define & Create Satellite DBs (Linked to Hub) --- + + # Landmines DB + props_landmines = { + "Statement / Question": {"title": {}}, + "Type": {"select": { + "options": [ + {"name": "Landmine Question", "color": "red"}, + {"name": "Competitor Weakness", "color": "green"}, + {"name": "Competitor Strength", "color": "orange"} + ] + }}, + "Related Competitor": { + "relation": { + "database_id": hub_db_id, + "dual_property": {"synced_property_name": "Related Landmines & Intel"} + } + } + } + landmines_db_id = create_database(token, PARENT_PAGE_ID, DB_TITLE_LANDMINES, props_landmines) + + # References DB + props_refs = { + "Customer Name": {"title": {}}, + "Industry": {"select": {}}, + "Snippet": {"rich_text": {}}, + "Case Study URL": {"url": {}}, + "Related Competitor": { + "relation": { + "database_id": hub_db_id, + "dual_property": {"synced_property_name": "Related References"} + } + } + } + refs_db_id = create_database(token, PARENT_PAGE_ID, DB_TITLE_REFS, props_refs) + + # --- STEP 3: Import Competitors (and store IDs) --- + competitor_map = {} # Maps Name -> Notion Page ID + + competitors = data.get('competitors_shortlist', []) or data.get('competitor_candidates', []) + print(f"\nImporting {len(competitors)} Competitors...") + + for comp in competitors: + c_name = comp.get('name') + if not c_name: continue + + # Gather Data + c_url = comp.get('url', '') + + # Find extended analysis data + analysis_data = next((a for a in data.get('analyses', []) if a.get('competitor', {}).get('name') == c_name), {}) + battlecard_data = next((b for b in data.get('battlecards', []) if b.get('competitor_name') == c_name), {}) + + industries = analysis_data.get('target_industries', []) + portfolio = analysis_data.get('portfolio', []) + portfolio_text = "\n".join([f"{p.get('product')}: {p.get('purpose')}" for p in portfolio]) + usps = format_list_as_bullets(analysis_data.get('differentiators', [])) + silver_bullet = battlecard_data.get('silver_bullet', '') + + # Create Page + props = { + "Name": {"title": [{"text": {"content": c_name}}]}, + "Portfolio Summary": {"rich_text": [{"text": {"content": portfolio_text[:2000]}}]}, + "USPs": {"rich_text": [{"text": {"content": usps[:2000]}}]}, + "Silver Bullet": {"rich_text": [{"text": {"content": silver_bullet[:2000]}}]}, + "Target Industries": {"multi_select": [{"name": i.replace(',', '')} for i in industries]}, + } + if c_url: props["Website"] = {"url": c_url} + + page_id = create_page(token, hub_db_id, props) + if page_id: + competitor_map[c_name] = page_id + print(f" - Created: {c_name}") + + # --- STEP 4: Import Landmines & Intel --- + print("\nImporting Landmines & Intel...") + for card in data.get('battlecards', []): + c_name = card.get('competitor_name') + comp_page_id = competitor_map.get(c_name) + if not comp_page_id: continue + + # 1. Landmines + for q in card.get('landmine_questions', []): + props = { + "Statement / Question": {"title": [{"text": {"content": q}}]}, + "Type": {"select": {"name": "Landmine Question"}}, + "Related Competitor": {"relation": [{"id": comp_page_id}]} + } + create_page(token, landmines_db_id, props) + + # 2. Weaknesses + # The JSON has "strengths_vs_weaknesses" combined. We'll import them as general Intel points. + for point in card.get('strengths_vs_weaknesses', []): + # Try to guess type based on text, or just default to Weakness context from Battlecard + p_type = "Competitor Weakness" # Assuming these are points for us to exploit + props = { + "Statement / Question": {"title": [{"text": {"content": point}}]}, + "Type": {"select": {"name": p_type}}, + "Related Competitor": {"relation": [{"id": comp_page_id}]} + } + create_page(token, landmines_db_id, props) + print(" - Landmines imported.") + + # --- STEP 5: Import References --- + print("\nImporting References...") + count_refs = 0 + for ref_group in data.get('reference_analysis', []): + c_name = ref_group.get('competitor_name') + comp_page_id = competitor_map.get(c_name) + if not comp_page_id: continue + + for ref in ref_group.get('references', []): + r_name = ref.get('name', 'Unknown') + r_industry = ref.get('industry', 'Unknown') + r_snippet = ref.get('testimonial_snippet', '') + r_url = ref.get('case_study_url', '') + + props = { + "Customer Name": {"title": [{"text": {"content": r_name}}]}, + "Industry": {"select": {"name": r_industry}}, + "Snippet": {"rich_text": [{"text": {"content": r_snippet[:2000]}}]}, + "Related Competitor": {"relation": [{"id": comp_page_id}]} + } + if r_url and r_url.startswith('http'): + props["Case Study URL"] = {"url": r_url} + + create_page(token, refs_db_id, props) + count_refs += 1 + + print(f" - {count_refs} References imported.") + print("\n✅ Relational Import Complete!") + +if __name__ == "__main__": + main() diff --git a/refresh_references.py b/refresh_references.py new file mode 100644 index 00000000..a9b6e3b4 --- /dev/null +++ b/refresh_references.py @@ -0,0 +1,35 @@ +import asyncio +import json +import os +import sys + +# Path to the orchestrator +sys.path.append(os.path.join(os.getcwd(), 'competitor-analysis-app')) + +from competitor_analysis_orchestrator import analyze_single_competitor_references + +async def refresh_references(): + json_path = 'analysis_robo-planet.de.json' + + with open(json_path, 'r') as f: + data = json.load(f) + + competitors = data.get('competitors_shortlist', []) + if not competitors: + competitors = data.get('competitor_candidates', []) + + print(f"Refreshing references for {len(competitors)} competitors...") + + tasks = [analyze_single_competitor_references(c) for c in competitors] + results = await asyncio.gather(*tasks) + + # Filter and update + data['reference_analysis'] = [r for r in results if r is not None] + + with open(json_path, 'w') as f: + json.dump(data, f, indent=2) + + print(f"Successfully updated {json_path} with grounded reference data.") + +if __name__ == "__main__": + asyncio.run(refresh_references())