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:
@@ -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).*
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
103
import_competitive_radar.py
Normal 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
78
refresh_classification.py
Normal 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())
|
||||
Reference in New Issue
Block a user