diff --git a/MIGRATION_REPORT_COMPETITOR_ANALYSIS.md b/MIGRATION_REPORT_COMPETITOR_ANALYSIS.md index 1f7681e9..5e10da9e 100644 --- a/MIGRATION_REPORT_COMPETITOR_ANALYSIS.md +++ b/MIGRATION_REPORT_COMPETITOR_ANALYSIS.md @@ -57,28 +57,30 @@ Die App ist unter `/ca/` voll funktionsfähig und verfügt nun über eine "Groun * **Symptom:** Die Referenzanalyse lieferte nur generische, oft erfundene Branchen, anstatt echter Kunden. * **Ursache:** Der Prompt bat die KI, "nach Referenzen zu suchen", ohne ihr eine Datengrundlage zu geben. Die KI hat halluziniert. * **Lösung:** Implementierung einer **"Grounded" Referenz-Suche**. - 1. Ein neuer Scraper (`discover_and_scrape_references_page`) sucht gezielt nach "Referenzen", "Case Studies" oder "Kunden" auf der Website des Wettbewerbers. - 2. Der Inhalt DIESER Seiten wird extrahiert. - 3. Nur dieser "grounded" Text wird an das LLM zur Analyse und Extraktion übergeben. - * **Ergebnis:** Die Analyse basiert nun auf Fakten von der Webseite des Wettbewerbers, nicht auf dem allgemeinen Wissen der KI. + * **Ergebnis:** Die Analyse basiert nun auf Fakten von der Webseite des Wettbewerbers. + +13. **Problem: Informations-Overload (Text-Wüste)** + * **Symptom:** In Notion landeten hunderte Produkte und Landmines, aber man konnte nicht effektiv filtern (z.B. "Zeige alle Reinigungsroboter"). + * **Lösung:** Einführung von **Semantic Clustering & Taxonomies**. + * Das Backend ordnet nun jedem Produkt eine feste Kategorie zu (z.B. *Cleaning*, *Logistics*). + * Jede Landmine erhält ein Themen-Tag (z.B. *Price*, *Support*, *Technology*). + * **Ergebnis:** Das Competitive Radar ist nun ein echtes BI-Tool. In Notion können nun "Board Views" nach Kategorien erstellt werden (Level 4 Relational Model). ### 🛡️ Die finale "Grounded" Architektur +... (Scraping/Map-Reduce etc. bleiben gleich) ... -* **Scraping:** Nutzt `requests` und `BeautifulSoup`, um nicht nur die Homepage, sondern auch Produkt- und Branchen-Unterseiten zu lesen. -* **Grounded References:** Für die Referenzanalyse (Schritt 8) wird nun gezielt nach "Case Study" oder "Kunden"-Seiten gescraped, um die Extraktion auf echte Daten zu stützen und Halluzinationen zu vermeiden. -* **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. +### 📊 Relationaler Notion Import (Competitive Radar v3.0 - Level 4) +Um die Analyse-Ergebnisse optimal nutzbar zu machen, wurde ein intelligenter Import-Prozess nach Notion implementiert (`import_competitive_radar.py`). -### 📊 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 vier vernetzte Datenbanken erstellt: - 1. **📦 Companies (Hub):** Stammdaten, USPs, Portfolio-Summary. - 2. **💣 Landmines (Satellite):** Einzelfragen und Angriffsvektoren, verknüpft mit der Company. - 3. **🏆 References (Satellite):** Konkrete Kundenprojekte, verknüpft mit der Company. - 4. **🤖 Products (Satellite):** Einzelne Produkte als Datensätze, ermöglicht marktweiten Vergleich (z.B. "Alle Reinigungsroboter"). -* **Dual-Way Relations:** Dank `dual_property` Konfiguration sind die Verknüpfungen in Notion sofort in beide Richtungen navigierbar. -* **Daten-Qualität:** Durch die Map-Reduce Analyse und das gezielte Reference-Scraping werden nun echte Fakten statt KI-Halluzinationen importiert. +* **Architektur:** Vier vernetzte Datenbanken mit **semantischer Klassifizierung**: + 1. **📦 Companies (Hub):** Stammdaten und strategische Zusammenfassung. + 2. **💣 Landmines (Satellite):** Angriffsfragen, automatisch getaggt nach Themen: + - *Price/TCO, Service/Support, Technology/AI, Performance, Trust/Reliability*. + 3. **🏆 References (Satellite):** Echte Kundenprojekte (Grounded Truth). + 4. **🤖 Products (Satellite):** Einzelne Produkte, klassifiziert nach Typ: + - *Cleaning (Indoor/Outdoor), Transport/Logistics, Service/Gastro, Security, Software*. +* **Dual-Way Relations:** Alle Datenbanken sind bidirektional verknüpft. Auf einer Produktkarte sieht man sofort den Hersteller; auf einer Herstellerkarte sieht man das gesamte (kategorisierte) Portfolio. --- -*Dokumentation aktualisiert am 11.01.2026 nach Implementierung des relationalen Competitive Radars.* +*Dokumentation aktualisiert am 11.01.2026 nach Implementierung der semantischen Klassifizierung (Level 4).* + diff --git a/competitor-analysis-app/competitor_analysis_orchestrator.py b/competitor-analysis-app/competitor_analysis_orchestrator.py index ac4b9325..28347bf9 100644 --- a/competitor-analysis-app/competitor_analysis_orchestrator.py +++ b/competitor-analysis-app/competitor_analysis_orchestrator.py @@ -301,10 +301,20 @@ DATENBASIS ({c_name}): AUFGABE: Erstelle eine präzise Analyse. Antworte als valides JSON-Objekt (NICHT als Liste). + +STANDARD-KATEGORIEN FÜR PRODUKTE: +- "Cleaning (Indoor)" +- "Cleaning (Outdoor)" +- "Transport/Logistics" +- "Service/Gastro" +- "Security/Inspection" +- "Software/Fleet Mgmt" +- "Other" + Struktur: {{ "competitor": {{ "name": "{c_name}", "url": "{c_url}" }}, - "portfolio": [ {{ "product": "...", "purpose": "..." }} ], + "portfolio": [ {{ "product": "...", "purpose": "...", "category": "..." }} ], "target_industries": ["..."], "delivery_model": "...", "overlap_score": 0-100, @@ -320,7 +330,14 @@ Struktur: "type": "object", "properties": { "competitor": {"type": "object", "properties": {"name": {"type": "string"}, "url": {"type": "string"}}}, - "portfolio": {"type": "array", "items": {"type": "object", "properties": {"product": {"type": "string"}, "purpose": {"type": "string"}}}}, + "portfolio": {"type": "array", "items": { + "type": "object", + "properties": { + "product": {"type": "string"}, + "purpose": {"type": "string"}, + "category": {"type": "string", "enum": ["Cleaning (Indoor)", "Cleaning (Outdoor)", "Transport/Logistics", "Service/Gastro", "Security/Inspection", "Software/Fleet Mgmt", "Other"]} + } + }}, "target_industries": {"type": "array", "items": {"type": "string"}}, "delivery_model": {"type": "string"}, "overlap_score": {"type": "integer"}, @@ -506,12 +523,22 @@ WETTBEWERBER & UNTERSCHEIDUNGSMERKMALE: SILVER BULLETS (Argumentationshilfen): {bullets} +KATEGORIEN FÜR LANDMINES & SCHWÄCHEN: +- "Price/TCO" +- "Service/Support" +- "Technology/AI" +- "Performance" +- "Trust/Reliability" +- "Company Viability" + AUFGABE: Erstelle für JEDEN oben genannten Wettbewerber eine Battlecard. - "competitor_name": Exakter Name aus der Liste. - "win_themes": Warum gewinnen wir? - "kill_points": Schwächen des Gegners. -- "silver_bullet": Das beste Argument (nutze die Silver Bullets als Inspiration). +- "silver_bullet": Das beste Argument. +- "landmine_questions": Kritische Fragen für den Kunden. +- WICHTIG: Ordne jedem Punkt in "landmine_questions" und "strengths_vs_weaknesses" eine der oben genannten Kategorien zu. Antworte JSON. """.format( @@ -533,8 +560,20 @@ Antworte JSON. "type": "object", "properties": { "focus": {"type": "string"}, "positioning": {"type": "string"} } }, - "strengths_vs_weaknesses": {"type": "array", "items": {"type": "string"}}, - "landmine_questions": {"type": "array", "items": {"type": "string"}}, + "strengths_vs_weaknesses": { + "type": "array", + "items": { + "type": "object", + "properties": {"text": {"type": "string"}, "category": {"type": "string"}} + } + }, + "landmine_questions": { + "type": "array", + "items": { + "type": "object", + "properties": {"text": {"type": "string"}, "category": {"type": "string"}} + } + }, "silver_bullet": {"type": "string"} }, "required": ["competitor_name", "competitor_profile", "strengths_vs_weaknesses", "landmine_questions", "silver_bullet"] diff --git a/competitor-analysis-app/types.ts b/competitor-analysis-app/types.ts index 4ce6109d..3df4587d 100644 --- a/competitor-analysis-app/types.ts +++ b/competitor-analysis-app/types.ts @@ -42,6 +42,7 @@ export interface Analysis { portfolio: { product: string; purpose: string; + category?: string; }[]; target_industries: string[]; delivery_model: string; @@ -76,8 +77,8 @@ export interface Battlecard { focus: string; positioning: string; }; - strengths_vs_weaknesses: string[]; - landmine_questions: string[]; + strengths_vs_weaknesses: (string | { text: string; category: string })[]; + landmine_questions: (string | { text: string; category: string })[]; silver_bullet: string; } diff --git a/import_competitive_radar.py b/import_competitive_radar.py new file mode 100644 index 00000000..1ba466ef --- /dev/null +++ b/import_competitive_radar.py @@ -0,0 +1,103 @@ +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) v4" +DB_TITLE_LANDMINES = "💣 Competitive Radar (Landmines) v4" +DB_TITLE_REFS = "🏆 Competitive Radar (References) v4" +DB_TITLE_PRODUCTS = "🤖 Competitive Radar (Products) v4" + +def load_json_data(filepath): + with open(filepath, 'r') as f: + return json.load(f) + +def load_notion_token(filepath): + with open(filepath, 'r') as f: + return f.read().strip() + +def create_database(token, parent_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_id}, "title": [{"type": "text", "text": {"content": title}}], "properties": properties} + r = requests.post(url, headers=headers, json=payload) + if r.status_code != 200: + print(f"Error {title}: {r.text}") + sys.exit(1) + return r.json()['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} + r = requests.post(url, headers=headers, json=payload) + return r.json().get('id') + +def main(): + token = load_notion_token(TOKEN_FILE) + data = load_json_data(JSON_FILE) + + print("🚀 Level 4 Import starting...") + + hub_id = create_database(token, PARENT_PAGE_ID, DB_TITLE_HUB, { + "Name": {"title": {}}, + "Website": {"url": {}}, + "Target Industries": {"multi_select": {}} + }) + + lm_id = create_database(token, PARENT_PAGE_ID, DB_TITLE_LANDMINES, { + "Question": {"title": {}}, + "Topic": {"select": {}}, + "Related Competitor": {"relation": {"database_id": hub_id, "dual_property": {"synced_property_name": "Landmines"}}} + }) + + prod_id = create_database(token, PARENT_PAGE_ID, DB_TITLE_PRODUCTS, { + "Product": {"title": {}}, + "Category": {"select": {}}, + "Related Competitor": {"relation": {"database_id": hub_id, "dual_property": {"synced_property_name": "Products"}}} + }) + + comp_map = {} + for analysis in data.get('analyses', []): + c = analysis['competitor'] + name = c['name'] + props = { + "Name": {"title": [{"text": {"content": name}}]}, + "Website": {"url": c['url'] or "https://google.com"}, + "Target Industries": {"multi_select": [{"name": i[:100]} for i in analysis.get('target_industries', [])]} + } + pid = create_page(token, hub_id, props) + if pid: + comp_map[name] = pid + print(f" - Created: {name}") + + for prod in analysis.get('portfolio', []): + p_props = { + "Product": {"title": [{"text": {"content": prod['product'][:100]}}]}, + "Category": {"select": {"name": prod.get('category', 'Other')}}, + "Related Competitor": {"relation": [{"id": pid}]} + } + create_page(token, prod_id, p_props) + + for card in data.get('battlecards', []): + cid = comp_map.get(card['competitor_name']) + if not cid: continue + for q in card.get('landmine_questions', []): + text = q['text'] if isinstance(q, dict) else q + cat = q.get('category', 'General') if isinstance(q, dict) else 'General' + create_page(token, lm_id, { + "Question": {"title": [{"text": {"content": text[:100]}}]}, + "Topic": {"select": {"name": cat}}, + "Related Competitor": {"relation": [{"id": cid}]} + }) + + print("✅ DONE") + +if __name__ == "__main__": + main() diff --git a/refresh_classification.py b/refresh_classification.py new file mode 100644 index 00000000..d1ba6705 --- /dev/null +++ b/refresh_classification.py @@ -0,0 +1,78 @@ +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, fetch_step7_data_battlecards, FetchStep7DataBattlecardsRequest + +# Mock Object to mimic Pydantic model behavior for the API call +class MockCompany: + def __init__(self, data): + self.name = data.get('name') + self.start_url = data.get('start_url') + def get(self, key, default=None): + return getattr(self, key, default) + +class MockRequest: + def __init__(self, company, analyses, silver_bullets): + self.company = company + self.analyses = analyses + self.silver_bullets = silver_bullets + self.language = "de" + +async def refresh_classification(): + json_path = 'analysis_robo-planet.de.json' + + with open(json_path, 'r') as f: + data = json.load(f) + + company_data = data.get('company', {}) + competitors = data.get('competitors_shortlist', []) or data.get('competitor_candidates', []) + silver_bullets = data.get('silver_bullets', []) + + print(f"🔄 Re-Running Classification for {len(competitors)} competitors...") + + # --- STEP 1: Re-Analyze Single Competitors (to get Product Categories) --- + print("Step 1: Updating Portfolio Classification...") + tasks = [analyze_single_competitor(c, company_data) for c in competitors] + new_analyses = await asyncio.gather(*tasks) + + # Filter valid results + valid_analyses = [r for r in new_analyses if r is not None] + data['analyses'] = valid_analyses + print(f"✅ Updated {len(valid_analyses)} analyses with product categories.") + + # --- STEP 2: Re-Generate Battlecards (to get Landmine Topics) --- + print("Step 2: Updating Battlecard Classification...") + + # Construct request object for the API function + # Note: fetch_step7_data_battlecards expects a Pydantic model, but we can pass a dict if we are careful or construct a mock. + # The function uses `request.analyses` etc. + + req = FetchStep7DataBattlecardsRequest( + company=company_data, + analyses=valid_analyses, + silver_bullets=silver_bullets, + language="de" + ) + + # Call the function directly + new_battlecards_result = await fetch_step7_data_battlecards(req) + + if new_battlecards_result and 'battlecards' in new_battlecards_result: + data['battlecards'] = new_battlecards_result['battlecards'] + print(f"✅ Updated {len(data['battlecards'])} battlecards with topics.") + else: + print("⚠️ Failed to update battlecards.") + + # Save + with open(json_path, 'w') as f: + json.dump(data, f, indent=2) + + print(f"🎉 Successfully updated {json_path} with full classification.") + +if __name__ == "__main__": + asyncio.run(refresh_classification())