diff --git a/company-explorer/backend/scripts/notion_maintenance/check_notion_verticals.py b/company-explorer/backend/scripts/notion_maintenance/check_notion_verticals.py new file mode 100644 index 00000000..522ee598 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/check_notion_verticals.py @@ -0,0 +1,117 @@ +import os +import requests +import json +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" # ID from the user's link + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found in environment.") + exit(1) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +def get_vertical_data(vertical_name): + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = { + "filter": { + "property": "Vertical", + "title": { + "contains": vertical_name + } + } + } + + response = requests.post(url, headers=headers, json=payload) + + if response.status_code != 200: + print(f"Error fetching data for '{vertical_name}': {response.status_code} - {response.text}") + return None + + results = response.json().get("results", []) + if not results: + print(f"No entry found for vertical '{vertical_name}'") + return None + + # Assuming the first result is the correct one + page = results[0] + props = page["properties"] + + # Extract Pains + pains_prop = props.get("Pains", {}).get("rich_text", []) + pains = pains_prop[0]["plain_text"] if pains_prop else "N/A" + + # Extract Gains + gains_prop = props.get("Gains", {}).get("rich_text", []) + gains = gains_prop[0]["plain_text"] if gains_prop else "N/A" + + # Extract Ops Focus (Checkbox) if available + # The property name might be "Ops. Focus: Secondary" based on user description + # Let's check keys to be sure, but user mentioned "Ops. Focus: Secondary" + # Actually, let's just dump the keys if needed, but for now try to guess + ops_focus = "Unknown" + if "Ops. Focus: Secondary" in props: + ops_focus = props["Ops. Focus: Secondary"].get("checkbox", False) + elif "Ops Focus" in props: # Fallback guess + ops_focus = props["Ops Focus"].get("checkbox", False) + + # Extract Product Categories + primary_product = "N/A" + secondary_product = "N/A" + + # Assuming these are Select or Multi-select fields, or Relations. + # User mentioned "Primary Product Category" and "Secondary Product Category". + if "Primary Product Category" in props: + pp_data = props["Primary Product Category"].get("select") or props["Primary Product Category"].get("multi_select") + if pp_data: + if isinstance(pp_data, list): + primary_product = ", ".join([item["name"] for item in pp_data]) + else: + primary_product = pp_data["name"] + + if "Secondary Product Category" in props: + sp_data = props["Secondary Product Category"].get("select") or props["Secondary Product Category"].get("multi_select") + if sp_data: + if isinstance(sp_data, list): + secondary_product = ", ".join([item["name"] for item in sp_data]) + else: + secondary_product = sp_data["name"] + + return { + "name": vertical_name, + "pains": pains, + "gains": gains, + "ops_focus_secondary": ops_focus, + "primary_product": primary_product, + "secondary_product": secondary_product + } + +verticals_to_check = [ + "Krankenhaus", + "Pflege", # Might be "Altenheim" or similar + "Hotel", + "Industrie", # Might be "Manufacturing" + "Logistik", + "Einzelhandel", + "Facility Management" +] + +print("-" * 60) +for v in verticals_to_check: + data = get_vertical_data(v) + if data: + print(f"VERTICAL: {data['name']}") + print(f" Primary Product: {data['primary_product']}") + print(f" Secondary Product: {data['secondary_product']}") + print(f" Ops. Focus Secondary: {data['ops_focus_secondary']}") + print(f" PAINS: {data['pains']}") + print(f" GAINS: {data['gains']}") + print("-" * 60) diff --git a/company-explorer/backend/scripts/notion_maintenance/check_relations.py b/company-explorer/backend/scripts/notion_maintenance/check_relations.py new file mode 100644 index 00000000..30151bad --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/check_relations.py @@ -0,0 +1,90 @@ +import os +import requests +import json +from dotenv import load_dotenv + +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" # Verticals DB +PRODUCT_DB_ID = "2ec88f42854480f0b154f7a07342eb58" # Product Categories DB (from user link) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +# 1. Fetch Product Map (ID -> Name) +product_map = {} +def fetch_products(): + url = f"https://api.notion.com/v1/databases/{PRODUCT_DB_ID}/query" + response = requests.post(url, headers=headers, json={"page_size": 100}) + if response.status_code == 200: + results = response.json().get("results", []) + for p in results: + p_id = p["id"] + # Name property might be "Name" or "Product Category" + props = p["properties"] + name = "Unknown" + if "Name" in props: + name = props["Name"]["title"][0]["plain_text"] if props["Name"]["title"] else "N/A" + elif "Product Category" in props: + name = props["Product Category"]["title"][0]["plain_text"] if props["Product Category"]["title"] else "N/A" + + product_map[p_id] = name + # Also map the page ID itself if used in relations + + else: + print(f"Error fetching products: {response.status_code}") + +# 2. Check Verticals with Relation Resolution +def check_vertical_relations(search_term): + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = { + "filter": { + "property": "Vertical", + "title": { + "contains": search_term + } + } + } + resp = requests.post(url, headers=headers, json=payload) + if resp.status_code == 200: + results = resp.json().get("results", []) + if not results: + print(f"❌ No vertical found for '{search_term}'") + return + + for page in results: + props = page["properties"] + title = props["Vertical"]["title"][0]["plain_text"] + + # Resolve Primary + pp_ids = [r["id"] for r in props.get("Primary Product Category", {}).get("relation", [])] + pp_names = [product_map.get(pid, pid) for pid in pp_ids] + + # Resolve Secondary + sp_ids = [r["id"] for r in props.get("Secondary Product", {}).get("relation", [])] + sp_names = [product_map.get(pid, pid) for pid in sp_ids] + + print(f"\nđŸ”č VERTICAL: {title}") + print(f" Primary Product (Rel): {', '.join(pp_names)}") + print(f" Secondary Product (Rel): {', '.join(sp_names)}") + + # Pains/Gains short check + pains = props.get("Pains", {}).get("rich_text", []) + print(f" Pains Length: {len(pains[0]['plain_text']) if pains else 0} chars") + + else: + print(f"Error fetching vertical: {resp.status_code}") + +# Run +print("Fetching Product Map...") +fetch_products() +print(f"Loaded {len(product_map)} products.") + +print("\nChecking Verticals...") +targets = ["Hospital", "Hotel", "Logistics", "Manufacturing", "Retail", "Reinigungs", "Dienstleister", "Facility"] +for t in targets: + check_vertical_relations(t) diff --git a/company-explorer/backend/scripts/notion_maintenance/check_specific_verticals.py b/company-explorer/backend/scripts/notion_maintenance/check_specific_verticals.py new file mode 100644 index 00000000..a7635882 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/check_specific_verticals.py @@ -0,0 +1,87 @@ +import os +import requests +import json +from dotenv import load_dotenv + +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found.") + exit(1) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +def get_vertical_details(vertical_name_contains): + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = { + "filter": { + "property": "Vertical", + "title": { + "contains": vertical_name_contains + } + } + } + + response = requests.post(url, headers=headers, json=payload) + if response.status_code != 200: + print(f"Error: {response.status_code}") + return + + results = response.json().get("results", []) + if not results: + print(f"❌ No entry found containing '{vertical_name_contains}'") + return + + for page in results: + props = page["properties"] + + # safely extract title + title_list = props.get("Vertical", {}).get("title", []) + title = title_list[0]["plain_text"] if title_list else "Unknown Title" + + # Pains + pains_list = props.get("Pains", {}).get("rich_text", []) + pains = pains_list[0]["plain_text"] if pains_list else "N/A" + + # Gains + gains_list = props.get("Gains", {}).get("rich_text", []) + gains = gains_list[0]["plain_text"] if gains_list else "N/A" + + # Ops Focus + ops_focus = props.get("Ops Focus: Secondary", {}).get("checkbox", False) + + # Products + # Primary is select + pp_select = props.get("Primary Product Category", {}).get("select") + pp = pp_select["name"] if pp_select else "N/A" + + # Secondary is select + sp_select = props.get("Secondary Product", {}).get("select") + sp = sp_select["name"] if sp_select else "N/A" + + print(f"\nđŸ”č VERTICAL: {title}") + print(f" Primary: {pp}") + print(f" Secondary: {sp}") + print(f" Ops Focus Secondary? {'✅ YES' if ops_focus else '❌ NO'}") + print(f" PAINS:\n {pains}") + print(f" GAINS:\n {gains}") + print("-" * 40) + +targets = [ + "Hospital", + "Hotel", + "Logistics", + "Manufacturing", + "Retail", + "Facility Management" +] + +for t in targets: + get_vertical_details(t) diff --git a/company-explorer/backend/scripts/notion_maintenance/list_notion_structure.py b/company-explorer/backend/scripts/notion_maintenance/list_notion_structure.py new file mode 100644 index 00000000..3f0d9c00 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/list_notion_structure.py @@ -0,0 +1,66 @@ +import os +import requests +import json +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found in environment.") + exit(1) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +def list_pages_and_keys(): + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = { + "page_size": 10 # Just list a few to see structure + } + + response = requests.post(url, headers=headers, json=payload) + + if response.status_code != 200: + print(f"Error fetching data: {response.status_code} - {response.text}") + return + + results = response.json().get("results", []) + + if not results: + print("No pages found.") + return + + print(f"Found {len(results)} pages.") + + # Print keys from the first page + first_page = results[0] + props = first_page["properties"] + print("\n--- Property Keys Found ---") + for key in props.keys(): + print(f"- {key}") + + print("\n--- Page Titles (Verticals) ---") + for page in results: + title_prop = page["properties"].get("Vertical", {}).get("title", []) # Assuming title prop is named "Vertical" based on user input + if not title_prop: + # Try finding the title property dynamically if "Vertical" is wrong + for k, v in page["properties"].items(): + if v["id"] == "title": + title_prop = v["title"] + break + + if title_prop: + title = title_prop[0]["plain_text"] + print(f"- {title}") + else: + print("- (No Title)") + +if __name__ == "__main__": + list_pages_and_keys() diff --git a/company-explorer/backend/scripts/notion_maintenance/update_notion_full.py b/company-explorer/backend/scripts/notion_maintenance/update_notion_full.py new file mode 100644 index 00000000..d7fbe0f5 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/update_notion_full.py @@ -0,0 +1,89 @@ +import os +import requests +import json +from dotenv import load_dotenv + +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found.") + exit(1) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +# COMPLETE LIST OF UPDATES +updates = { + "Infrastructure - Transport": { # Airports, Stations + "Pains": "Sicherheitsbereiche erfordern personalintensives Screening von externen ReinigungskrĂ€ften. Verschmutzte Böden (Winter/Salz) erhöhen das Rutschrisiko fĂŒr Passagiere und Klagerisiken.", + "Gains": "Autonome Reinigung innerhalb der Sicherheitszonen ohne externe Personalwechsel. Permanente Trocknung von NĂ€sse (Schneematsch) in Eingangsbereichen." + }, + "Leisure - Indoor Active": { # Bowling, Cinema, Gym + "Pains": "Personal ist rar und teuer, GĂ€ste erwarten aber Service am Platz. Reinigung im laufenden Betrieb stört den Erlebnischarakter.", + "Gains": "Service-Roboter als Event-Faktor und Entlastung: GetrĂ€nke kommen zum Gast, Personal bleibt an der Bar/Theke. Konstante Sauberkeit auch bei hoher Frequenz." + }, + "Leisure - Outdoor Park": { # Zoos, Theme Parks + "Pains": "Enorme FlĂ€chenleistung (Wege) erfordert viele ArbeitskrĂ€fte fĂŒr die Grobschmutzbeseitigung (Laub, MĂŒll). Sichtbare Reinigungstrupps stören die Immersion der GĂ€ste.", + "Gains": "Autonome GroßflĂ€chenreinigung (Kehren) in den frĂŒhen Morgenstunden vor Parköffnung. Erhalt der 'heilen Welt' (Immersion) fĂŒr Besucher." + }, + "Leisure - Wet & Spa": { # Pools, Thermen + "Pains": "Hohes Unfallrisiko durch NĂ€sse auf Fliesen (Rutschgefahr). Hoher Aufwand fĂŒr permanente Desinfektion und Trocknung im laufenden Betrieb bindet Aufsichtspersonal.", + "Gains": "Permanente Trocknung und Desinfektion kritischer Barfußbereiche. Reduktion der Rutschgefahr und Haftungsrisiken. Entlastung der Bademeister (Fokus auf Aufsicht)." + }, + "Retail - Shopping Center": { # Malls + "Pains": "Food-Court ist der Schmutz-Hotspot: VerschĂŒttete GetrĂ€nke und Essensreste wirken unhygienisch und binden Personal dauerhaft. Dreckige Böden senken die Verweildauer.", + "Gains": "Sofortige Beseitigung von Malheuren im Food-Court. Steigerung der AufenthaltsqualitĂ€t und Verweildauer der Kunden durch sichtbare Sauberkeit." + }, + "Retail - Non-Food": { # DIY, Furniture + "Pains": "Riesige GangflĂ€chen verstauben schnell, Personal ist knapp und soll beraten, nicht kehren. Verschmutzte Böden wirken im Premium-Segment (Möbel) wertmindernd.", + "Gains": "Staubfreie Umgebung fĂŒr angenehmes Einkaufsklima. Roboter reinigen autonom große FlĂ€chen, wĂ€hrend Mitarbeiter fĂŒr Kundenberatung verfĂŒgbar sind." + }, + "Infrastructure - Public": { # Fairs, Schools + "Pains": "Extrem kurze Turnaround-Zeiten zwischen Messetagen oder Events. Hohe NachtzuschlĂ€ge fĂŒr die Endreinigung der HallengĂ€nge oder Klassenzimmer.", + "Gains": "Automatisierte Nachtreinigung der GĂ€nge/Flure stellt die Optik fĂŒr den nĂ€chsten Morgen sicher. Kalkulierbare Kosten ohne Nachtzuschlag." + }, + "Hospitality - Gastronomy": { # Restaurants + "Pains": "Servicepersonal verbringt Zeit auf Laufwegen statt am Gast ('Teller-Taxi'). Personalmangel fĂŒhrt zu langen Wartezeiten und Umsatzverlust.", + "Gains": "ServicekrĂ€fte werden von Laufwegen befreit und haben Zeit fĂŒr aktive Beratung und Verkauf (Upselling). Steigerung der TischumschlagshĂ€ufigkeit." + } +} + +def update_vertical(vertical_name, new_data): + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = { + "filter": { + "property": "Vertical", + "title": { + "contains": vertical_name + } + } + } + resp = requests.post(url, headers=headers, json=payload) + if resp.status_code != 200: return + + results = resp.json().get("results", []) + if not results: + print(f"Skipping {vertical_name} (Not found)") + return + + page_id = results[0]["id"] + update_url = f"https://api.notion.com/v1/pages/{page_id}" + update_payload = { + "properties": { + "Pains": {"rich_text": [{"text": {"content": new_data["Pains"]}}]}, + "Gains": {"rich_text": [{"text": {"content": new_data["Gains"]}}]} + } + } + requests.patch(update_url, headers=headers, json=update_payload) + print(f"✅ Updated {vertical_name}") + +print("Starting FULL Notion Update...") +for v_name, data in updates.items(): + update_vertical(v_name, data) +print("Done.") diff --git a/company-explorer/backend/scripts/notion_maintenance/update_notion_pains_gains.py b/company-explorer/backend/scripts/notion_maintenance/update_notion_pains_gains.py new file mode 100644 index 00000000..6af2a4a7 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/update_notion_pains_gains.py @@ -0,0 +1,94 @@ +import os +import requests +import json +from dotenv import load_dotenv + +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found.") + exit(1) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +# Define the updates with "Sharp" Pains/Gains +updates = { + "Healthcare - Hospital": { + "Pains": "FachpflegekrĂ€fte sind bis zu 30% der Schichtzeit mit logistischen RoutinetĂ€tigkeiten (WĂ€sche, Essen, Laborproben) gebunden ('HĂ€nde weg vom Bett'). Steigende Hygienerisiken bei gleichzeitigem Personalmangel im Reinigungsteam fĂŒhren zu lĂŒckenhafter Dokumentation und GefĂ€hrdung der RKI-KonformitĂ€t.", + "Gains": "RĂŒckgewinnung von ca. 2,5h Fachkraft-KapazitĂ€t pro Schicht durch automatisierte Stationslogistik. Validierbare, RKI-konforme ReinigungsqualitĂ€t rund um die Uhr, unabhĂ€ngig vom Krankenstand des Reinigungsteams." + }, + "Hospitality - Hotel": { + "Pains": "Enorme Fluktuation im Housekeeping gefĂ€hrdet die pĂŒnktliche Zimmer-Freigabe (Check-in 15:00 Uhr). Hohe NachtzuschlĂ€ge oder fehlendes Personal verhindern, dass die Lobby und Konferenzbereiche morgens um 06:00 Uhr perfekt glĂ€nzen.", + "Gains": "Lautlose Nachtreinigung der Lobby und Flure ohne PersonalzuschlĂ€ge. ServicekrĂ€fte im Restaurant werden von Laufwegen ('Teller-Taxi') befreit und haben Zeit fĂŒr aktives Upselling am Gast." + }, + "Logistics - Warehouse": { + "Pains": "Verschmutzte Fahrwege durch Palettenabrieb und Staub gefĂ€hrden die Sensorik von FTS (Fahrerlosen Transportsystemen) und erhöhen das Unfallrisiko fĂŒr Flurförderzeuge. Manuelle Reinigung stört den 24/7-Betrieb und bindet Fachpersonal.", + "Gains": "Permanente Staubreduktion im laufenden Betrieb schĂŒtzt empfindliche Anlagentechnik (Lichtschranken). Saubere Hallen als Visitenkarte und Sicherheitsfaktor (Rutschgefahr), ohne operative Unterbrechungen." + }, + "Industry - Manufacturing": { + "Pains": "Hochbezahlte Facharbeiter unterbrechen die Wertschöpfung fĂŒr unproduktive Such- und Holzeiten von Material (C-Teile). Intransparente MaterialflĂŒsse an der Linie fĂŒhren zu MikrostillstĂ€nden und gefĂ€hrden die Taktzeit.", + "Gains": "Just-in-Time Materialversorgung direkt an die Linie. FachkrĂ€fte bleiben an der Maschine. Stabilisierung der Taktzeiten und OEE durch automatisierten Nachschub." + }, + "Reinigungsdienstleister": { # Facility Management + "Pains": "Margendruck durch steigende Tariflöhne bei gleichzeitigem Preisdiktat der Auftraggeber. Hohe Fluktuation (>30%) fĂŒhrt zu stĂ€ndiger Rekrutierung ('No-Show'-Quote), was Objektleiter bindet und die QualitĂ€tskontrolle vernachlĂ€ssigt.", + "Gains": "Kalkulationssicherheit durch Fixkosten statt variabler Personalkosten. Garantierte Reinigungsleistung in Objekten unabhĂ€ngig vom Personalstand. InnovationstrĂ€ger fĂŒr Ausschreibungen." + }, + "Retail - Food": { # Supermarkets + "Pains": "Reinigungskosten steigen linear zur FlĂ€che, wĂ€hrend Kundenfrequenz schwankt. Sichtbare Reinigungsmaschinen blockieren tagsĂŒber Kundenwege ('Störfaktor'). Abends/Nachts schwer Personal zu finden.", + "Gains": "Unsichtbare Reinigung: Roboter fahren in Randzeiten oder weichen Kunden dynamisch aus. Konstantes Sauberkeits-Level ('Lobby-Effekt') steigert Verweildauer." + } +} + +def update_vertical(vertical_name, new_data): + # 1. Find Page ID + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = { + "filter": { + "property": "Vertical", + "title": { + "contains": vertical_name + } + } + } + resp = requests.post(url, headers=headers, json=payload) + if resp.status_code != 200: + print(f"Error searching {vertical_name}: {resp.status_code}") + return + + results = resp.json().get("results", []) + if not results: + print(f"Skipping {vertical_name} (Not found)") + return + + page_id = results[0]["id"] + + # 2. Update Page + update_url = f"https://api.notion.com/v1/pages/{page_id}" + update_payload = { + "properties": { + "Pains": { + "rich_text": [{"text": {"content": new_data["Pains"]}}] + }, + "Gains": { + "rich_text": [{"text": {"content": new_data["Gains"]}}] + } + } + } + + upd_resp = requests.patch(update_url, headers=headers, json=update_payload) + if upd_resp.status_code == 200: + print(f"✅ Updated {vertical_name}") + else: + print(f"❌ Failed to update {vertical_name}: {upd_resp.text}") + +print("Starting Notion Update...") +for v_name, data in updates.items(): + update_vertical(v_name, data) +print("Done.") diff --git a/connector-superoffice/README.md b/connector-superoffice/README.md index ebb438c3..ac1b4622 100644 --- a/connector-superoffice/README.md +++ b/connector-superoffice/README.md @@ -1,75 +1,113 @@ -# SuperOffice Connector ("The Muscle") - GTM Engine v2.0 +# SuperOffice Connector & GTM Engine ("The Muscle & The Brain") -Dies ist der Microservice zur bidirektionalen Anbindung von **SuperOffice CRM** an die **Company Explorer Intelligence**. -Der Connector agiert als intelligenter Bote ("Muscle"): Er nimmt Webhook-Events entgegen, filtert Rauschen heraus, fragt das "Gehirn" (Company Explorer) nach Instruktionen und schreibt Ergebnisse (Marketing-Texte, Branchen-Verticals, Rollen) ins CRM zurĂŒck. +Dieses Dokument beschreibt die Architektur der **Go-to-Market (GTM) Engine**, die SuperOffice CRM mit der Company Explorer Intelligence verbindet. -## 1. Architektur: "Noise-Reduced Event Pipeline" +Ziel des Systems ist der vollautomatisierte Versand von **hyper-personalisierten E-Mails**, die so wirken, als wĂ€ren sie manuell von einem Branchenexperten geschrieben worden. -Wir nutzen eine **Event-gesteuerte Architektur** mit integrierter RauschunterdrĂŒckung, um die CRM-Last zu minimieren und Endlosschleifen zu verhindern. +--- -**Der Datenfluss:** -1. **Auslöser:** Ein User Ă€ndert Stammdaten in SuperOffice. -2. **Filterung (Noise Reduction):** Der Webhook-Receiver ignoriert sofort: - * Irrelevante EntitĂ€ten (Sales, Projects, Appointments, Documents). - * Irrelevante Felder (Telefon, E-Mail, Fax, interne Systemfelder). - * *Nur strategische Änderungen (Name, Website, Job-Titel, Position) triggern die Pipeline.* -3. **Queueing:** Valide Events landen in der lokalen `SQLite`-Queue (`connector_queue.db`). -4. **Provisioning:** Der Worker fragt den **Company Explorer** (:8000): "Was ist die KI-Wahrheit fĂŒr diesen Kontakt?". -5. **Write-Back:** Der Connector schreibt die Ergebnisse (Vertical-ID, Persona-ID, E-Mail-Snippets) via REST API zurĂŒck in die SuperOffice UDF-Felder. +## 1. Das Konzept: "Static Magic" -## 2. 🚀 Go-Live Checkliste (User Tasks) +Anders als bei ĂŒblichen KI-Tools, die E-Mails "on the fly" generieren, setzt dieses System auf **vorberechnete, statische Textbausteine**. -Um das System auf der Produktivumgebung ("Live") in Betrieb zu nehmen, mĂŒssen folgende Schritte durchgefĂŒhrt werden: +**Warum?** +1. **QualitĂ€tssicherung:** Jeder Baustein kann vor dem Versand geprĂŒft werden. +2. **Performance:** SuperOffice muss beim Versand keine KI anfragen, sondern nur Felder zusammenfĂŒgen. +3. **Konsistenz:** Ein "Finanzleiter im Maschinenbau" bekommt immer dieselbe, perfekte Argumentation – egal bei welchem Unternehmen. -### Schritt A: SuperOffice Registrierung (IT / Admin) -Da wir eine **Private App** nutzen, ist keine Zertifizierung nötig. -1. Loggen Sie sich ins [SuperOffice Developer Portal](https://dev.superoffice.com/) ein. -2. Registrieren Sie eine neue App ("Custom Application"). - * **Redirect URI:** `http://localhost` - * **Scopes:** `Contact:Read/Write`, `Person:Read/Write`, `List:Read`, `Appointment:Write`. -3. Notieren Sie sich **Client ID**, **Client Secret** und den **Token** (falls System User genutzt wird). +### Die E-Mail-Formel -### Schritt B: Konfiguration & Mapping -1. **Credentials:** Tragen Sie die Daten aus Schritt A in die `.env` Datei auf dem Server ein (`SO_CLIENT_ID`, etc.). -2. **Discovery:** Starten Sie den Container und fĂŒhren Sie einmalig das Discovery-Tool aus, um die IDs der Felder in der Live-Umgebung zu finden: - ```bash - python3 connector-superoffice/discover_fields.py - ``` -3. **Mapping Update:** Tragen Sie die ermittelten IDs in die `.env` ein: - * `VERTICAL_MAP_JSON`: Mappen Sie die CE-Branchen auf die SuperOffice "Business"-IDs. - * `PERSONA_MAP_JSON`: Mappen Sie die Rollen (z.B. "Influencer", "Wirtschaftlicher Entscheider") auf die SuperOffice "Position"-IDs. +Eine E-Mail setzt sich aus **drei statischen Komponenten** zusammen, die im CRM (SuperOffice) gespeichert sind: -### Schritt C: Webhook Einrichtung (SuperOffice Admin) -Gehen Sie in SuperOffice zu **Einstellungen & Verwaltung -> Webhooks** und legen Sie einen neuen Hook an: -* **Target URL:** `http://:8003/webhook?token=` -* **Events:** `contact.created`, `contact.changed`, `person.created`, `person.changed`. - -### Schritt D: Feiertags-Import -Damit der Versand an Feiertagen pausiert: -1. Kopieren Sie den Inhalt von `connector-superoffice/import_holidays_CRMSCRIPT.txt`. -2. FĂŒhren Sie ihn in SuperOffice unter **CRMScript -> Execute** aus. - -## 3. Business Logik & Features - -### 4.1. Persona Mapping ("Golden Record") -Das Feld `Position` (Rolle) in SuperOffice wird als Ziel-Feld fĂŒr die CE-Archetypen genutzt. -* **Logik:** Der CE analysiert den Jobtitel (z.B. "Einkaufsleiter") -> Mappt auf "Influencer". -* **Sync:** Der Connector setzt das Feld `Position` in SuperOffice auf den entsprechenden Wert (sofern in der Config gemappt). - -### 4.2. Vertical Mapping -KI-Verticals (z.B. "Healthcare - Hospital") werden auf die SuperOffice-Branchenliste gemappt. Manuelle Änderungen durch User im CRM werden aktuell beim nĂ€chsten Update ĂŒberschrieben (Master: CE). - -## 4. Testing & Simulation - -Verwenden Sie `test_full_roundtrip.py`, um die Kette zu testen, ohne E-Mails zu versenden. Das Skript erstellt stattdessen **Termine** in SuperOffice als Beweis. - -```bash -# Startet Simulation fĂŒr Person ID 2 -python3 connector-superoffice/tests/test_full_roundtrip.py +```text +[1. Opener (Unternehmens-Spezifisch)] + [2. Bridge (Persona x Vertical)] + [3. Social Proof (Vertical)] ``` -## 5. Roadmap (v2.1) +* **1. Opener (Der Haken):** Bezieht sich zu 100% auf das spezifische Unternehmen und dessen GeschĂ€ftsmodell. + * *Quelle:* `Company`-Objekt (Feld: `ai_opener`). + * *Beispiel:* "Die prĂ€zise Just-in-Time-Fertigung von **MĂŒller CNC** erfordert einen reibungslosen Materialfluss ohne MikrostillstĂ€nde." +* **2. Bridge (Die Relevanz):** Holt die Person in ihrer Rolle ab und verknĂŒpft sie mit dem Branchen-Pain. + * *Quelle:* `Matrix`-Tabelle (Feld: `intro`). + * *Beispiel:* "FĂŒr Sie als **Produktionsleiter** bedeutet das, trotz FachkrĂ€ftemangel die Taktzeiten an der Linie stabil zu halten." +* **3. Social Proof (Die Lösung):** Zeigt Referenzen und den konkreten Nutzen (Gains). + * *Quelle:* `Matrix`-Tabelle (Feld: `social_proof`). + * *Beispiel:* "Unternehmen wie **Jungheinrich** nutzen unsere Transportroboter, um FachkrĂ€fte an der Maschine zu halten und Suchzeiten um 30% zu senken." -* [ ] **Manual Override Protection:** Schutz manueller Änderungen (Vertical/Rolle) durch den User vor Überschreiben durch die KI. -* [ ] **Notion Dashboard:** KPI-Reporting. -* [ ] **Lead-Attribution:** Automatisches Setzen der `Sale.Source` auf "Marketing Automation". \ No newline at end of file +--- + +## 2. Die Datenbasis (Foundation) + +Die QualitĂ€t der Texte steht und fĂ€llt mit der Datenbasis. Diese wird zentral in **Notion** gepflegt und in den Company Explorer synchronisiert. + +### A. Verticals (Branchen) +Definiert die **Makro-Pains** und **Gains** einer Branche sowie das **passende Produkt**. +* *Beispiel:* Healthcare -> Pain: "PflegekrĂ€fte machen Logistik" -> Gain: "HĂ€nde fĂŒrs Bett" -> Produkt: Service-Roboter. +* *Wichtig:* Unterscheidung nach **Ops-Focus** (Operativ vs. Infrastruktur) steuert das Produkt (Reinigung vs. Service). + +### B. Personas (Rollen) +Definiert die **persönlichen Pains** einer Rolle. +* *Beispiel:* Produktionsleiter -> Pain: "OEE / Taktzeit". +* *Beispiel:* GeschĂ€ftsfĂŒhrer -> Pain: "ROI / Amortisation". + +--- + +## 3. Die Matrix-Engine (Multiplikation) + +Das Skript `generate_matrix.py` (im Backend) ist das HerzstĂŒck. Es berechnet **alle möglichen Kombinationen** aus Verticals und Personas voraus. + +**Logik:** +1. Lade alle Verticals (`V`) und Personas (`P`). +2. FĂŒr jede Kombination `V x P`: + * Lade `V.Pains` und `P.Pains`. + * Generiere via Gemini einen **perfekten Satz 2 (Bridge)** und **Satz 3 (Proof)**. + * Generiere ein **Subject**, das den Persona-Pain trifft. +3. Speichere das Ergebnis in der Tabelle `marketing_matrix`. + +*Ergebnis:* Eine Lookup-Tabelle, aus der fĂŒr jeden Kontakt sofort der passende Text gezogen werden kann. + +--- + +## 4. Der "Opener" (First Sentence) + +Dieser Baustein ist der einzige, der **pro Unternehmen** generiert wird (bei der Analyse/Discovery). + +**Logik:** +1. Scrape Website-Content. +2. Identifiziere das **Vertical** (z.B. Maschinenbau). +3. Lade den **Core-Pain** des Verticals (z.B. "Materialfluss"). +4. **Prompt:** "Analysiere das GeschĂ€ftsmodell von [Firma]. Formuliere einen Satz, der erklĂ€rt, warum [Core-Pain] fĂŒr genau dieses GeschĂ€ftsmodell kritisch ist." + +*Ergebnis:* Ein Satz, der beweist: "Ich habe verstanden, was ihr tut." + +--- + +## 5. SuperOffice Connector ("The Muscle") + +Der Connector ist der Bote, der diese Daten in das CRM bringt. + +**Workflow:** +1. **Trigger:** Kontakt-Änderung in SuperOffice (Webhook). +2. **Enrichment:** Connector fragt Company Explorer: "Gib mir Daten fĂŒr Firma X, Person Y". +3. **Lookup:** Company Explorer... + * Holt den `Opener` aus der Company-Tabelle. + * Bestimmt `Vertical` und `Persona`. + * Sucht den passenden Eintrag in der `MarketingMatrix`. +4. **Write-Back:** Connector schreibt die Texte in die UDF-Felder (User Defined Fields) des Kontakts in SuperOffice. + * `UDF_Opener` + * `UDF_Bridge` + * `UDF_Proof` + * `UDF_Subject` + +--- + +## 6. Setup & Wartung + +### Neue Branche hinzufĂŒgen +1. In **Notion** anlegen (Pains/Gains/Produkte definieren). +2. Sync-Skript laufen lassen: `python3 backend/scripts/sync_notion_industries.py`. +3. Matrix neu berechnen: `python3 backend/scripts/generate_matrix.py --live`. + +### Prompt-Tuning +Die Prompts fĂŒr Matrix und Opener liegen in: +* Matrix: `backend/scripts/generate_matrix.py` +* Opener: `backend/services/classification.py` (oder `enrichment.py`)