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

Upload main.py

Browse files
Files changed (1) hide show
  1. main.py +107 -73
main.py CHANGED
@@ -10,6 +10,7 @@ app = FastAPI()
10
  # --- EINSTELLUNGEN ---
11
  COLLECTION_KNOWLEDGE = "knowledge_base"
12
  COLLECTION_INBOX = "inbox"
 
13
 
14
  # --- GLOBALE VARIABLE (RAM) ---
15
  KNOWLEDGE_CACHE = []
@@ -53,33 +54,68 @@ def reload_knowledge():
53
  async def startup_event():
54
  reload_knowledge()
55
 
56
- # --- ENDPUNKTE ---
57
- @app.get("/")
58
- def home():
59
- return {"status": "Udo Agent API (Stemming Mode) ist bereit.", "docs": len(KNOWLEDGE_CACHE)}
60
-
61
- @app.get("/refresh_knowledge")
62
- def refresh_endpoint():
63
- count = reload_knowledge()
64
- return {"status": "Cache aktualisiert", "docs_loaded": count}
65
-
66
- # --- 🧠 HELPER: DEUTSCHE WORTSTÄMME ---
67
  def get_stem(word):
68
- """
69
- Ein sehr einfacher 'Stemmer' für Deutsch.
70
- Schneidet Endungen wie 'en', 'ern', 'te', 's' ab.
71
- Macht aus 'Preise' -> 'preis', 'kostet' -> 'kost'.
72
- """
73
  w = word.lower().strip()
74
- # Reihenfolge wichtig! Längere Endungen zuerst.
75
  endings = ["ern", "em", "er", "en", "es", "st", "te", "e", "s", "t"]
76
-
77
  for end in endings:
78
- if w.endswith(end) and len(w) > (len(end) + 2): # Nicht zu viel abschneiden
79
  return w[:-len(end)]
80
  return w
81
 
82
- # --- 🧠 DIE NEUE INTELLIGENTE SUCHE ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  @app.post("/search")
84
  async def search_knowledge(request: Request):
85
  try:
@@ -87,102 +123,100 @@ async def search_knowledge(request: Request):
87
  except:
88
  return {"result": "Fehler: Kein JSON."}
89
 
90
- # Frage extrahieren
91
  query_text = ""
92
  if "query" in data: query_text = data["query"]
93
  elif "message" in data:
94
  try:
 
95
  args = data["message"]["toolCalls"][0]["function"]["arguments"]
96
- query_text = json.loads(args).get("query", "") if isinstance(args, str) else args.get("query", "")
 
 
 
97
  except: pass
 
 
98
  if not query_text and "args" in data: query_text = data["args"].get("query", "")
99
 
100
  print(f"🔎 FRAGE: '{query_text}'")
101
-
102
- if not query_text: return {"result": "Akustik-Fehler."}
103
 
104
- # --- SCORING ALGORITHMUS V3 (Mit "Low Confidence" Inbox) ---
105
  best_doc = None
106
  best_score = 0
107
 
108
- # 1. Query vorbereiten
109
  query_words_raw = query_text.lower().replace("?", "").replace(".", "").split()
110
  query_stems = [get_stem(w) for w in query_words_raw if len(w) > 2]
111
 
112
  print(f" ⚙️ Suchst��mme: {query_stems}")
113
 
114
- # Wir prüfen JEDES Dokument
115
  for entry in KNOWLEDGE_CACHE:
116
  score = 0
117
- doc_id = entry.get('id', 'unknown')
118
  t_answer = entry.get("answer", "")
119
  t_question = entry.get("question", "")
120
  t_keywords = entry.get("keywords", [])
121
 
122
- if not t_answer or len(t_answer) < 5: continue
123
 
124
- # A) Keyword Treffer
125
  if isinstance(t_keywords, list):
126
  for k in t_keywords:
127
  k_stem = get_stem(k)
128
  if k_stem in query_stems:
129
- if k_stem in ['udo', 'capaneo']: score += 2
130
- else: score += 20
131
 
132
- # B) Frage/Titel Treffer
133
  if t_question:
134
- q_words = t_question.lower().replace("?", "").split()
135
  for qw in q_words:
136
  if len(qw) < 3: continue
137
- qw_stem = get_stem(qw)
138
- if qw_stem in query_stems:
139
  score += 15
140
 
 
 
 
 
 
 
 
 
 
 
141
  if score > best_score:
142
  best_score = score
143
  best_doc = entry
144
- # DEBUG: Zeig uns Kandidaten im Log
145
- if score > 10:
146
- print(f" Candidate {doc_id}: {score} Pts (Titel: {t_question[:30]}...)")
147
 
148
- # --- ERGEBNIS LOGIK ---
149
-
150
- # Schwelle für "Ich bin mir sicher"
151
- CONFIDENCE_THRESHOLD = 70
152
-
153
- if best_doc and best_score >= 10:
154
- print(f"🏆 GEWINNER: Doc {best_doc['id']} mit {best_score} Punkten.")
155
-
156
- # DEBUG: Zeig uns die ersten 100 Zeichen der Antwort im Log!
157
- preview = best_doc['answer'][:100].replace("\n", " ")
158
- print(f"📤 SENDE AN VAPI: '{preview}...'")
159
-
160
- # LOGIK: Wenn der Score "naja" ist (zwischen 10 und 70), ab in die Inbox zur Prüfung!
161
- if best_score < CONFIDENCE_THRESHOLD:
162
- print(f"⚠️ LOW CONFIDENCE ({best_score}). Schreibe Backup in Inbox...")
163
- if db:
164
- try:
165
- db.collection(COLLECTION_INBOX).add({
166
- "question": query_text,
167
- "status": "review_needed", # Markierung für dich
168
- "found_answer": best_doc['answer'], # Was hat er gefunden?
169
- "score": best_score,
170
- "timestamp": firestore.SERVER_TIMESTAMP,
171
- "source": "AI Call (Low Confidence)"
172
- })
173
- except: pass
174
 
 
 
175
  return {"result": best_doc['answer']}
176
 
 
177
  else:
178
- print(f"⚠️ KEIN TREFFER (Best Score: {best_score}).")
179
- if db:
180
- try:
181
  db.collection(COLLECTION_INBOX).add({
182
  "question": query_text,
183
- "status": "open",
184
- "timestamp": firestore.SERVER_TIMESTAMP,
185
- "source": "AI Call (No Hit)"
 
186
  })
187
- except: pass
188
- return {"result": "Dazu habe ich leider keine Informationen in meiner Datenbank."}
 
 
 
 
 
 
 
 
 
 
 
10
  # --- EINSTELLUNGEN ---
11
  COLLECTION_KNOWLEDGE = "knowledge_base"
12
  COLLECTION_INBOX = "inbox"
13
+ COLLECTION_RULES = "availability_rules"
14
 
15
  # --- GLOBALE VARIABLE (RAM) ---
16
  KNOWLEDGE_CACHE = []
 
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:
 
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():
217
+ return {"status": "Udo API Online", "docs": len(KNOWLEDGE_CACHE)}
218
+
219
+ @app.get("/refresh_knowledge")
220
+ def refresh_endpoint():
221
+ count = reload_knowledge()
222
+ return {"status": "Cache aktualisiert", "docs_loaded": count}