""" Elite Agent Tools – Alle Function Tools für den Webstark KI-Agenten. Modular aufgebaut, damit agent.py schlank bleibt. """ import os import json import logging import aiohttp from datetime import datetime from livekit.agents import RunContext, function_tool from livekit.agents.llm import ToolError # Professionelle Email-Templates importieren from email_templates import ( build_package_overview_email, build_thankyou_email, build_custom_email, build_developer_briefing, ) logger = logging.getLogger("livekit-agent") # ============================================================ # TOOL 1: Websuche via Tavily API # Ermöglicht Elite, aktuelle Infos aus dem Internet zu holen. # ============================================================ @function_tool() async def search_web(context: RunContext, query: str) -> str: """Durchsuche das Internet nach aktuellen Informationen. Nutze dieses Tool, wenn der Nutzer nach aktuellen Trends, Webseiten-Infos oder Branchenwissen fragt. Args: query: Die Suchanfrage, z.B. 'Webdesign Trends 2025 Schweiz' """ tavily_key = os.environ.get("TAVILY_API_KEY") if not tavily_key: raise ToolError("Websuche ist momentan nicht verfügbar. Bitte kontaktiere icarus.mod56@gmail.com.") try: from tavily import AsyncTavilyClient client = AsyncTavilyClient(api_key=tavily_key) result = await client.search(query=query, max_results=3, search_depth="basic") # Ergebnisse für die KI aufbereiten summary_parts = [] for item in result.get("results", []): title = item.get("title", "") content = item.get("content", "")[:300] url = item.get("url", "") summary_parts.append(f"**{title}**\n{content}\nQuelle: {url}") if not summary_parts: return "Keine relevanten Ergebnisse gefunden." return "\n\n".join(summary_parts) except Exception as e: logger.error(f"Tavily-Sucherror: {e}") raise ToolError("Die Websuche ist fehlgeschlagen. Bitte versuche es später erneut.") # ============================================================ # TOOL 2: Lead speichern # Speichert Kontaktdaten von Interessenten als JSON-Datei. # ============================================================ @function_tool() async def save_lead( context: RunContext, name: str, email: str, interest: str, phone: str = "", ) -> str: """Speichere die Kontaktdaten eines interessierten Kunden. Nutze dieses Tool, wenn der Nutzer seinen Namen und seine E-Mail hinterlassen möchte oder Interesse an einem Paket bekundet. Frage aktiv nach Name und E-Mail, bevor du dieses Tool aufrufst. Args: name: Vollständiger Name des Kunden email: E-Mail-Adresse des Kunden interest: Woran ist der Kunde interessiert (z.B. 'Starter-Paket', 'SEO-Optimierung') phone: Telefonnummer (optional) """ if not name or not email: raise ToolError("Name und E-Mail sind erforderlich, um den Lead zu speichern.") if "@" not in email: raise ToolError("Die E-Mail-Adresse scheint ungültig zu sein. Bitte erneut fragen.") # Unterbrechungen verbieten – wir schreiben Daten context.disallow_interruptions() lead_data = { "name": name, "email": email, "phone": phone, "interest": interest, "created_at": datetime.utcnow().isoformat(), } # Lead als JSON-Datei speichern leads_dir = os.path.join(os.path.dirname(__file__), "leads") os.makedirs(leads_dir, exist_ok=True) timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S") safe_name = name.replace(" ", "_").lower()[:20] filename = f"lead_{safe_name}_{timestamp}.json" filepath = os.path.join(leads_dir, filename) with open(filepath, "w", encoding="utf-8") as f: json.dump(lead_data, f, ensure_ascii=False, indent=2) logger.info(f"Lead gespeichert: {filepath}") # Benachrichtigungs-E-Mail an Webstark senden (wenn Resend konfiguriert) await _notify_new_lead(lead_data) return f"Kontaktdaten von {name} wurden erfolgreich gespeichert. Wir melden uns in Kürze per E-Mail bei {email}." # ============================================================ # TOOL 3: Kostenvoranschlag berechnen # ============================================================ @function_tool() async def calculate_quote( context: RunContext, package: str, extra_pages: int = 0, seo_hours: int = 0, chatbot: bool = False, ) -> str: """Berechne einen Kostenvoranschlag für ein Webstark-Projekt. Nutze dieses Tool, wenn der Nutzer nach einem Preis oder Angebot fragt. Args: package: Das gewählte Paket ('starter', 'professional', 'enterprise') extra_pages: Anzahl zusätzlicher Unterseiten (über die im Paket enthaltenen hinaus) seo_hours: Gewünschte SEO-Optimierungsstunden chatbot: Ob ein KI-Chatbot gewünscht ist (im Starter bereits enthalten) """ # Webstark Preisliste (Stand: webstark.org) packages = { "starter": { "name": "Starter (AI-Enhanced)", "base_price": 890, "included_pages": 1, "includes_chatbot": True, "description": "KI-generiertes Design, Automatisches Basis-SEO, Responsive One-Page, Chatbot-Integration (Basis)" }, "professional": { "name": "Professional (AI-Powered)", "base_price": 1490, "included_pages": 5, "includes_chatbot": True, "description": "Alles aus Starter, Content-Automation Engine, Predictive Analytics, A/B-Testing AI, Erweiterter KI-Support" }, "enterprise": { "name": "Enterprise (AI-First)", "base_price": 0, "included_pages": 0, "includes_chatbot": True, "is_custom": True, "description": "Custom AI-Workflows, Dedicated AI-Team, 24/7 Priority Support, On-site Workshops" } } EXTRA_PAGE_PRICE = 200 SEO_HOUR_PRICE = 120 CHATBOT_ADDON_PRICE = 350 pkg_key = package.lower().strip() if pkg_key not in packages: return ( f"Das Paket '{package}' ist nicht verfügbar. " f"Verfügbare Pakete: Starter (890 CHF), Professional (1.490 CHF), Enterprise (Individuell)." ) pkg = packages[pkg_key] # Enterprise = individuell if pkg.get("is_custom"): return ( f"Das {pkg['name']}-Paket ist massgeschneidert und wird individuell kalkuliert.\n" f"Enthält: {pkg['description']}\n\n" f"Bitte kontaktiere uns unter icarus.mod56@gmail.com oder hinterlasse deine Kontaktdaten." ) total = pkg["base_price"] breakdown = [f"{pkg['name']}: {pkg['base_price']} CHF"] if extra_pages > 0: page_cost = extra_pages * EXTRA_PAGE_PRICE total += page_cost breakdown.append(f"{extra_pages} Zusatzseiten: {page_cost} CHF") if seo_hours > 0: seo_cost = seo_hours * SEO_HOUR_PRICE total += seo_cost breakdown.append(f"{seo_hours} SEO-Stunden: {seo_cost} CHF") if chatbot and not pkg["includes_chatbot"]: total += CHATBOT_ADDON_PRICE breakdown.append(f"KI-Chatbot Add-on: {CHATBOT_ADDON_PRICE} CHF") result = f"Kostenvoranschlag für {pkg['name']}:\n" result += "\n".join(f" • {item}" for item in breakdown) result += f"\n\nGesamtpreis: {total} CHF (zzgl. MwSt.)" result += f"\n\nInklusive: {pkg['description']}" return result # ============================================================ # TOOL 4: E-Mail senden via Resend API # Elite kann Zusammenfassungen oder Angebote per E-Mail senden. # ============================================================ @function_tool() async def send_email( context: RunContext, to_email: str, message: str = "", subject: str = "Informationen von Webstark", email_type: str = "custom", customer_name: str = "Kunde", ) -> str: """Sende eine E-Mail an den Kunden. Nutze dieses Tool, um dem Kunden Informationen per E-Mail zu schicken. Frage den Kunden vorher um Erlaubnis. Args: to_email: Empfänger E-Mail-Adresse message: Inhalt der E-Mail (Klartext, optional bei email_type='angebot') subject: Betreff der E-Mail email_type: Art der Email. 'angebot' für Paketübersicht, 'custom' für freien Text. customer_name: Name des Kunden für die persönliche Anrede """ resend_key = os.environ.get("RESEND_API_KEY") if not resend_key: logger.error("RESEND_API_KEY ist NICHT gesetzt!") raise ToolError("E-Mail-Versand nicht möglich: API-Key fehlt.") if "@" not in to_email: raise ToolError("Die E-Mail-Adresse scheint ungültig zu sein.") # Unterbrechungen verbieten – E-Mail wird gesendet context.disallow_interruptions() # Email-Template basierend auf Typ auswählen if email_type == "angebot": html_body = build_package_overview_email(customer_name) if subject == "Informationen von Webstark": subject = "Deine Webstark Paketübersicht" else: html_body = build_custom_email(message) logger.info(f"E-Mail-Versand: an={to_email}, typ={email_type}") try: async with aiohttp.ClientSession() as session: async with session.post( "https://api.resend.com/emails", headers={ "Authorization": f"Bearer {resend_key}", "Content-Type": "application/json", "Accept-Encoding": "identity", }, json={ "from": "Elite ", "to": [to_email], "subject": subject, "html": html_body, }, timeout=aiohttp.ClientTimeout(total=10), ) as resp: if resp.status in (200, 201): logger.info(f"✅ E-Mail gesendet an {to_email}: {subject}") return f"E-Mail wurde erfolgreich an {to_email} gesendet." else: error_text = await resp.text() logger.error(f"❌ Resend HTTP {resp.status}: {error_text}") raise ToolError(f"E-Mail fehlgeschlagen (HTTP {resp.status}): {error_text}") except aiohttp.ClientError as e: logger.error(f"❌ Netzwerkfehler: {e}") raise ToolError(f"E-Mail fehlgeschlagen (Netzwerk): {e}") # ============================================================ # TOOL 5: Termin-Link vorschlagen # Gibt dem Kunden einen direkten Buchungslink. # ============================================================ @function_tool() async def suggest_appointment( context: RunContext, topic: str, ) -> str: """Schlage dem Kunden einen Beratungstermin vor. Nutze dieses Tool, wenn der Kunde ein tiefergehendes Gespräch möchte, eine komplexe Anfrage hat, oder du das Enterprise-Paket empfiehlst. Args: topic: Worum geht es im Termin (z.B. 'SEO-Beratung', 'Webdesign-Projekt', 'Enterprise-Angebot') """ # Termin-Link (anpassbar – Cal.com, Calendly, etc.) booking_url = os.environ.get("BOOKING_URL", "https://webstark.org/kontakt") return ( f"Gerne! Für eine ausführliche Beratung zum Thema '{topic}' " f"kannst du direkt einen Termin buchen:\n\n" f"📅 Termin buchen: {booking_url}\n\n" f"Alternativ erreichst du uns auch unter icarus.mod56@gmail.com." ) # ============================================================ # TOOL 6: FAQ-Wissensbasis durchsuchen # Lokale Wissensbasis für häufige Fragen (ohne API-Kosten). # ============================================================ # FAQ-Datenbank – schnelle Antworten ohne LLM-Kosten FAQ_DATABASE = [ { "keywords": ["dauer", "wie lange", "zeitraum", "fertig"], "question": "Wie lange dauert ein Website-Projekt?", "answer": "Ein Starter-Projekt ist in ca. 1-2 Wochen fertig. Professional-Projekte dauern 3-5 Wochen, Enterprise-Projekte werden individuell geplant." }, { "keywords": ["zahlung", "bezahlen", "rechnung", "anzahlung"], "question": "Wie läuft die Zahlung?", "answer": "Wir arbeiten mit 50% Anzahlung bei Projektstart und 50% bei Fertigstellung. Ratenzahlung ist bei grösseren Projekten möglich." }, { "keywords": ["hosting", "server", "domain", "online"], "question": "Ist Hosting inklusive?", "answer": "Hosting wird separat berechnet. Wir empfehlen Vercel (kostenlos für kleine Projekte) oder managed Hosting ab 15 CHF/Monat. Die Domain-Registrierung können wir ebenfalls übernehmen." }, { "keywords": ["änderung", "revision", "korrektur", "anpassen"], "question": "Wie viele Änderungen sind inklusive?", "answer": "Im Starter-Paket sind 2 Korrekturschleifen inklusive, im Professional-Paket 5. Zusätzliche Änderungen werden zum Stundensatz von 120 CHF berechnet." }, { "keywords": ["garantie", "geld zurück", "zufriedenheit", "unzufrieden"], "question": "Gibt es eine Zufriedenheitsgarantie?", "answer": "Ja! Wir bieten eine 30-Tage Geld-zurück-Garantie. Wenn du nicht zufrieden bist, erstatten wir den vollen Betrag." }, { "keywords": ["seo", "google", "ranking", "suchmaschine", "auffindbar"], "question": "Was beinhaltet die SEO-Optimierung?", "answer": "Basis-SEO (im Starter inklusive) umfasst: Meta-Tags, Seitengeschwindigkeit, Mobile-Optimierung und strukturierte Daten. Erweitertes SEO (Professional) beinhaltet zusätzlich Keyword-Analyse, Content-Strategie und monatliches Reporting." }, { "keywords": ["chatbot", "ki", "bot", "automatisch", "chat"], "question": "Was kann der Chatbot?", "answer": "Der Basis-Chatbot (im Starter) beantwortet häufige Fragen automatisch. Der erweiterte KI-Support (Professional) lernt aus Gesprächen und kann komplexere Anfragen bearbeiten." }, { "keywords": ["kontakt", "erreichen", "email", "telefon", "support"], "question": "Wie erreiche ich Webstark?", "answer": "Du erreichst uns per E-Mail unter icarus.mod56@gmail.com oder über den Live-Chat auf webstark.org. Telefon-Support ist aktuell nicht verfügbar." }, ] @function_tool() async def lookup_faq(context: RunContext, question: str) -> str: """Suche in der Webstark FAQ-Wissensbasis nach einer Antwort. Nutze dieses Tool ZUERST, bevor du search_web verwendest. Es enthält häufig gestellte Fragen zu Preisen, Abläufen und Services. Args: question: Die Frage des Kunden, z.B. 'Wie lange dauert ein Projekt?' """ question_lower = question.lower() # Relevanz-Score berechnen basierend auf Keyword-Matches best_match = None best_score = 0 for faq in FAQ_DATABASE: score = sum(1 for kw in faq["keywords"] if kw in question_lower) if score > best_score: best_score = score best_match = faq if best_match and best_score > 0: return f"FAQ: {best_match['question']}\n\n{best_match['answer']}" return "Keine passende FAQ gefunden. Nutze search_web für eine Internet-Recherche oder verweise auf icarus.mod56@gmail.com." # ============================================================ # TOOL 7: Gesprächs-Zusammenfassung erstellen # Erstellt eine strukturierte Zusammenfassung am Ende des Gesprächs. # ============================================================ @function_tool() async def create_conversation_summary( context: RunContext, customer_name: str, topics_discussed: str, action_items: str, interest_level: str = "mittel", customer_email: str = "", ) -> str: """Erstelle eine Zusammenfassung des Gesprächs. Nutze dieses Tool am Ende eines Gesprächs oder wenn der Kunde sich verabschiedet, um alle wichtigen Punkte festzuhalten. Wenn die E-Mail des Kunden bekannt ist, wird automatisch eine Danke-Email mit Zusammenfassung gesendet. Args: customer_name: Name des Kunden (oder 'Unbekannt') topics_discussed: Komma-getrennte Liste der besprochenen Themen action_items: Nächste Schritte oder offene Aufgaben interest_level: Wie interessiert ist der Kunde? ('hoch', 'mittel', 'niedrig') customer_email: E-Mail des Kunden (falls bekannt, für Danke-Email) """ context.disallow_interruptions() summary = { "customer_name": customer_name, "timestamp": datetime.utcnow().isoformat(), "topics": topics_discussed, "action_items": action_items, "interest_level": interest_level, "customer_email": customer_email, } # Zusammenfassung speichern summaries_dir = os.path.join(os.path.dirname(__file__), "summaries") os.makedirs(summaries_dir, exist_ok=True) timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S") filename = f"summary_{timestamp}.json" filepath = os.path.join(summaries_dir, filename) with open(filepath, "w", encoding="utf-8") as f: json.dump(summary, f, ensure_ascii=False, indent=2) logger.info(f"Gesprächs-Zusammenfassung gespeichert: {filepath}") # Developer-Briefing an Webstark senden await _notify_summary(summary) # Automatische Danke-Email an den Kunden (falls Email bekannt) if customer_email and "@" in customer_email: await _send_thankyou_email(customer_email, customer_name, topics_discussed, action_items) result = ( f"Zusammenfassung gespeichert!\n" f"Kunde: {customer_name}\n" f"Themen: {topics_discussed}\n" f"Nächste Schritte: {action_items}\n" f"Interesse: {interest_level}" ) if customer_email: result += f"\nDanke-Email gesendet an: {customer_email}" return result # ============================================================ # INTERNE HILFSFUNKTIONEN (nicht als Tool exponiert) # ============================================================ async def _notify_new_lead(lead_data: dict) -> None: """Sendet eine Benachrichtigungs-E-Mail an Webstark bei neuem Lead.""" resend_key = os.environ.get("RESEND_API_KEY") notify_email = os.environ.get("NOTIFY_EMAIL", "icarus.mod56@gmail.com") if not resend_key: logger.info("Kein RESEND_API_KEY – Lead-Benachrichtigung übersprungen.") return resend_from_name = os.environ.get("RESEND_FROM_NAME", "Elite") resend_from_email = os.environ.get("RESEND_FROM_EMAIL", "elite@webstark.org") try: async with aiohttp.ClientSession() as session: await session.post( "https://api.resend.com/emails", headers={ "Authorization": f"Bearer {resend_key}", "Content-Type": "application/json", "Accept-Encoding": "identity", }, json={ "from": "Elite ", "to": [notify_email], "subject": f"🔥 Neuer Lead: {lead_data['name']} – {lead_data['interest']}", "html": f"""

Neuer Lead von Elite

Name: {lead_data['name']}

E-Mail: {lead_data['email']}

Telefon: {lead_data.get('phone', '-')}

Interesse: {lead_data['interest']}

Zeitpunkt: {lead_data['created_at']}

""", }, timeout=aiohttp.ClientTimeout(total=10), ) logger.info(f"Lead-Benachrichtigung gesendet an {notify_email}") except Exception as e: logger.warning(f"Lead-Benachrichtigung fehlgeschlagen: {e}") async def _notify_summary(summary: dict) -> None: """Sendet ein detailliertes Developer-Briefing per E-Mail an Webstark.""" resend_key = os.environ.get("RESEND_API_KEY") notify_email = os.environ.get("NOTIFY_EMAIL", "icarus.mod56@gmail.com") if not resend_key: return try: # Professionelles Briefing-Template verwenden html_body = build_developer_briefing(summary) async with aiohttp.ClientSession() as session: await session.post( "https://api.resend.com/emails", headers={ "Authorization": f"Bearer {resend_key}", "Content-Type": "application/json", "Accept-Encoding": "identity", }, json={ "from": "Elite ", "to": [notify_email], "subject": f"📊 Briefing: {summary['customer_name']} ({summary.get('interest_level', 'mittel')})", "html": html_body, }, timeout=aiohttp.ClientTimeout(total=10), ) logger.info(f"Developer-Briefing gesendet an {notify_email}") except Exception as e: logger.warning(f"Developer-Briefing fehlgeschlagen: {e}") async def _send_thankyou_email(to_email: str, name: str, topics: str, next_steps: str) -> None: """Sendet eine automatische Danke-Email an den Kunden nach dem Gespräch.""" resend_key = os.environ.get("RESEND_API_KEY") if not resend_key: return try: html_body = build_thankyou_email(name, topics, next_steps) async with aiohttp.ClientSession() as session: await session.post( "https://api.resend.com/emails", headers={ "Authorization": f"Bearer {resend_key}", "Content-Type": "application/json", "Accept-Encoding": "identity", }, json={ "from": "Elite ", "to": [to_email], "subject": f"Danke für dein Interesse, {name}! – Webstark", "html": html_body, }, timeout=aiohttp.ClientTimeout(total=10), ) logger.info(f"Danke-Email gesendet an {to_email}") except Exception as e: logger.warning(f"Danke-Email fehlgeschlagen: {e}") # Alle Tools als Liste exportieren (wird in agent.py importiert) ALL_TOOLS = [ search_web, save_lead, calculate_quote, send_email, suggest_appointment, lookup_faq, create_conversation_summary, ]