Ogghey commited on
Commit
3ea2bf9
·
verified ·
1 Parent(s): 9d56b27

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +211 -142
app.py CHANGED
@@ -1,196 +1,265 @@
1
  import os
2
  import shutil
3
- from fastapi import FastAPI, Request
4
  from fastapi.responses import JSONResponse
 
5
  from sentence_transformers import SentenceTransformer, util
6
  import torch
7
  import requests
 
 
8
 
9
- # Rate limit
10
  from slowapi import Limiter, _rate_limit_exceeded_handler
11
  from slowapi.util import get_remote_address
12
  from slowapi.errors import RateLimitExceeded
13
 
14
- # Inisialisasi FastAPI dan Limiter
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  limiter = Limiter(key_func=get_remote_address)
16
- app = FastAPI()
17
  app.state.limiter = limiter
18
  app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
19
 
20
- # 🔐 Paksa cache aman untuk Hugging Face Spaces
21
- HF_CACHE = "/tmp/hf"
22
- os.environ["TRANSFORMERS_CACHE"] = HF_CACHE
23
- os.environ["HF_HOME"] = HF_CACHE
24
- os.makedirs(HF_CACHE, exist_ok=True)
25
-
26
- # Bersihkan cache model jika terkunci
27
- if os.path.exists(f"{HF_CACHE}/models--sentence-transformers--paraphrase-MiniLM-L3-v2.lock"):
28
- os.remove(f"{HF_CACHE}/models--sentence-transformers--paraphrase-MiniLM-L3-v2.lock")
29
- if os.path.exists(f"{HF_CACHE}/models--sentence-transformers--paraphrase-MiniLM-L3-v2"):
30
- shutil.rmtree(f"{HF_CACHE}/models--sentence-transformers--paraphrase-MiniLM-L3-v2", ignore_errors=True)
31
-
32
- # Supabase
33
- SUPABASE_URL = "https://olbjfxlclotxtnpjvpfj.supabase.co"
34
- SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9sYmpmeGxjbG90eHRucGp2cGZqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIyMzYwMDEsImV4cCI6MjA2NzgxMjAwMX0.7q_o5DCFEAAysnWXMChH4MI5qNhIVc4OgpT5JvgYxc0"
35
-
36
- # Model
37
- model = SentenceTransformer("sentence-transformers/paraphrase-MiniLM-L3-v2")
38
-
39
- # 🔍 Ambil FAQ berdasarkan UID
40
- def get_faq_from_supabase(admin_id):
41
- """Ambil FAQ khusus untuk admin tertentu"""
42
- url = f"{SUPABASE_URL}/rest/v1/faq_items?admin_id=eq.{admin_id}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  headers = {
44
- "apikey": SUPABASE_KEY,
45
- "Authorization": f"Bearer {SUPABASE_KEY}",
46
  "Content-Type": "application/json"
47
  }
 
48
  try:
49
- r = requests.get(url, headers=headers)
50
- r.raise_for_status()
51
- return r.json()
52
- except Exception as e:
53
- print(f"❌ Error mengambil FAQ untuk admin {admin_id}:", e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  return []
55
 
56
- # 🔮 Endpoint prediksi jawaban dari pertanyaan user
57
  @app.post("/predict")
58
- @limiter.limit("5/minute")
59
- async def predict(request: Request):
60
- body = await request.json()
61
- admin_id, question = body.get("data", [None, None]) # admin_id dari UID di link
62
-
 
 
 
 
 
63
  if not admin_id or not question:
64
- return {"data": ["Admin ID atau pertanyaan tidak valid."]}
 
 
 
65
 
66
- # Ambil FAQ khusus admin ini saja
67
  faqs = get_faq_from_supabase(admin_id)
68
  if not faqs:
69
- return {"data": ["Maaf, belum ada FAQ yang tersedia."]}
70
 
71
- # Proses pencarian jawaban
72
  questions = [f["question"] for f in faqs]
73
  answers = [f["answer"] for f in faqs]
74
 
75
  try:
76
- embeddings = model.encode(questions, convert_to_tensor=True)
 
77
  query_embedding = model.encode(question, convert_to_tensor=True)
78
- similarity = util.pytorch_cos_sim(query_embedding, embeddings)
 
 
79
  best_idx = torch.argmax(similarity).item()
 
80
 
81
- # Threshold similarity (minimal 0.5)
82
- if similarity[0][best_idx] < 0.5:
83
- return {"data": ["Maaf, saya tidak mengerti pertanyaan Anda."]}
84
 
85
  return {"data": [answers[best_idx]]}
86
  except Exception as e:
87
- print("❌ Error processing question:", e)
88
- return {"data": ["Terjadi kesalahan saat memproses pertanyaan."]}
 
 
89
 
90
- # Save chat
91
  @app.post("/save_chat")
92
- async def save_chat(request: Request):
93
- body = await request.json()
94
- admin_id = body.get("admin_id")
95
- session_id = body.get("session_id")
96
- is_bot = body.get("is_bot", False)
97
- is_admin = body.get("is_admin", False)
98
- message = body.get("message")
99
-
100
- if not all([admin_id, session_id, message]):
101
- return JSONResponse({"error": "Data tidak lengkap"}, status_code=400)
102
-
103
- url = f"{SUPABASE_URL}/rest/v1/chat_logs"
104
- headers = {
105
- "apikey": SUPABASE_KEY,
106
- "Authorization": f"Bearer {SUPABASE_KEY}",
107
- "Content-Type": "application/json",
108
- "Prefer": "return=representation"
109
- }
110
- payload = {
111
- "admin_id": admin_id,
112
- "session_id": session_id,
113
- "is_bot": is_bot,
114
- "is_admin": is_admin,
115
- "message": message
116
- }
117
-
118
  try:
119
- r = requests.post(url, json=payload, headers=headers)
120
- r.raise_for_status()
121
- return {"message": "Pesan berhasil disimpan", "id": r.json()[0]["id"]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  except Exception as e:
123
- print("❌ Gagal menyimpan pesan:", e)
124
- return JSONResponse({"error": "Gagal menyimpan pesan"}, status_code=500)
 
 
125
 
126
- # Chat history
127
  @app.get("/chat_history")
128
- async def get_chat_history(request: Request, admin_id: str, session_id: str):
129
- """Ambil history chat spesifik untuk sesi ini"""
130
- url = f"{SUPABASE_URL}/rest/v1/chat_logs"
131
- params = {
132
- "admin_id": f"eq.{admin_id}",
133
- "or": f"(session_id.eq.{session_id},is_bot.eq.true)",
134
- "order": "created_at.asc"
135
- }
136
- headers = {
137
- "apikey": SUPABASE_KEY,
138
- "Authorization": f"Bearer {SUPABASE_KEY}"
139
- }
140
-
141
  try:
142
- r = requests.get(url, params=params, headers=headers)
143
- r.raise_for_status()
144
- return r.json()
 
 
 
 
 
 
 
 
 
145
  except Exception as e:
146
- print(f"❌ Error mengambil history untuk {session_id}:", e)
147
- return JSONResponse({"error": "Gagal mengambil history"}, status_code=500)
148
-
149
- # 🧹 Hapus satu pesan berdasarkan ID
150
- @app.post("/delete_chat")
151
- async def delete_chat(request: Request):
152
- body = await request.json()
153
- message_id = body.get("id")
154
-
155
- if not message_id:
156
- return JSONResponse({"error": "ID pesan wajib diisi."}, status_code=400)
157
 
158
- url = f"{SUPABASE_URL}/rest/v1/chat_logs?id=eq.{message_id}"
159
- headers = {
160
- "apikey": SUPABASE_KEY,
161
- "Authorization": f"Bearer {SUPABASE_KEY}",
162
- "Content-Type": "application/json",
163
- "Prefer": "return=representation"
164
- }
 
165
 
166
  try:
167
- r = requests.delete(url, headers=headers)
168
- r.raise_for_status()
169
- return {"message": f"Pesan dengan ID {message_id} berhasil dihapus."}
 
 
 
 
170
  except Exception as e:
171
- print("❌ Gagal hapus pesan:", e)
172
- return JSONResponse({"error": "Gagal menghapus pesan."}, status_code=500)
 
 
173
 
174
- # 🧼 Hapus semua pesan milik user tertentu
175
  @app.post("/delete_all_by_uid")
176
- async def delete_all_by_uid(request: Request):
177
- body = await request.json()
178
- uid = body.get("uid")
179
-
180
- if not uid:
181
- return JSONResponse({"error": "UID wajib diisi."}, status_code=400)
182
-
183
- url = f"{SUPABASE_URL}/rest/v1/chat_logs?uid=eq.{uid}"
184
- headers = {
185
- "apikey": SUPABASE_KEY,
186
- "Authorization": f"Bearer {SUPABASE_KEY}",
187
- "Content-Type": "application/json",
188
- "Prefer": "return=representation"
189
- }
190
 
191
  try:
192
- r = requests.delete(url, headers=headers)
193
- r.raise_for_status()
194
- return {"message": f"Semua pesan dengan UID {uid} berhasil dihapus."}
 
 
 
 
195
  except Exception as e:
196
- return JSONResponse({"error": str(e)}, status_code=500)
 
 
 
 
 
 
 
 
 
1
  import os
2
  import shutil
3
+ from fastapi import FastAPI, Request, HTTPException, Depends
4
  from fastapi.responses import JSONResponse
5
+ from fastapi.middleware.cors import CORSMiddleware
6
  from sentence_transformers import SentenceTransformer, util
7
  import torch
8
  import requests
9
+ from typing import List, Dict, Optional
10
+ from pydantic import BaseModel
11
 
12
+ # Rate limiting
13
  from slowapi import Limiter, _rate_limit_exceeded_handler
14
  from slowapi.util import get_remote_address
15
  from slowapi.errors import RateLimitExceeded
16
 
17
+ # Configuration
18
+ class Config:
19
+ SUPABASE_URL = "https://olbjfxlclotxtnpjvpfj.supabase.co"
20
+ SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9sYmpmeGxjbG90eHRucGp2cGZqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIyMzYwMDEsImV4cCI6MjA2NzgxMjAwMX0.7q_o5DCFEAAysnWXMChH4MI5qNhIVc4OgpT5JvgYxc0"
21
+ MODEL_NAME = "sentence-transformers/paraphrase-MiniLM-L3-v2"
22
+ SIMILARITY_THRESHOLD = 0.7
23
+ HF_CACHE = "/tmp/hf"
24
+ RATE_LIMIT = "10/minute"
25
+
26
+ # Initialize FastAPI
27
+ app = FastAPI(title="Biruu Chatbot API", version="1.0.0")
28
+
29
+ # CORS Middleware
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=["*"],
33
+ allow_methods=["*"],
34
+ allow_headers=["*"],
35
+ )
36
+
37
+ # Rate Limiter
38
  limiter = Limiter(key_func=get_remote_address)
 
39
  app.state.limiter = limiter
40
  app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
41
 
42
+ # Setup Hugging Face cache
43
+ os.makedirs(Config.HF_CACHE, exist_ok=True)
44
+ os.environ["TRANSFORMERS_CACHE"] = Config.HF_CACHE
45
+ os.environ["HF_HOME"] = Config.HF_CACHE
46
+
47
+ # Clean up locked cache
48
+ lock_file = f"{Config.HF_CACHE}/models--{Config.MODEL_NAME.replace('/', '--')}.lock"
49
+ if os.path.exists(lock_file):
50
+ os.remove(lock_file)
51
+ model_cache = f"{Config.HF_CACHE}/models--{Config.MODEL_NAME.replace('/', '--')}"
52
+ if os.path.exists(model_cache):
53
+ shutil.rmtree(model_cache, ignore_errors=True)
54
+
55
+ # Initialize model
56
+ try:
57
+ model = SentenceTransformer(Config.MODEL_NAME)
58
+ except Exception as e:
59
+ raise RuntimeError(f"Failed to load model: {str(e)}")
60
+
61
+ # Pydantic Models
62
+ class ChatMessage(BaseModel):
63
+ admin_id: str
64
+ session_id: str
65
+ is_bot: bool
66
+ is_admin: bool
67
+ message: str
68
+
69
+ class DeleteRequest(BaseModel):
70
+ id: Optional[str] = None
71
+ uid: Optional[str] = None
72
+
73
+ # Helper Functions
74
+ def make_supabase_request(
75
+ method: str,
76
+ endpoint: str,
77
+ params: Optional[Dict] = None,
78
+ data: Optional[Dict] = None
79
+ ) -> requests.Response:
80
+ """Generic function to make Supabase API requests"""
81
+ url = f"{Config.SUPABASE_URL}{endpoint}"
82
  headers = {
83
+ "apikey": Config.SUPABASE_KEY,
84
+ "Authorization": f"Bearer {Config.SUPABASE_KEY}",
85
  "Content-Type": "application/json"
86
  }
87
+
88
  try:
89
+ if method == "GET":
90
+ response = requests.get(url, headers=headers, params=params)
91
+ elif method == "POST":
92
+ response = requests.post(url, headers=headers, json=data)
93
+ elif method == "DELETE":
94
+ response = requests.delete(url, headers=headers)
95
+ else:
96
+ raise ValueError("Unsupported HTTP method")
97
+
98
+ response.raise_for_status()
99
+ return response
100
+ except requests.exceptions.RequestException as e:
101
+ raise HTTPException(
102
+ status_code=500,
103
+ detail=f"Supabase request failed: {str(e)}"
104
+ )
105
+
106
+ def get_faq_from_supabase(admin_id: str) -> List[Dict]:
107
+ """Get FAQ items for a specific admin"""
108
+ try:
109
+ response = make_supabase_request(
110
+ "GET",
111
+ "/rest/v1/faq_items",
112
+ params={"admin_id": f"eq.{admin_id}"}
113
+ )
114
+ return response.json()
115
+ except HTTPException:
116
  return []
117
 
118
+ # API Endpoints
119
  @app.post("/predict")
120
+ @limiter.limit(Config.RATE_LIMIT)
121
+ async def predict(request: Request, data: List[str] = ["", ""]):
122
+ """Get answer for user question"""
123
+ if len(data) != 2:
124
+ raise HTTPException(
125
+ status_code=400,
126
+ detail="Invalid input format. Expected [admin_id, question]"
127
+ )
128
+
129
+ admin_id, question = data
130
  if not admin_id or not question:
131
+ raise HTTPException(
132
+ status_code=400,
133
+ detail="Admin ID and question are required"
134
+ )
135
 
136
+ # Get FAQs for this admin
137
  faqs = get_faq_from_supabase(admin_id)
138
  if not faqs:
139
+ return {"data": ["Maaf, belum ada FAQ yang tersedia untuk chatbot ini."]}
140
 
141
+ # Process question
142
  questions = [f["question"] for f in faqs]
143
  answers = [f["answer"] for f in faqs]
144
 
145
  try:
146
+ # Encode questions and user input
147
+ question_embeddings = model.encode(questions, convert_to_tensor=True)
148
  query_embedding = model.encode(question, convert_to_tensor=True)
149
+
150
+ # Calculate similarity
151
+ similarity = util.pytorch_cos_sim(query_embedding, question_embeddings)
152
  best_idx = torch.argmax(similarity).item()
153
+ best_score = similarity[0][best_idx].item()
154
 
155
+ # Apply threshold
156
+ if best_score < Config.SIMILARITY_THRESHOLD:
157
+ return {"data": ["Maaf, saya tidak mengerti pertanyaan Anda. Coba tanyakan dengan cara lain."]}
158
 
159
  return {"data": [answers[best_idx]]}
160
  except Exception as e:
161
+ raise HTTPException(
162
+ status_code=500,
163
+ detail=f"Error processing question: {str(e)}"
164
+ )
165
 
 
166
  @app.post("/save_chat")
167
+ async def save_chat(chat: ChatMessage):
168
+ """Save chat message to database"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  try:
170
+ response = make_supabase_request(
171
+ "POST",
172
+ "/rest/v1/chat_logs",
173
+ data={
174
+ "admin_id": chat.admin_id,
175
+ "session_id": chat.session_id,
176
+ "is_bot": chat.is_bot,
177
+ "is_admin": chat.is_admin,
178
+ "message": chat.message
179
+ }
180
+ )
181
+ saved_data = response.json()[0]
182
+ return {
183
+ "message": "Pesan berhasil disimpan",
184
+ "id": saved_data["id"]
185
+ }
186
+ except HTTPException as e:
187
+ raise e
188
  except Exception as e:
189
+ raise HTTPException(
190
+ status_code=500,
191
+ detail=f"Failed to save chat: {str(e)}"
192
+ )
193
 
 
194
  @app.get("/chat_history")
195
+ async def get_chat_history(admin_id: str, session_id: str):
196
+ """Get chat history for specific session"""
 
 
 
 
 
 
 
 
 
 
 
197
  try:
198
+ response = make_supabase_request(
199
+ "GET",
200
+ "/rest/v1/chat_logs",
201
+ params={
202
+ "admin_id": f"eq.{admin_id}",
203
+ "or": f"(session_id.eq.{session_id},is_bot.eq.true)",
204
+ "order": "created_at.asc"
205
+ }
206
+ )
207
+ return response.json()
208
+ except HTTPException as e:
209
+ raise e
210
  except Exception as e:
211
+ raise HTTPException(
212
+ status_code=500,
213
+ detail=f"Failed to get chat history: {str(e)}"
214
+ )
 
 
 
 
 
 
 
215
 
216
+ @app.post("/delete_chat")
217
+ async def delete_chat(request: DeleteRequest):
218
+ """Delete specific chat message"""
219
+ if not request.id:
220
+ raise HTTPException(
221
+ status_code=400,
222
+ detail="Message ID is required"
223
+ )
224
 
225
  try:
226
+ make_supabase_request(
227
+ "DELETE",
228
+ f"/rest/v1/chat_logs?id=eq.{request.id}"
229
+ )
230
+ return {"message": f"Pesan dengan ID {request.id} berhasil dihapus."}
231
+ except HTTPException as e:
232
+ raise e
233
  except Exception as e:
234
+ raise HTTPException(
235
+ status_code=500,
236
+ detail=f"Failed to delete message: {str(e)}"
237
+ )
238
 
 
239
  @app.post("/delete_all_by_uid")
240
+ async def delete_all_by_uid(request: DeleteRequest):
241
+ """Delete all messages for specific user"""
242
+ if not request.uid:
243
+ raise HTTPException(
244
+ status_code=400,
245
+ detail="UID is required"
246
+ )
 
 
 
 
 
 
 
247
 
248
  try:
249
+ make_supabase_request(
250
+ "DELETE",
251
+ f"/rest/v1/chat_logs?admin_id=eq.{request.uid}"
252
+ )
253
+ return {"message": f"Semua pesan untuk UID {request.uid} berhasil dihapus."}
254
+ except HTTPException as e:
255
+ raise e
256
  except Exception as e:
257
+ raise HTTPException(
258
+ status_code=500,
259
+ detail=f"Failed to delete messages: {str(e)}"
260
+ )
261
+
262
+ @app.get("/health")
263
+ async def health_check():
264
+ """Health check endpoint"""
265
+ return {"status": "healthy", "version": "1.0.0"}