feat(market-intel): Finalize Level 4 Competitive Radar (Semantics & Relations)

- Implemented semantic classification for Products (e.g. 'Cleaning', 'Logistics') and Battlecards (e.g. 'Price', 'Support').
- Created 'import_competitive_radar.py' for full 4-database relational import to Notion.
- Updated Orchestrator with new prompts for structured output.
- Cleaned up obsolete scripts.
This commit is contained in:
2026-01-11 12:54:12 +00:00
parent 6f15673482
commit c6e67b31e8
5 changed files with 249 additions and 26 deletions

View File

@@ -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).*

View File

@@ -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"]

View File

@@ -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;
}

103
import_competitive_radar.py Normal file
View File

@@ -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()

78
refresh_classification.py Normal file
View File

@@ -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())