diff --git a/lead-engine/app.py b/lead-engine/app.py index e2c99fd4..7edd37fb 100644 --- a/lead-engine/app.py +++ b/lead-engine/app.py @@ -1,6 +1,6 @@ import streamlit as st import pandas as pd -from db import get_leads, init_db +from db import get_leads, init_db, reset_lead import json import re import os @@ -130,6 +130,15 @@ if not df.empty: pass with st.expander(f"{date_str} | {row['company_name']} ({row['status']})"): + # Quality Warning + meta = {} + if row.get('lead_metadata'): + try: meta = json.loads(row['lead_metadata']) + except: pass + + if meta.get('is_low_quality'): + st.warning("⚠️ Low Quality Lead detected (Free-mail or missing company). Use for reclamation if applicable.") + c1, c2 = st.columns(2) # --- Left Column: Lead Data --- @@ -174,6 +183,11 @@ if not df.empty: st.rerun() else: st.error(f"Sync failed: {res.get('message')}") + else: + if c1.button("🔄 Reset Status to New", key=f"reset_{row['id']}"): + reset_lead(row['id']) + st.toast("Lead status reset.") + st.rerun() with c1.expander("Show Original Email Content"): st.text(clean_html_to_text(row['raw_body'])) diff --git a/lead-engine/db.py b/lead-engine/db.py index a483f02d..61971b40 100644 --- a/lead-engine/db.py +++ b/lead-engine/db.py @@ -55,9 +55,15 @@ def insert_lead(lead_data): 'area': lead_data.get('area'), 'purpose': lead_data.get('purpose'), 'zip': lead_data.get('zip'), - 'city': lead_data.get('city') + 'city': lead_data.get('city'), + 'role': lead_data.get('role'), + 'is_free_mail': lead_data.get('is_free_mail', False), + 'is_low_quality': lead_data.get('is_low_quality', False) } + # Use provided received_at or default to now + received_at = lead_data.get('received_at') or datetime.now() + conn = sqlite3.connect(DB_PATH) c = conn.cursor() try: @@ -66,7 +72,7 @@ def insert_lead(lead_data): VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( lead_data.get('id'), - datetime.now(), + received_at, lead_data.get('company'), lead_data.get('contact'), lead_data.get('email'), @@ -111,3 +117,11 @@ def update_lead_status(lead_id, status, response_draft=None): c.execute('UPDATE leads SET status = ? WHERE id = ?', (status, lead_id)) conn.commit() conn.close() + +def reset_lead(lead_id): + """Resets a lead to 'new' status and clears enrichment data.""" + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute('UPDATE leads SET status = "new", enrichment_data = NULL WHERE id = ?', (lead_id,)) + conn.commit() + conn.close() diff --git a/lead-engine/generate_reply.py b/lead-engine/generate_reply.py index 4656ab78..251ada8e 100644 --- a/lead-engine/generate_reply.py +++ b/lead-engine/generate_reply.py @@ -1,6 +1,8 @@ import os import json import requests +import sqlite3 +import re # Load API Key def get_gemini_key(): @@ -21,71 +23,145 @@ def get_gemini_key(): return os.getenv("GEMINI_API_KEY") -def generate_email_draft(lead_data, company_data, booking_link="https://outlook.office365.com/owa/calendar/RoboplanetGmbH@robo-planet.de/bookings/"): +def get_matrix_context(industry_name, persona_name): + """Fetches Pains, Gains and Arguments from CE Database.""" + context = { + "industry_pains": "", + "industry_gains": "", + "persona_description": "", + "persona_arguments": "" + } + db_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'companies_v3_fixed_2.db') + if not os.path.exists(db_path): + return context + + try: + conn = sqlite3.connect(db_path) + c = conn.cursor() + + # Get Industry Data + c.execute('SELECT pains, gains FROM industries WHERE name = ?', (industry_name,)) + ind_res = c.fetchone() + if ind_res: + context["industry_pains"], context["industry_gains"] = ind_res + + # Get Persona Data + c.execute('SELECT description, convincing_arguments FROM personas WHERE name = ?', (persona_name,)) + per_res = c.fetchone() + if per_res: + context["persona_description"], context["persona_arguments"] = per_res + + conn.close() + except Exception as e: + print(f"DB Error in matrix lookup: {e}") + + return context + +def get_product_recommendation(area_str): """ - Generates a personalized sales email using Gemini API. + Selects the right robot based on the surface area mentioned in the lead. + """ + # Naive extraction of first number in the string + nums = re.findall(r'\d+', area_str.replace('.', '').replace(',', '')) + area_val = int(nums[0]) if nums else 0 + + if area_val >= 5000 or "über 10.000" in area_str: + return { + "name": "Scrubber 75", + "reason": "als industrielles Kraftpaket für Großflächen ausgelegt", + "usp": "höchste Effizienz und Autonomie auf mehreren tausend Quadratmetern" + } + elif area_val >= 1000: + return { + "name": "Scrubber 50 oder Phantas", + "reason": "die optimale Balance zwischen Reinigungsleistung und Wendigkeit", + "usp": "ideal für mittelgroße Fertigungs- und Lagerbereiche" + } + else: + return { + "name": "Phantas oder Pudu CC1", + "reason": "kompakt und wendig für komplexe Umgebungen", + "usp": "perfekt für Büros, Praxen oder engere Verkehrswege" + } + +def generate_email_draft(lead_data, company_data, booking_link="[IHR BUCHUNGSLINK - BITTE IN .ENV EINTRAGEN]"): + """ + Generates a high-end, personalized sales email using Gemini API and Matrix knowledge. """ api_key = get_gemini_key() if not api_key: return "Error: Gemini API Key not found." - # Extract Data + # Extract Data from Lead Engine company_name = lead_data.get('company_name', 'Interessent') contact_name = lead_data.get('contact_name', 'Damen und Herren') # Metadata from Lead meta = {} if lead_data.get('lead_metadata'): - try: - meta = json.loads(lead_data.get('lead_metadata')) - except: - pass + try: meta = json.loads(lead_data['lead_metadata']) + except: pass area = meta.get('area', 'Unbekannte Fläche') purpose = meta.get('purpose', 'Reinigung') - city = meta.get('city', '') - role = meta.get('role', 'Unbekannt') + role = meta.get('role', 'Wirtschaftlicher Entscheider') # Data from Company Explorer - ce_summary = company_data.get('research_dossier') or company_data.get('summary', 'Keine Details verfügbar.') - ce_vertical = company_data.get('industry_ai') or company_data.get('vertical', 'Allgemein') - ce_website = company_data.get('website', '') + ce_summary = company_data.get('research_dossier') or company_data.get('summary', '') + ce_vertical = company_data.get('industry_ai') or company_data.get('vertical', 'Industry - Manufacturing') + ce_opener = company_data.get('ai_opener', '') - # Prompt Engineering + # Product logic + product = get_product_recommendation(area) + + # Fetch "Golden Records" from Matrix + matrix = get_matrix_context(ce_vertical, role) + + # Prompt Engineering for "Unwiderstehliche E-Mail" prompt = f""" - Du bist ein erfahrener Vertriebsexperte für Roboter-Reinigungslösungen bei Robo-Planet. - Deine Aufgabe ist es, eine Antwort-E-Mail auf eine Lead-Anfrage zu formulieren. - - HINTERGRUND ZUM PROSPEKT (Aus Analyse): + Du bist ein Senior Sales Executive bei Robo-Planet. Antworte auf eine Anfrage von Tradingtwins. + Schreibe eine E-Mail auf "Human Expert Level". + + WICHTIGE STRATEGIE: + - Starte NICHT mit seiner Position (CFO). Starte mit der Wertschätzung für sein UNTERNEHMEN ({company_name}). + - Der Empfänger soll durch die Tiefe der Argumente MERKEN, dass wir für einen Entscheider schreiben. + - Mappe ihn erst später als "finanziellen/wirtschaftlichen Entscheider". + - Erwähne eine ROI-Perspektive (Amortisation). + + KONTEXT (Vom Company Explorer): - Firma: {company_name} - - Standort: {city} - - Branche/Vertical: {ce_vertical} - - Web-Zusammenfassung: {ce_summary} + - Branche: {ce_vertical} + - Branchen-Pains (Nutze diese für die Argumentation): {matrix['industry_pains']} + - Branchen-Gains: {matrix['industry_gains']} + - Dossier/Business-Profil: {ce_summary} + - Strategischer Aufhänger: {ce_opener} ANSPRECHPARTNER: - Name: {contact_name} - - Rolle/Position: {role} (WICHTIG: Nutze dieses Wissen für den Tonfall. Ein Geschäftsführer braucht Argumente zu ROI/Effizienz, ein Facility Manager zu Operativem/Handling.) + - Rolle: {role} + + PRODUKT-EMPFEHLUNG (Basierend auf Fläche {area}): + - Modell: {product['name']} + - Warum: {product['reason']} + - USP: {product['usp']} - ANFRAGE-DETAILS (Vom Kunden): - - Reinigungsfläche: {area} - - Einsatzzweck: {purpose} + ANFRAGE-DETAILS: + - Bedarf: {area} + - Zweck: {purpose} - DEIN ZIEL: - Schreibe eine kurze, prägnante und wertschätzende E-Mail. - 1. Bedanke dich für die Anfrage. - 2. Zeige kurz, dass du verstanden hast, was die Firma macht. - 3. Gehe auf die Fläche ({area}) ein. - - Wenn > 1000qm oder Industrie/Halle: Erwähne den "Puma M20" oder "Scrubber 75" als Kraftpaket. - - Wenn < 1000qm oder Büro/Praxis/Gastro: Erwähne den "Phantas" oder "Pudu CC1" als wendige Lösung. - - Wenn "Unbekannt": Stelle eine offene Frage zur Umgebung. - 4. Call to Action: Schlage ein kurzes Beratungsgespräch vor. - 5. Füge diesen Buchungslink ein: {booking_link} + AUFGABE: + Schreibe eine E-Mail mit dieser Struktur: + 1. EINSTIEG: Fokus auf Klemm Bohrtechnik und deren Marktstellung/Produkte (Bezug auf den 'Strategischen Aufhänger'). + 2. DIE BRÜCKE: Verknüpfe die Präzision ihrer Produkte mit der Notwendigkeit von sauberen Hallenböden (besonders bei {area}). Nutze den Schmerzpunkt "Prozesssicherheit/Sensorik". + 3. DIE LÖSUNG: Positioniere den {product['name']} als genau die richtige Wahl für diese Größenordnung ({area}). + 4. ROI-LOGIK: Sprich ihn als wirtschaftlichen Entscheider an. Erwähne, dass wir für solche Projekte ROI-Kalkulationen erstellen, die oft eine Amortisation in unter 18-24 Monaten zeigen. + 5. CALL TO ACTION: Beratungsgespräch + Buchungslink: {booking_link} - TONALITÄT: - Professionell, hilfreich, auf den Punkt. Keine Marketing-Floskeln. + STIL: + Senior, Augenhöhe, keine Floskeln, extrem fokussiert auf Effizienz und Qualität. FORMAT: - Betreff: [Vorschlag für Betreff] + Betreff: [Relevanter Betreff, der direkt auf Klemm Bohrtechnik / Effizienz zielt] [E-Mail Text] """ @@ -93,9 +169,7 @@ def generate_email_draft(lead_data, company_data, booking_link="https://outlook. # Call Gemini API url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}" headers = {'Content-Type': 'application/json'} - payload = { - "contents": [{"parts": [{"text": prompt}]}] - } + payload = {"contents": [{"parts": [{"text": prompt}]}]} try: response = requests.post(url, headers=headers, json=payload) @@ -116,4 +190,4 @@ if __name__ == "__main__": "vertical": "Healthcare / Krankenhaus", "summary": "Ein großes Klinikum der Maximalversorgung mit Fokus auf Kardiologie." } - print(generate_email_draft(mock_lead, mock_company)) + print(generate_email_draft(mock_lead, mock_company)) \ No newline at end of file diff --git a/lead-engine/trading_twins_ingest.py b/lead-engine/trading_twins_ingest.py index cb60107d..75a39136 100644 --- a/lead-engine/trading_twins_ingest.py +++ b/lead-engine/trading_twins_ingest.py @@ -70,6 +70,18 @@ def fetch_tradingtwins_emails(token, limit=200): filtered = [m for m in all_msgs if "Neue Anfrage zum Thema Roboter" in (m.get('subject') or '')] return filtered +def is_free_mail(email_addr): + """Checks if an email belongs to a known free-mail provider.""" + if not email_addr: return False + free_domains = { + 'gmail.com', 'googlemail.com', 'outlook.com', 'hotmail.com', 'live.com', + 'msn.com', 'icloud.com', 'me.com', 'mac.com', 'yahoo.com', 'ymail.com', + 'rocketmail.com', 'gmx.de', 'gmx.net', 'web.de', 't-online.de', + 'freenet.de', 'mail.com', 'protonmail.com', 'proton.me', 'online.de' + } + domain = email_addr.split('@')[-1].lower() + return domain in free_domains + def parse_tradingtwins_html(html_body): """ Extracts data from the Tradingtwins HTML table structure. @@ -80,15 +92,15 @@ def parse_tradingtwins_html(html_body): # Map label names in HTML to our keys field_map = { 'Firma': 'company', - 'Vorname': 'contact_first', # Key fixed to match ingest.py logic - 'Nachname': 'contact_last', # Key fixed to match ingest.py logic + 'Vorname': 'contact_first', + 'Nachname': 'contact_last', 'E-Mail': 'email', 'Rufnummer': 'phone', - 'Einsatzzweck': 'purpose', # Specific field - 'Reinigungs-Fläche': 'area', # Specific field + 'Einsatzzweck': 'purpose', + 'Reinigungs-Fläche': 'area', 'PLZ': 'zip', 'Stadt': 'city', - 'Lead-ID': 'source_id' # Mapped to DB column source_id + 'Lead-ID': 'source_id' } for label, key in field_map.items(): @@ -103,6 +115,13 @@ def parse_tradingtwins_html(html_body): if data.get('contact_first') and data.get('contact_last'): data['contact'] = f"{data['contact_first']} {data['contact_last']}" + # Quality Check: Free mail or missing company + email = data.get('email', '') + company = data.get('company', '-') + + data['is_free_mail'] = is_free_mail(email) + data['is_low_quality'] = data['is_free_mail'] or company == '-' or not company + # Ensure source_id is present and map to 'id' for db.py compatibility if not data.get('source_id'): data['source_id'] = f"tt_unknown_{int(datetime.now().timestamp())}" @@ -111,7 +130,7 @@ def parse_tradingtwins_html(html_body): return data -def process_leads(auto_sync=True): +def process_leads(auto_sync=False): init_db() new_count = 0 try: