File size: 6,435 Bytes
7606f4c
1b5d2ca
eeb5b5c
 
 
45dc038
6e26e1d
eeb5b5c
 
3d18df1
45dc038
0cfb1eb
db10d88
45dc038
0459685
45dc038
c106ebb
bb1daa0
 
 
eeb5b5c
 
 
bb1daa0
eeb5b5c
bb1daa0
eeb5b5c
bb1daa0
6e26e1d
a3b124a
db10d88
 
3d18df1
db10d88
 
a3b124a
bb1daa0
db10d88
a3b124a
db10d88
 
a3b124a
db10d88
 
3e87852
 
a3b124a
 
3e87852
 
93ddfa4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0cfb1eb
93ddfa4
0cfb1eb
701465f
 
93ddfa4
 
 
 
 
 
701465f
 
93ddfa4
701465f
93ddfa4
0cfb1eb
701465f
 
 
 
 
3d18df1
701465f
93ddfa4
701465f
a3b124a
701465f
 
93ddfa4
 
 
701465f
93ddfa4
 
 
 
 
 
 
 
 
701465f
0cfb1eb
 
93ddfa4
0cfb1eb
1b5d2ca
a3b124a
93ddfa4
 
 
 
 
 
 
 
 
 
 
 
 
bb1daa0
 
3d18df1
93ddfa4
 
 
 
 
 
 
 
3d18df1
93ddfa4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212e616
 
3d18df1
212e616
 
 
3d18df1
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
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}")

@app.on_event("startup")
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)
# ==========================================
@app.post("/check_availability")
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!)
# ==========================================
@app.post("/search")
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
# ==========================================
@app.post("/vapi-incoming")
async def dummy_incoming(request: Request):
    return {"status": "ok"}

@app.get("/")
def home():
    return {"status": "Online", "docs_loaded": len(KNOWLEDGE_CACHE)}