Spaces:
Paused
Paused
| from fastapi import FastAPI, Request | |
| import json | |
| import os | |
| import firebase_admin | |
| from firebase_admin import credentials, firestore | |
| from datetime import datetime | |
| app = FastAPI() | |
| # --- SETUP --- | |
| COLLECTION_KNOWLEDGE = "knowledge_base" | |
| COLLECTION_RULES = "availability_rules" | |
| KNOWLEDGE_CACHE = [] | |
| # --- FIREBASE VERBINDUNG --- | |
| db = None | |
| try: | |
| key = os.environ.get("FIREBASE_KEY") | |
| if key: | |
| cred = credentials.Certificate(json.loads(key)) | |
| if not firebase_admin._apps: | |
| firebase_admin.initialize_app(cred) | |
| db = firestore.client() | |
| print("✅ DB VERBUNDEN") | |
| else: | |
| print("❌ FEHLER: FIREBASE_KEY fehlt!") | |
| except Exception as e: | |
| print(f"❌ DB CRASH: {e}") | |
| # --- CACHE LADEN --- | |
| def reload_knowledge(): | |
| global KNOWLEDGE_CACHE | |
| if not db: return | |
| try: | |
| docs = db.collection(COLLECTION_KNOWLEDGE).stream() | |
| KNOWLEDGE_CACHE = [d.to_dict() for d in docs] | |
| print(f"📚 {len(KNOWLEDGE_CACHE)} Einträge geladen.") | |
| except Exception as e: | |
| print(f"❌ Cache Fehler: {e}") | |
| async def startup(): | |
| reload_knowledge() | |
| def get_stem(word): | |
| w = word.lower().strip() | |
| for end in ["ern", "en", "er", "es", "st", "te", "e", "s", "t"]: | |
| if w.endswith(end) and len(w) > len(end)+2: return w[:-len(end)] | |
| return w | |
| # --- HELPER: VAPI REQUEST PARSER --- | |
| # Holt sicher die ID und die Argumente aus dem Vapi-Salat | |
| def parse_vapi_request(data): | |
| tool_call_id = "unknown" | |
| args = {} | |
| try: | |
| msg = data.get("message", {}) | |
| # Variante A: Dein Log-Format ("toolCallList") | |
| if "toolCallList" in msg: | |
| call = msg["toolCallList"][0] | |
| tool_call_id = call["id"] | |
| if "function" in call and "arguments" in call["function"]: | |
| args = call["function"]["arguments"] | |
| # Variante B: Standard Vapi ("toolCalls") | |
| elif "toolCalls" in msg: | |
| call = msg["toolCalls"][0] | |
| tool_call_id = call["id"] | |
| if "function" in call and "arguments" in call["function"]: | |
| args = call["function"]["arguments"] | |
| # Argumente sind oft ein String, müssen zu JSON werden | |
| if isinstance(args, str): | |
| args = json.loads(args) | |
| except Exception as e: | |
| print(f"⚠️ Parsing Info: {e}") | |
| return tool_call_id, args | |
| # ========================================== | |
| # TOOL 1: VERFÜGBARKEIT (Strict Vapi Format) | |
| # ========================================== | |
| async def check_availability(request: Request): | |
| data = await request.json() | |
| print(f"🚦 TOOL REQUEST: {json.dumps(data)[:100]}...") # Debug Log | |
| # 1. ID extrahieren (WICHTIG!) | |
| tool_call_id, _ = parse_vapi_request(data) | |
| today = datetime.now().strftime("%Y-%m-%d") | |
| status = "available" | |
| instruction = "Normal arbeiten" | |
| # 2. Datenbank Check | |
| try: | |
| if db: | |
| rules = db.collection(COLLECTION_RULES).where("active", "==", True).stream() | |
| for r in rules: | |
| rd = r.to_dict() | |
| if rd.get('start_date') <= today <= rd.get('end_date'): | |
| print(f"🛑 REGEL AKTIV: {rd.get('name')}") | |
| status = "unavailable" | |
| instruction = rd.get('instruction_text') | |
| break | |
| except Exception as e: | |
| print(f"❌ ERROR CHECK: {e}") | |
| print(f"👉 RESULTAT: {status} | {instruction}") | |
| # 3. Antwort im strikten Vapi-Format | |
| return { | |
| "results": [ | |
| { | |
| "toolCallId": tool_call_id, | |
| "result": { | |
| "status": status, | |
| "instruction": instruction | |
| } | |
| } | |
| ] | |
| } | |
| # ========================================== | |
| # TOOL 2: SUCHE (Jetzt auch mit ID Rückgabe!) | |
| # ========================================== | |
| async def search(request: Request): | |
| data = await request.json() | |
| # 1. ID und Query extrahieren | |
| tool_call_id, args = parse_vapi_request(data) | |
| query = args.get("search_query") or args.get("query") or data.get("search_query") | |
| print(f"🔎 FRAGE (ID: {tool_call_id}): '{query}'") | |
| answer_text = "Dazu habe ich keine Informationen." | |
| if query: | |
| # --- SUCHE LOGIK (Mit Udo/Capaneo Boost) --- | |
| STOP_WORDS = ["hallo", "guten", "tag", "moin", "bitte", "danke", "frage"] | |
| q_words = [get_stem(w) for w in query.lower().split() if len(w)>2] | |
| relevant_words = [w for w in q_words if w not in STOP_WORDS] | |
| if relevant_words: | |
| best_doc = None | |
| best_score = 0 | |
| for doc in KNOWLEDGE_CACHE: | |
| score = 0 | |
| title = doc.get("question", "").lower() | |
| content = doc.get("answer", "").lower() | |
| keywords = [k.lower() for k in doc.get("keywords", [])] | |
| for word in relevant_words: | |
| if word in title: score += 50 | |
| for k in keywords: | |
| if get_stem(k) == get_stem(word): score += 30 | |
| if word in content: score += 5 | |
| if score > best_score: | |
| best_score = score | |
| best_doc = doc | |
| if best_doc and best_score >= 20: | |
| print(f"🏆 TREFFER ({best_score}): {best_doc.get('question')}") | |
| answer_text = best_doc.get("answer") | |
| else: | |
| print(f"⚠️ Zu wenig Relevanz (Max: {best_score})") | |
| # 3. Antwort im strikten Vapi-Format | |
| # Bei 'search' erwartet das LLM meist einfach den Text im result | |
| return { | |
| "results": [ | |
| { | |
| "toolCallId": tool_call_id, | |
| "result": answer_text | |
| } | |
| ] | |
| } | |
| # ========================================== | |
| # 3. MÜLLSCHLUCKER | |
| # ========================================== | |
| async def dummy_incoming(request: Request): | |
| return {"status": "ok"} | |
| def home(): | |
| return {"status": "Online", "docs_loaded": len(KNOWLEDGE_CACHE)} |