DeepCoreB4's picture
Upload 2 files
2dfab01 verified
"""
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 <elite@webstark.org>",
"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 <elite@webstark.org>",
"to": [notify_email],
"subject": f"🔥 Neuer Lead: {lead_data['name']}{lead_data['interest']}",
"html": f"""
<h2>Neuer Lead von Elite</h2>
<p><b>Name:</b> {lead_data['name']}</p>
<p><b>E-Mail:</b> {lead_data['email']}</p>
<p><b>Telefon:</b> {lead_data.get('phone', '-')}</p>
<p><b>Interesse:</b> {lead_data['interest']}</p>
<p><b>Zeitpunkt:</b> {lead_data['created_at']}</p>
""",
},
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 <elite@webstark.org>",
"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 <elite@webstark.org>",
"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,
]