martinbrahm commited on
Commit
7606f4c
·
verified ·
1 Parent(s): 43240a0

Upload main.py

Browse files
Files changed (1) hide show
  1. main.py +158 -152
main.py CHANGED
@@ -1,162 +1,168 @@
1
- from fastapi import FastAPI, HTTPException, Request
2
- from pydantic import BaseModel
3
- import firebase_admin
4
- from firebase_admin import credentials, firestore
5
- import google.generativeai as genai
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
- # --- DATENMODELLE ---
34
- class SearchRequest(BaseModel):
35
- query: str # Das, was der User gefragt hat
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
- payload = await request.json()
 
 
91
 
92
- # Vapi sendet das Transkript oft tief verschachtelt
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
- response = model.generate_content(prompt)
132
- clean_json = response.text.replace("```json", "").replace("```", "").strip()
133
-
134
- if not clean_json:
135
- return {"status": "no_data_generated"}
136
 
137
- new_knowledge = json.loads(clean_json)
138
-
139
- # Speichern in Firebase (als PENDING zur Prüfung)
140
- batch = db.batch()
141
- count = 0
142
- for entry in new_knowledge:
143
- doc_ref = db.collection("knowledge_base").document()
144
- doc_data = {
145
- "question": entry.get("question"),
146
- "answer": entry.get("answer"),
147
- "keywords": entry.get("keywords", []),
148
- "category": "Auto-Learning",
149
- "status": "pending", # WICHTIG: Muss erst von dir genehmigt werden!
150
- "source": "Call Analysis",
151
- "created_at": firestore.SERVER_TIMESTAMP
152
- }
153
- batch.set(doc_ref, doc_data)
154
- count += 1
 
 
155
 
156
- batch.commit()
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" Fehler beim Lernen: {e}")
162
- return {"status": "error", "details": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)