martinbrahm commited on
Commit
a3b124a
·
verified ·
1 Parent(s): 0cfb1eb

Upload main.py

Browse files
Files changed (1) hide show
  1. main.py +124 -147
main.py CHANGED
@@ -7,12 +7,10 @@ from datetime import datetime
7
 
8
  app = FastAPI()
9
 
10
- # --- EINSTELLUNGEN ---
11
  COLLECTION_KNOWLEDGE = "knowledge_base"
12
- COLLECTION_INBOX = "inbox"
13
  COLLECTION_RULES = "availability_rules"
14
-
15
- # --- GLOBALE VARIABLE (RAM) ---
16
  KNOWLEDGE_CACHE = []
17
 
18
  # --- FIREBASE VERBINDUNG ---
@@ -20,197 +18,176 @@ db = None
20
  try:
21
  firebase_key_json = os.environ.get("FIREBASE_KEY")
22
  if firebase_key_json:
23
- cred_dict = json.loads(firebase_key_json)
24
- cred = credentials.Certificate(cred_dict)
25
  if not firebase_admin._apps:
26
  firebase_admin.initialize_app(cred)
27
  db = firestore.client()
28
- print(f"✅ SYSTEM: Verbunden mit Projekt '{db.project}'")
29
  else:
30
- print("⚠️ FEHLER: Kein FIREBASE_KEY gefunden.")
31
  except Exception as e:
32
- print(f"❌ FEHLER beim Start: {e}")
33
 
34
- # --- LADEN ---
35
  def reload_knowledge():
36
  global KNOWLEDGE_CACHE
37
- if not db: return 0
38
- print("🔄 Lade Wissensdatenbank...")
39
  try:
40
  docs = db.collection(COLLECTION_KNOWLEDGE).stream()
41
- new_cache = []
42
- for doc in docs:
43
- d = doc.to_dict()
44
- d["id"] = doc.id
45
- new_cache.append(d)
46
- KNOWLEDGE_CACHE = new_cache
47
- print(f"🚀 TURBO-MODE: {len(KNOWLEDGE_CACHE)} Dokumente im RAM!")
48
- return len(KNOWLEDGE_CACHE)
49
  except Exception as e:
50
- print(f"❌ Fehler Cache: {e}")
51
- return 0
52
 
53
  @app.on_event("startup")
54
- async def startup_event():
55
  reload_knowledge()
56
 
57
- # --- HELPER: DEUTSCHE WORTSTÄMME ---
58
  def get_stem(word):
59
  w = word.lower().strip()
60
- endings = ["ern", "em", "er", "en", "es", "st", "te", "e", "s", "t"]
61
- for end in endings:
62
- if w.endswith(end) and len(w) > (len(end) + 2):
63
- return w[:-len(end)]
64
  return w
65
 
66
  # ==========================================
67
- # 1. NEUER ENDPUNKT: VERFÜGBARKEIT (INJECTION)
68
  # ==========================================
 
69
  @app.post("/vapi-incoming")
70
- async def handle_vapi_incoming(request: Request):
71
- """
72
- Dieser Endpunkt wird von Vapi aufgerufen, BEVOR der Anruf startet (Server URL).
73
- Hier prüfen wir das Datum und injizieren die Regel.
74
- """
75
  try:
76
  data = await request.json()
77
- message_type = data.get("message", {}).get("type")
 
 
78
 
79
- # Nur reagieren, wenn Vapi nach Konfiguration fragt
80
- if message_type == "assistant-request":
81
- print("📞 Eingehender Anruf (Setup) - Prüfe Verfügbarkeit...")
82
-
83
- today_str = datetime.now().strftime("%Y-%m-%d")
84
- context_text = ""
85
 
 
86
  if db:
87
- # Hole alle aktiven Regeln
88
  rules = db.collection(COLLECTION_RULES).where("active", "==", True).stream()
89
  for r in rules:
90
  rd = r.to_dict()
91
- # Prüfen: Ist heute im Zeitraum?
92
- if rd.get('start_date') <= today_str <= rd.get('end_date'):
 
 
93
  print(f"✅ REGEL AKTIV: {rd.get('name')}")
94
- context_text = f"WICHTIGE VORGABE ZUR ERREICHBARKEIT: {rd.get('instruction_text')}"
95
- break # Erste passende Regel gewinnt
96
-
97
- if not context_text:
98
- print("ℹ️ Keine Sonderregel für heute.")
99
 
100
- # Antwort an Vapi: "Fülle die Variable {{seasonal_context}}!"
101
  return {
102
  "assistant": {
103
  "variableValues": {
104
- "seasonal_context": context_text
105
  }
106
  }
107
  }
108
-
109
- except Exception as e:
110
- print(f"❌ Fehler im Vapi-Incoming Handler: {e}")
111
-
112
- # Fallback / OK für andere Nachrichten
113
- return {"status": "ok"}
114
 
 
 
 
115
 
116
  # ==========================================
117
- # 2. OPTIMIERTE SUCHE (JETZT AUCH IM TEXT)
118
  # ==========================================
119
  @app.post("/search")
120
- async def search_knowledge(request: Request):
121
  try:
122
  data = await request.json()
123
- except:
124
- return {"result": "Fehler: Kein JSON."}
125
-
126
- # Query holen (egal ob direkt oder via Vapi Tool)
127
- query_text = ""
128
- if "query" in data: query_text = data["query"]
129
- elif "message" in data:
130
- try:
131
- # Vapi schickt Argumente oft als JSON-String
132
- args = data["message"]["toolCalls"][0]["function"]["arguments"]
133
- if isinstance(args, str):
134
- query_text = json.loads(args).get("search_query", "")
135
- else:
136
- query_text = args.get("search_query", "")
137
- except: pass
138
-
139
- # Fallback für dein altes Schema
140
- if not query_text and "args" in data: query_text = data["args"].get("query", "")
141
-
142
- print(f"🔎 FRAGE: '{query_text}'")
143
- if not query_text: return {"result": "Ich habe die Frage akustisch nicht verstanden."}
144
-
145
- best_doc = None
146
- best_score = 0
147
-
148
- query_words_raw = query_text.lower().replace("?", "").replace(".", "").split()
149
- query_stems = [get_stem(w) for w in query_words_raw if len(w) > 2]
150
-
151
- print(f" ⚙️ Suchstämme: {query_stems}")
152
-
153
- for entry in KNOWLEDGE_CACHE:
154
- score = 0
155
- t_answer = entry.get("answer", "")
156
- t_question = entry.get("question", "")
157
- t_keywords = entry.get("keywords", [])
158
 
159
- if not t_answer: continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
- # A) Keyword Treffer (Höchste Prio: 20 Pkt)
162
- if isinstance(t_keywords, list):
163
- for k in t_keywords:
164
- k_stem = get_stem(k)
165
- if k_stem in query_stems:
 
 
 
 
166
  score += 20
167
-
168
- # B) Frage/Titel Treffer (Mittlere Prio: 15 Pkt)
169
- if t_question:
170
- q_words = t_question.lower().split()
171
- for qw in q_words:
172
- if len(qw) < 3: continue
173
- if get_stem(qw) in query_stems:
174
- score += 15
175
-
176
- # C) NEU: Antwort-Text Treffer (Niedrige Prio: 5 Pkt pro Wort)
177
- # Damit findet er "Preis" auch, wenn es nur im Text steht!
178
- a_words = t_answer.lower().split()
179
- for aw in a_words:
180
- if len(aw) < 3: continue
181
- if get_stem(aw) in query_stems:
182
- score += 5
183
-
184
- # --- SCORING ENDE ---
185
-
186
- if score > best_score:
187
- best_score = score
188
- best_doc = entry
189
- if score > 15:
190
- print(f" Candidate: {score} Pts -> {t_question}")
191
-
192
- # --- ERGEBNIS ---
193
- CONFIDENCE_THRESHOLD = 25 # Etwas gesenkt, damit er toleranter ist
194
-
195
- if best_doc and best_score >= CONFIDENCE_THRESHOLD:
196
- print(f"🏆 GEWINNER: {best_score} Pkt | {best_doc.get('question')}")
197
- return {"result": best_doc['answer']}
198
-
199
- # Fallback: Low Confidence Loggen
200
- else:
201
- print(f"⚠️ LOW CONFIDENCE ({best_score}).")
202
- if db and best_doc: # Wenn er zumindest irgendwas gefunden hat (z.B. Score 10)
203
- try:
204
- db.collection(COLLECTION_INBOX).add({
205
- "question": query_text,
206
- "status": "review_needed",
207
- "found_answer": best_doc['answer'],
208
- "score": best_score,
209
- "timestamp": firestore.SERVER_TIMESTAMP
210
- })
211
- except: pass
212
-
213
- return {"result": "Dazu habe ich leider keine genauen Informationen in meiner Datenbank."}
214
 
215
  @app.get("/")
216
  def home():
 
7
 
8
  app = FastAPI()
9
 
10
+ # --- KONFIGURATION ---
11
  COLLECTION_KNOWLEDGE = "knowledge_base"
 
12
  COLLECTION_RULES = "availability_rules"
13
+ COLLECTION_INBOX = "inbox"
 
14
  KNOWLEDGE_CACHE = []
15
 
16
  # --- FIREBASE VERBINDUNG ---
 
18
  try:
19
  firebase_key_json = os.environ.get("FIREBASE_KEY")
20
  if firebase_key_json:
21
+ cred = credentials.Certificate(json.loads(firebase_key_json))
 
22
  if not firebase_admin._apps:
23
  firebase_admin.initialize_app(cred)
24
  db = firestore.client()
25
+ print(f"✅ DB VERBUNDEN: {db.project}")
26
  else:
27
+ print("⚠️ KEIN FIREBASE KEY GEFUNDEN!")
28
  except Exception as e:
29
+ print(f"❌ DB FEHLER: {str(e)}")
30
 
31
+ # --- CACHE LADEN ---
32
  def reload_knowledge():
33
  global KNOWLEDGE_CACHE
34
+ if not db: return
 
35
  try:
36
  docs = db.collection(COLLECTION_KNOWLEDGE).stream()
37
+ KNOWLEDGE_CACHE = [d.to_dict() for d in docs]
38
+ print(f"📚 {len(KNOWLEDGE_CACHE)} Wissens-Einträge geladen.")
 
 
 
 
 
 
39
  except Exception as e:
40
+ print(f"❌ Cache Fehler: {e}")
 
41
 
42
  @app.on_event("startup")
43
+ async def startup():
44
  reload_knowledge()
45
 
46
+ # --- HELPER: TEXT NORMALISIEREN (Stemmer) ---
47
  def get_stem(word):
48
  w = word.lower().strip()
49
+ # Schneidet einfache deutsche Endungen ab für bessere Treffer
50
+ for end in ["ern", "en", "er", "es", "st", "te", "e", "s", "t"]:
51
+ if w.endswith(end) and len(w) > len(end)+2: return w[:-len(end)]
 
52
  return w
53
 
54
  # ==========================================
55
+ # ENDPUNKT 1: VERFÜGBARKEIT (Server URL)
56
  # ==========================================
57
+ # Vapi ruft das VOR dem Anruf auf, um Kontext zu laden.
58
  @app.post("/vapi-incoming")
59
+ async def handle_incoming(request: Request):
 
 
 
 
60
  try:
61
  data = await request.json()
62
+ msg_type = data.get("message", {}).get("type")
63
+
64
+ print(f"📞 INCOMING REQUEST: {msg_type}")
65
 
66
+ if msg_type == "assistant-request":
67
+ today = datetime.now().strftime("%Y-%m-%d")
68
+ print(f"🗓️ Prüfe Regeln für heute: {today}")
 
 
 
69
 
70
+ context = ""
71
  if db:
 
72
  rules = db.collection(COLLECTION_RULES).where("active", "==", True).stream()
73
  for r in rules:
74
  rd = r.to_dict()
75
+ start = rd.get('start_date', '0000-00-00')
76
+ end = rd.get('end_date', '9999-99-99')
77
+
78
+ if start <= today <= end:
79
  print(f"✅ REGEL AKTIV: {rd.get('name')}")
80
+ context = f"WICHTIGE VERFÜGBARKEITS-INFO: {rd.get('instruction_text')}"
81
+ break # Erste Regel gewinnt
82
+
83
+ if not context:
84
+ print("ℹ️ Keine Sonderregeln für heute.")
85
 
 
86
  return {
87
  "assistant": {
88
  "variableValues": {
89
+ "seasonal_context": context
90
  }
91
  }
92
  }
93
+
94
+ # Für alle anderen Events (status-update etc.) einfach OK senden
95
+ return {"status": "ok"}
 
 
 
96
 
97
+ except Exception as e:
98
+ print(f"❌ ERROR INCOMING: {str(e)}")
99
+ return {"status": "error"}
100
 
101
  # ==========================================
102
+ # ENDPUNKT 2: SUCHE / LOOKUP (Tool URL)
103
  # ==========================================
104
  @app.post("/search")
105
+ async def search(request: Request):
106
  try:
107
  data = await request.json()
108
+ # Debugging: Wir drucken das Paket, falls was schief geht
109
+ # print(f"📦 RAW TOOL DATA: {json.dumps(data)[:200]}...")
110
+
111
+ query = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ # --- ROBUSTE EXTRAKTION ---
114
+ # 1. Fall: Vapi schickt Argumente direkt (selten)
115
+ if "search_query" in data:
116
+ query = data["search_query"]
117
+
118
+ # 2. Fall: Vapi schickt es im "message" -> "toolCalls" Block (Standard)
119
+ elif "message" in data and "toolCalls" in data["message"]:
120
+ try:
121
+ args = data["message"]["toolCalls"][0]["function"]["arguments"]
122
+ # Vapi sendet Arguments oft als JSON-String, nicht als Objekt!
123
+ if isinstance(args, str):
124
+ args = json.loads(args)
125
+
126
+ query = args.get("search_query") or args.get("query") or args.get("q")
127
+ except Exception as parsing_err:
128
+ print(f"⚠️ Argument Parsing Fehler: {parsing_err}")
129
+
130
+ print(f"🔎 GEPARSTE FRAGE: '{query}'")
131
+
132
+ if not query:
133
+ return {"result": "Fehler: Ich habe die Frage technisch nicht verstanden (leeres Argument)."}
134
+
135
+ # --- SUCHE STARTEN ---
136
+ best_doc = None
137
+ best_score = 0
138
+
139
+ # Suchbegriffe vorbereiten
140
+ q_clean = query.lower().replace("?", "").replace(".", "")
141
+ q_stems = [get_stem(w) for w in q_clean.split() if len(w)>2]
142
+
143
+ print(f" ⚙️ Such-Stämme: {q_stems}")
144
 
145
+ for doc in KNOWLEDGE_CACHE:
146
+ score = 0
147
+ # Wir bauen einen großen Text-Blob aus Frage, Antwort und Keywords
148
+ txt_content = (doc.get("question", "") + " " + doc.get("answer", "")).lower()
149
+ keywords = [k.lower() for k in doc.get("keywords", [])]
150
+
151
+ # A) Keywords prüfen (Hohe Punkte: 20)
152
+ for k in keywords:
153
+ if any(get_stem(k) in s for s in q_stems):
154
  score += 20
155
+
156
+ # B) Textinhalt prüfen (Mittlere Punkte: 5 pro Treffer)
157
+ for stem in q_stems:
158
+ if stem in txt_content:
159
+ score += 5
160
+
161
+ # C) Exakte Phrase im Titel (Bonus: 10)
162
+ if query.lower() in doc.get("question", "").lower():
163
+ score += 10
164
+
165
+ if score > best_score:
166
+ best_score = score
167
+ best_doc = doc
168
+
169
+ # --- ERGEBNIS ---
170
+ if best_doc and best_score >= 10:
171
+ print(f"🏆 TREFFER ({best_score} Pkt): {best_doc.get('question')}")
172
+ return {"result": best_doc.get("answer")}
173
+ else:
174
+ print(f"⚠️ KEIN TREFFER (Max Score: {best_score})")
175
+ # In Inbox speichern (optional) zur Verbesserung
176
+ if db and best_score > 0:
177
+ try:
178
+ db.collection(COLLECTION_INBOX).add({
179
+ "question": query,
180
+ "status": "review_needed",
181
+ "score": best_score,
182
+ "timestamp": firestore.SERVER_TIMESTAMP
183
+ })
184
+ except: pass
185
+
186
+ return {"result": "Dazu habe ich leider keine Informationen in meiner Datenbank."}
187
+
188
+ except Exception as e:
189
+ print(f" SEARCH ERROR: {str(e)}")
190
+ return {"result": "Systemfehler bei der Suche."}
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  @app.get("/")
193
  def home():