Spaces:
Paused
Paused
Upload main.py
Browse files
main.py
CHANGED
|
@@ -1,162 +1,168 @@
|
|
| 1 |
-
|
| 2 |
-
from
|
| 3 |
-
import
|
| 4 |
-
from
|
| 5 |
-
|
| 6 |
-
import os
|
| 7 |
-
import json
|
| 8 |
-
import datetime
|
| 9 |
-
|
| 10 |
-
# --- 1. SETUP & AUTHENTIFIZIERUNG ---
|
| 11 |
-
# Wir lesen die Keys aus den Hugging Face Secrets (Environment Variables)
|
| 12 |
-
firebase_key_json = os.getenv("FIREBASE_KEY")
|
| 13 |
-
gemini_key = os.getenv("GEMINI_API_KEY")
|
| 14 |
-
|
| 15 |
-
if not firebase_key_json or not gemini_key:
|
| 16 |
-
# Fallback für lokales Testen (falls du es lokal ausführst)
|
| 17 |
-
# Ersetze das nur lokal, NICHT im Repo hochladen!
|
| 18 |
-
print("⚠️ Warnung: Keine ENV-Variables gefunden. Prüfe lokale Config.")
|
| 19 |
-
else:
|
| 20 |
-
# Firebase initialisieren
|
| 21 |
-
if not firebase_admin._apps:
|
| 22 |
-
cred = credentials.Certificate(json.loads(firebase_key_json))
|
| 23 |
-
firebase_admin.initialize_app(cred)
|
| 24 |
-
|
| 25 |
-
db = firestore.client()
|
| 26 |
-
|
| 27 |
-
# Gemini initialisieren (nutzt dein funktionierendes Modell)
|
| 28 |
-
genai.configure(api_key=gemini_key)
|
| 29 |
-
model = genai.GenerativeModel('gemini-2.0-flash')
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
app = FastAPI()
|
| 32 |
|
| 33 |
-
#
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
# ======================================================
|
| 38 |
-
# ENDPOINT 1: Vapi fragt Wissen ab (LIVE im Anruf)
|
| 39 |
-
# ======================================================
|
| 40 |
-
@app.post("/search")
|
| 41 |
-
async def search_knowledge(request: SearchRequest):
|
| 42 |
-
print(f"🔎 Suche nach: {request.query}")
|
| 43 |
-
|
| 44 |
-
# A. Wissen aus Firebase holen (Nur genehmigtes!)
|
| 45 |
-
docs = db.collection("knowledge_base").where("status", "==", "approved").stream()
|
| 46 |
-
|
| 47 |
-
# Wir bauen einen Text-Kontext für Gemini
|
| 48 |
-
knowledge_list = []
|
| 49 |
-
for doc in docs:
|
| 50 |
-
d = doc.to_dict()
|
| 51 |
-
# Wir geben Gemini Frage + Antwort als Kontext
|
| 52 |
-
knowledge_list.append(f"Frage: {d.get('question')}\nAntwort: {d.get('answer')}")
|
| 53 |
-
|
| 54 |
-
context_text = "\n---\n".join(knowledge_list)
|
| 55 |
-
|
| 56 |
-
if not context_text:
|
| 57 |
-
return {"result": "Leider habe ich dazu keine Informationen in meiner Datenbank."}
|
| 58 |
-
|
| 59 |
-
# B. Gemini entscheidet, welche Antwort passt
|
| 60 |
-
prompt = f"""
|
| 61 |
-
Du bist der intelligente Assistent "Alex".
|
| 62 |
-
Ein User fragt: "{request.query}"
|
| 63 |
-
|
| 64 |
-
Hier ist dein geprüftes Wissen:
|
| 65 |
-
{context_text}
|
| 66 |
-
|
| 67 |
-
Aufgabe:
|
| 68 |
-
1. Suche die passendste Antwort im Wissen.
|
| 69 |
-
2. Formuliere sie freundlich und kurz (gesprochene Sprache).
|
| 70 |
-
3. Wenn die Antwort NICHT im Wissen steht, antworte exakt: "Dazu habe ich leider keine Informationen vorliegen." (Erfinde nichts!)
|
| 71 |
-
"""
|
| 72 |
-
|
| 73 |
-
try:
|
| 74 |
-
response = model.generate_content(prompt)
|
| 75 |
-
answer = response.text.strip()
|
| 76 |
-
print(f"🤖 Antwort: {answer}")
|
| 77 |
-
return {"result": answer}
|
| 78 |
-
except Exception as e:
|
| 79 |
-
print(f"❌ Fehler: {e}")
|
| 80 |
-
return {"result": "Es gab einen technischen Fehler bei der Abfrage."}
|
| 81 |
-
|
| 82 |
-
# ======================================================
|
| 83 |
-
# ENDPOINT 2: Lernen (NACH dem Anruf)
|
| 84 |
-
# ======================================================
|
| 85 |
-
@app.post("/learn")
|
| 86 |
-
async def learn_from_call(request: Request):
|
| 87 |
-
print("🎓 Analysiere Call für Lerneffekt...")
|
| 88 |
-
|
| 89 |
try:
|
| 90 |
-
|
|
|
|
|
|
|
| 91 |
|
| 92 |
-
|
| 93 |
-
# Struktur kann variieren, wir suchen das Feld "transcript" oder "messages"
|
| 94 |
-
# Hier ein generischer Ansatz für Vapi Webhooks:
|
| 95 |
-
transcript = ""
|
| 96 |
-
|
| 97 |
-
if "message" in payload and "transcript" in payload["message"]:
|
| 98 |
-
transcript = payload["message"]["transcript"]
|
| 99 |
-
elif "transcript" in payload:
|
| 100 |
-
transcript = payload["transcript"]
|
| 101 |
-
else:
|
| 102 |
-
# Fallback: Wir nehmen den ganzen Body als Text, falls Vapi anders sendet
|
| 103 |
-
transcript = str(payload)
|
| 104 |
-
|
| 105 |
-
if not transcript or len(transcript) < 50:
|
| 106 |
-
return {"status": "skipped", "reason": "Transkript zu kurz oder leer"}
|
| 107 |
-
|
| 108 |
-
# --- DER GEMINI LERN-CODE ---
|
| 109 |
-
prompt = f"""
|
| 110 |
-
Analysiere dieses Telefonat-Transkript.
|
| 111 |
-
Identifiziere Fragen, die der Bot NICHT oder nur schlecht beantworten konnte.
|
| 112 |
-
|
| 113 |
-
Erstelle für jede Wissenslücke einen neuen Datenbank-Eintrag.
|
| 114 |
-
|
| 115 |
-
Format (JSON Array):
|
| 116 |
-
[
|
| 117 |
-
{{
|
| 118 |
-
"question": "Die Frage des Kunden",
|
| 119 |
-
"answer": "Die ideale Antwort (formuliere sie professionell)",
|
| 120 |
-
"keywords": ["keyword1", "keyword2"],
|
| 121 |
-
"category": "Call-Analyse"
|
| 122 |
-
}}
|
| 123 |
-
]
|
| 124 |
-
|
| 125 |
-
Wenn alles gut lief und keine Lücken da sind, antworte mit einem leeren Array [].
|
| 126 |
-
|
| 127 |
-
Transkript:
|
| 128 |
-
{transcript[:15000]}
|
| 129 |
-
"""
|
| 130 |
|
| 131 |
-
|
| 132 |
-
clean_json = response.text.replace("```json", "").replace("```", "").strip()
|
| 133 |
-
|
| 134 |
-
if not clean_json:
|
| 135 |
-
return {"status": "no_data_generated"}
|
| 136 |
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
| 155 |
|
| 156 |
-
|
| 157 |
-
print(f"✅ {count} neue Einträge zur Prüfung angelegt.")
|
| 158 |
-
return {"status": "success", "entries_created": count}
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
except Exception as e:
|
| 161 |
-
print(f"
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uvicorn
|
| 2 |
+
from fastapi import FastAPI, Request
|
| 3 |
+
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text
|
| 4 |
+
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
| 5 |
+
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
+
# --- 1. DATENBANK SETUP ---
|
| 8 |
+
DATABASE_URL = "sqlite:///./vapi_leads.db"
|
| 9 |
+
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
| 10 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 11 |
+
|
| 12 |
+
class Base(DeclarativeBase):
|
| 13 |
+
pass
|
| 14 |
+
|
| 15 |
+
class CallLog(Base):
|
| 16 |
+
__tablename__ = "vapi_anrufe"
|
| 17 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 18 |
+
call_id = Column(String, unique=True)
|
| 19 |
+
customer_number = Column(String)
|
| 20 |
+
status = Column(String)
|
| 21 |
+
transcript = Column(Text, nullable=True)
|
| 22 |
+
timestamp = Column(DateTime, default=datetime.utcnow)
|
| 23 |
+
|
| 24 |
+
Base.metadata.create_all(bind=engine)
|
| 25 |
+
|
| 26 |
+
# --- 2. SERVER (Der entscheidende Teil) ---
|
| 27 |
app = FastAPI()
|
| 28 |
|
| 29 |
+
# WICHTIG: Hier definieren wir die Tür "/vapi", an die Vapi klopft!
|
| 30 |
+
@app.post("/vapi")
|
| 31 |
+
async def handle_vapi_event(request: Request):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
try:
|
| 33 |
+
data = await request.json()
|
| 34 |
+
message = data.get("message", {})
|
| 35 |
+
msg_type = message.get("type")
|
| 36 |
|
| 37 |
+
print(f"📩 Event empfangen: {msg_type}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
db = SessionLocal()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
# Fall 1: Anruf startet (neuer Name: assistant.started)
|
| 42 |
+
if msg_type == "assistant.started":
|
| 43 |
+
call_details = message.get("call", {})
|
| 44 |
+
customer_number = call_details.get("customer", {}).get("number", "Unbekannt")
|
| 45 |
+
call_id = call_details.get("id")
|
| 46 |
+
|
| 47 |
+
print(f"📞 Neuer Anruf erkannt! ID: {call_id}")
|
| 48 |
+
|
| 49 |
+
# Prüfen ob Anruf schon da ist, sonst anlegen
|
| 50 |
+
existing = db.query(CallLog).filter(CallLog.call_id == call_id).first()
|
| 51 |
+
if not existing:
|
| 52 |
+
new_call = CallLog(call_id=call_id, customer_number=customer_number, status="Live")
|
| 53 |
+
db.add(new_call)
|
| 54 |
+
db.commit()
|
| 55 |
+
|
| 56 |
+
# Fall 2: Anruf zu Ende (Transkript speichern)
|
| 57 |
+
elif msg_type == "end-of-call-report":
|
| 58 |
+
call_id = message.get("call", {}).get("id")
|
| 59 |
+
transcript = message.get("transcript", "")
|
| 60 |
+
summary = message.get("summary", "Keine Zusammenfassung")
|
| 61 |
|
| 62 |
+
print(f"✅ Anruf beendet. Transkript empfangen.")
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
existing_call = db.query(CallLog).filter(CallLog.call_id == call_id).first()
|
| 65 |
+
if existing_call:
|
| 66 |
+
existing_call.status = "Beendet"
|
| 67 |
+
existing_call.transcript = transcript
|
| 68 |
+
db.commit()
|
| 69 |
+
|
| 70 |
+
db.close()
|
| 71 |
except Exception as e:
|
| 72 |
+
print(f"⚠️ Fehler beim Verarbeiten: {e}")
|
| 73 |
+
|
| 74 |
+
return {"status": "ok"}
|
| 75 |
+
from fastapi.responses import HTMLResponse
|
| 76 |
+
|
| 77 |
+
# --- WEBSEITE FÜR TESTER ---
|
| 78 |
+
@app.get("/", response_class=HTMLResponse)
|
| 79 |
+
async def get_web_interface():
|
| 80 |
+
# HIER DEINE DATEN EINTRAGEN:
|
| 81 |
+
VAPI_PUBLIC_KEY = "f4f1efff-091c-495f-acab-4669b1cc7cbe" # Beginnt nicht mit 'ey...', ist kürzer
|
| 82 |
+
ASSISTANT_ID = "4008c58f-556c-44a3-b78d-2b920ea80c66"
|
| 83 |
+
|
| 84 |
+
html_content = f"""
|
| 85 |
+
<!DOCTYPE html>
|
| 86 |
+
<html lang="de">
|
| 87 |
+
<head>
|
| 88 |
+
<meta charset="UTF-8">
|
| 89 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 90 |
+
<title>Test: Alex Sales Bot</title>
|
| 91 |
+
<script src="https://cdn.jsdelivr.net/gh/VapiAI/html-script-tag@latest/dist/assets/index.js"></script>
|
| 92 |
+
<style>
|
| 93 |
+
body {{
|
| 94 |
+
font-family: 'Segoe UI', sans-serif;
|
| 95 |
+
background-color: #f4f4f9;
|
| 96 |
+
display: flex;
|
| 97 |
+
flex-direction: column;
|
| 98 |
+
align_items: center;
|
| 99 |
+
justify-content: center;
|
| 100 |
+
height: 100vh;
|
| 101 |
+
margin: 0;
|
| 102 |
+
color: #333;
|
| 103 |
+
}}
|
| 104 |
+
h1 {{ margin-bottom: 20px; }}
|
| 105 |
+
p {{ color: #666; margin-bottom: 40px; }}
|
| 106 |
+
.info {{
|
| 107 |
+
background: white; padding: 20px; border-radius: 10px;
|
| 108 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 109 |
+
text-align: center;
|
| 110 |
+
}}
|
| 111 |
+
</style>
|
| 112 |
+
</head>
|
| 113 |
+
<body>
|
| 114 |
+
<div class="info">
|
| 115 |
+
<h1>📞 Teste meinen Sales-Bot</h1>
|
| 116 |
+
<p>Klicke unten rechts auf den Button, um das Gespräch zu starten.</p>
|
| 117 |
+
<small>⚠️ Mein Laptop muss an sein, damit es funktioniert.</small>
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
<script>
|
| 121 |
+
var vapiInstance = null;
|
| 122 |
+
const assistant = "{ASSISTANT_ID}";
|
| 123 |
+
const apiKey = "{VAPI_PUBLIC_KEY}";
|
| 124 |
+
|
| 125 |
+
// Konfiguration des Buttons
|
| 126 |
+
const buttonConfig = {{
|
| 127 |
+
position: "bottom-right",
|
| 128 |
+
offset: "40px",
|
| 129 |
+
width: "50px",
|
| 130 |
+
height: "50px",
|
| 131 |
+
idle: {{
|
| 132 |
+
color: "rgb(93, 254, 202)",
|
| 133 |
+
type: "pill",
|
| 134 |
+
title: "Hier klicken zum Sprechen",
|
| 135 |
+
subtitle: "Kostenloser Testanruf",
|
| 136 |
+
icon: "https://unpkg.com/lucide-static@0.321.0/icons/phone.svg",
|
| 137 |
+
}},
|
| 138 |
+
loading: {{
|
| 139 |
+
color: "rgb(93, 124, 202)",
|
| 140 |
+
type: "pill",
|
| 141 |
+
title: "Verbinde...",
|
| 142 |
+
subtitle: "Bitte warten",
|
| 143 |
+
icon: "https://unpkg.com/lucide-static@0.321.0/icons/loader-2.svg",
|
| 144 |
+
}},
|
| 145 |
+
active: {{
|
| 146 |
+
color: "rgb(255, 0, 0)",
|
| 147 |
+
type: "pill",
|
| 148 |
+
title: "Anruf läuft...",
|
| 149 |
+
subtitle: "Klicken zum Auflegen",
|
| 150 |
+
icon: "https://unpkg.com/lucide-static@0.321.0/icons/phone-off.svg",
|
| 151 |
+
}}
|
| 152 |
+
}};
|
| 153 |
+
|
| 154 |
+
(function() {{
|
| 155 |
+
vapiInstance = window.vapiSDK.run({{
|
| 156 |
+
apiKey: apiKey,
|
| 157 |
+
assistant: assistant,
|
| 158 |
+
config: buttonConfig
|
| 159 |
+
}});
|
| 160 |
+
}})();
|
| 161 |
+
</script>
|
| 162 |
+
</body>
|
| 163 |
+
</html>
|
| 164 |
+
"""
|
| 165 |
+
return html_content
|
| 166 |
+
|
| 167 |
+
if __name__ == "__main__":
|
| 168 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|