xenux4u commited on
Commit
b007b12
·
verified ·
1 Parent(s): fae2bde

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +35 -112
app.py CHANGED
@@ -1,19 +1,12 @@
1
- """
2
- ORBIT – Flask Application (SaaS Edition)
3
- Backend: Google OAuth · SQLite/PostgreSQL · Multi-provider LLM routing
4
- """
5
  import os
6
  import json
7
  import requests
8
- import fitz # PyMuPDF
9
 
10
  from functools import wraps
11
  from datetime import datetime
12
 
13
- from flask import (
14
- Flask, render_template, redirect, url_for,
15
- session, jsonify, request, abort,
16
- )
17
  from flask_cors import CORS
18
  from authlib.integrations.flask_client import OAuth
19
 
@@ -21,25 +14,13 @@ from config import Config
21
  from extensions import db
22
  from models import User, UserSettings
23
 
24
- # ── Allow HTTP for local OAuth (dev only) ───────────────────────────────────
25
  os.environ.setdefault("OAUTHLIB_INSECURE_TRANSPORT", "1")
26
 
27
- # ── App Factory ─────────────────────────────────────────────────────────────
28
  app = Flask(__name__, template_folder="templates", static_folder="static")
29
  app.config.from_object(Config)
30
  CORS(app)
31
  db.init_app(app)
32
 
33
- # ── FIX IFRAME & SECURITY HEADERS ───────────────────────────────────────────
34
- @app.after_request
35
- def add_header(response):
36
- # Mengizinkan domain .my.id lo untuk membungkus app ini dalam iframe
37
- response.headers['Content-Security-Policy'] = "frame-ancestors 'self' https://orbit-ai.my.id https://*.my.id"
38
- # Menghapus batasan X-Frame-Options agar bisa tampil di domain luar
39
- response.headers.pop('X-Frame-Options', None)
40
- return response
41
-
42
- # ── OAuth Setup ─────────────────────────────────────────────────────────────
43
  oauth = OAuth(app)
44
  google = oauth.register(
45
  name="google",
@@ -49,14 +30,9 @@ google = oauth.register(
49
  client_kwargs={"scope": "openid email profile"},
50
  )
51
 
52
- # ── DB Init ──────────────────────────────────────────────────────────────────
53
  with app.app_context():
54
  db.create_all()
55
 
56
-
57
- # ─────────────────────────────────────────────
58
- # Auth helpers
59
- # ─────────────────────────────────────────────
60
  def login_required(f):
61
  @wraps(f)
62
  def decorated(*args, **kwargs):
@@ -67,68 +43,58 @@ def login_required(f):
67
  return f(*args, **kwargs)
68
  return decorated
69
 
70
-
71
  def get_current_user() -> User | None:
72
  uid = session.get("user_id")
73
  if not uid:
74
  return None
75
  return db.session.get(User, uid)
76
 
77
-
78
  def seed_user_settings(user: User):
79
- """Create default UserSettings for a brand-new user."""
80
  cfg = Config
81
  settings = UserSettings(
82
- user_id = user.id,
83
- provider = cfg.DEFAULT_PROVIDER,
84
- base_url = cfg.DEFAULT_BASE_URL,
85
- api_key = "",
86
- current_model = cfg.DEFAULT_MODEL,
87
- models_openrouter = list(cfg.DEFAULT_MODELS_OPENROUTER),
88
- models_nvidia = list(cfg.DEFAULT_MODELS_NVIDIA),
89
  )
90
  db.session.add(settings)
91
  db.session.commit()
92
 
93
-
94
- # ─────────────────────────────────────────────
95
- # Auth Routes
96
- # ─────────────────────────────────────────────
97
  @app.route("/auth/login")
98
  def auth_login():
99
- redirect_uri = url_for("auth_callback", _external=True)
100
  return google.authorize_redirect(redirect_uri)
101
 
102
-
103
  @app.route("/auth/callback")
104
  def auth_callback():
105
  try:
106
- token = google.authorize_access_token()
107
  user_info = token.get("userinfo") or google.userinfo()
108
  except Exception as e:
109
  return f"<h3>OAuth Error: {e}</h3><a href='/'>Retry</a>", 400
110
 
111
  google_id = user_info.get("sub")
112
- email = user_info.get("email", "")
113
- name = user_info.get("name", email)
114
- picture = user_info.get("picture", "")
115
 
116
- # Upsert user
117
  user = User.query.filter_by(google_id=google_id).first()
118
  is_new = user is None
119
 
120
  if is_new:
121
  user = User(google_id=google_id, email=email, name=name, picture=picture)
122
  db.session.add(user)
123
- db.session.flush() # get user.id before commit
124
  else:
125
- user.name = name
126
  user.picture = picture
127
  user.last_login = datetime.utcnow()
128
 
129
  db.session.commit()
130
 
131
- # Seed settings for first-time users
132
  if is_new or user.settings is None:
133
  seed_user_settings(user)
134
 
@@ -136,32 +102,22 @@ def auth_callback():
136
  session["user_id"] = user.id
137
  return redirect(url_for("index"))
138
 
139
-
140
  @app.route("/auth/logout")
141
  def auth_logout():
142
  session.clear()
143
  return redirect(url_for("login_page"))
144
 
145
-
146
- # ─────────────────────────────────────────────
147
- # Page Routes
148
- # ─────────────────────────────────────────────
149
  @app.route("/login")
150
  def login_page():
151
  if "user_id" in session:
152
  return redirect(url_for("index"))
153
  return render_template("login.html")
154
 
155
-
156
  @app.route("/")
157
  @login_required
158
  def index():
159
  return render_template("index.html")
160
 
161
-
162
- # ─────────────────────────────────────────────
163
- # API: Current User
164
- # ─────────────────────────────────────────────
165
  @app.route("/api/me")
166
  @login_required
167
  def api_me():
@@ -170,26 +126,21 @@ def api_me():
170
  return jsonify({"error": "User not found."}), 404
171
  return jsonify(user.to_dict())
172
 
173
-
174
- # ─────────────────────────────────────────────
175
- # API: User Settings
176
- # ─────────────────────────────────────────────
177
  @app.route("/api/settings", methods=["GET"])
178
  @login_required
179
  def api_settings_get():
180
  user = get_current_user()
181
- s = user.settings
182
  if not s:
183
  seed_user_settings(user)
184
  s = user.settings
185
  return jsonify(s.to_dict())
186
 
187
-
188
  @app.route("/api/settings", methods=["POST"])
189
  @login_required
190
  def api_settings_post():
191
  user = get_current_user()
192
- s = user.settings
193
  if not s:
194
  seed_user_settings(user)
195
  s = user.settings
@@ -198,7 +149,6 @@ def api_settings_post():
198
 
199
  if "provider" in data:
200
  s.provider = data["provider"]
201
- # Auto-set base_url from known providers
202
  s.base_url = Config.PROVIDER_URLS.get(data["provider"], s.base_url)
203
 
204
  if "base_url" in data and data["base_url"].strip():
@@ -220,49 +170,40 @@ def api_settings_post():
220
  db.session.commit()
221
  return jsonify(s.to_dict())
222
 
223
-
224
- # ─────────────────────────────────────────────
225
- # API: Chat
226
- # ─────────────────────────────────────────────
227
  @app.route("/api/chat", methods=["POST"])
228
  @login_required
229
  def api_chat():
230
  user = get_current_user()
231
- s = user.settings
232
  if not s:
233
- return jsonify({"error": "Settings not configured. Please save your API key first."}), 400
234
 
235
- data = request.get_json(force=True) or {}
236
- prompt = (data.get("prompt") or "").strip()
237
  messages = data.get("messages", [])
238
- # Allow client to override model (from the dropdown)
239
- model = (data.get("model") or s.current_model or "").strip()
240
 
241
  if not prompt:
242
  return jsonify({"error": "Prompt is required."}), 400
243
  if not s.api_key:
244
- return jsonify({"error": "No API key saved. Please open Settings and add your API key."}), 400
245
  if not model:
246
  return jsonify({"error": "No model selected."}), 400
247
 
248
  SYSTEM_PROMPT = (
249
  "You are ORBIT, an intelligent Educational Research Assistant. "
250
  "Provide accurate, well-structured academic answers. "
251
- "Use Markdown formatting (bold, bullet points, code blocks) where appropriate. "
252
  "Keep responses concise unless the user requests more detail."
253
  )
254
 
255
- api_key = s.api_key
256
  base_url = s.base_url
257
  provider = s.provider
258
 
259
  try:
260
- # ── Google Gemini ─────────────────────────────��──────────────────
261
  if provider == "Google Gemini":
262
- gemini_url = (
263
- f"https://generativelanguage.googleapis.com/v1beta/models/"
264
- f"{model}:generateContent?key={api_key}"
265
- )
266
  contents = []
267
  for m in messages[-10:]:
268
  role = "user" if m.get("role") == "user" else "model"
@@ -281,7 +222,6 @@ def api_chat():
281
  reply = json.dumps(result)
282
  return jsonify({"reply": reply})
283
 
284
- # ── OpenAI-Compatible ────────────────────────────────────────────
285
  headers = {
286
  "Content-Type": "application/json",
287
  "Authorization": f"Bearer {api_key}",
@@ -296,7 +236,7 @@ def api_chat():
296
  composed.append({"role": "user", "content": prompt})
297
 
298
  payload = {"model": model, "messages": composed, "temperature": 0.6, "max_tokens": 1536}
299
- resp = requests.post(base_url, headers=headers, json=payload, timeout=60)
300
 
301
  if resp.status_code != 200:
302
  try:
@@ -309,16 +249,12 @@ def api_chat():
309
  return jsonify({"reply": reply})
310
 
311
  except requests.exceptions.Timeout:
312
- return jsonify({"error": "Request timed out. The model is taking too long."}), 504
313
  except requests.exceptions.ConnectionError:
314
  return jsonify({"error": "Could not connect to the AI provider."}), 503
315
  except Exception as e:
316
  return jsonify({"error": f"Server error: {str(e)}"}), 500
317
 
318
-
319
- # ─────────────────────────────────────────────
320
- # API: Upload PDF
321
- # ─────────────────────────────────────────────
322
  @app.route("/api/upload_pdf", methods=["POST"])
323
  @login_required
324
  def api_upload_pdf():
@@ -354,7 +290,7 @@ def api_upload_pdf():
354
 
355
  full_text = "\n\n".join(parts).strip()
356
  if not full_text:
357
- return jsonify({"error": "Could not extract readable text. PDF may be image-based."}), 422
358
 
359
  return jsonify({"text": full_text, "filename": f.filename, "word_count": word_count})
360
 
@@ -363,14 +299,10 @@ def api_upload_pdf():
363
  except Exception as e:
364
  return jsonify({"error": f"PDF error: {str(e)}"}), 500
365
 
366
-
367
- # ─────────────────────────────────────────────
368
- # API: Validate DOI
369
- # ─────────────────────────────────────────────
370
  @app.route("/api/validate_doi", methods=["POST"])
371
  @login_required
372
  def api_validate_doi():
373
- data = request.get_json(force=True) or {}
374
  raw_doi = (data.get("doi") or "").strip()
375
  if not raw_doi:
376
  return jsonify({"error": "DOI is required."}), 400
@@ -393,8 +325,8 @@ def api_validate_doi():
393
 
394
  work = resp.json().get("message", {})
395
 
396
- titles = work.get("title", [])
397
- title = titles[0] if titles else "No title available"
398
 
399
  authors_raw = work.get("author", [])
400
  if authors_raw:
@@ -413,8 +345,8 @@ def api_validate_doi():
413
  break
414
 
415
  containers = work.get("container-title", [])
416
- journal = containers[0] if containers else work.get("publisher", "N/A")
417
- pub_type = work.get("type", "journal-article").replace("-", " ").title()
418
 
419
  return jsonify({"doi": raw_doi, "title": title, "authors": authors,
420
  "year": year, "journal": journal, "type": pub_type})
@@ -426,15 +358,6 @@ def api_validate_doi():
426
  except Exception as e:
427
  return jsonify({"error": f"DOI error: {str(e)}"}), 500
428
 
429
-
430
- # ─────────────────────────────────────────────
431
- # Entry Point
432
- # ─────────────────────────────────────────────
433
  if __name__ == "__main__":
434
- # Hugging Face menggunakan port 7860 secara default
435
  port = int(os.environ.get("PORT", 7860))
436
-
437
- # Cetak info untuk log server
438
- print(f"[ORBIT] Starting server on port {port}...")
439
-
440
  app.run(host="0.0.0.0", port=port, debug=False)
 
 
 
 
 
1
  import os
2
  import json
3
  import requests
4
+ import fitz
5
 
6
  from functools import wraps
7
  from datetime import datetime
8
 
9
+ from flask import Flask, render_template, redirect, url_for, session, jsonify, request, abort
 
 
 
10
  from flask_cors import CORS
11
  from authlib.integrations.flask_client import OAuth
12
 
 
14
  from extensions import db
15
  from models import User, UserSettings
16
 
 
17
  os.environ.setdefault("OAUTHLIB_INSECURE_TRANSPORT", "1")
18
 
 
19
  app = Flask(__name__, template_folder="templates", static_folder="static")
20
  app.config.from_object(Config)
21
  CORS(app)
22
  db.init_app(app)
23
 
 
 
 
 
 
 
 
 
 
 
24
  oauth = OAuth(app)
25
  google = oauth.register(
26
  name="google",
 
30
  client_kwargs={"scope": "openid email profile"},
31
  )
32
 
 
33
  with app.app_context():
34
  db.create_all()
35
 
 
 
 
 
36
  def login_required(f):
37
  @wraps(f)
38
  def decorated(*args, **kwargs):
 
43
  return f(*args, **kwargs)
44
  return decorated
45
 
 
46
  def get_current_user() -> User | None:
47
  uid = session.get("user_id")
48
  if not uid:
49
  return None
50
  return db.session.get(User, uid)
51
 
 
52
  def seed_user_settings(user: User):
 
53
  cfg = Config
54
  settings = UserSettings(
55
+ user_id=user.id,
56
+ provider=cfg.DEFAULT_PROVIDER,
57
+ base_url=cfg.DEFAULT_BASE_URL,
58
+ api_key="",
59
+ current_model=cfg.DEFAULT_MODEL,
60
+ models_openrouter=list(cfg.DEFAULT_MODELS_OPENROUTER),
61
+ models_nvidia=list(cfg.DEFAULT_MODELS_NVIDIA),
62
  )
63
  db.session.add(settings)
64
  db.session.commit()
65
 
 
 
 
 
66
  @app.route("/auth/login")
67
  def auth_login():
68
+ redirect_uri = "https://orbit-ai.my.id/auth/callback"
69
  return google.authorize_redirect(redirect_uri)
70
 
 
71
  @app.route("/auth/callback")
72
  def auth_callback():
73
  try:
74
+ token = google.authorize_access_token()
75
  user_info = token.get("userinfo") or google.userinfo()
76
  except Exception as e:
77
  return f"<h3>OAuth Error: {e}</h3><a href='/'>Retry</a>", 400
78
 
79
  google_id = user_info.get("sub")
80
+ email = user_info.get("email", "")
81
+ name = user_info.get("name", email)
82
+ picture = user_info.get("picture", "")
83
 
 
84
  user = User.query.filter_by(google_id=google_id).first()
85
  is_new = user is None
86
 
87
  if is_new:
88
  user = User(google_id=google_id, email=email, name=name, picture=picture)
89
  db.session.add(user)
90
+ db.session.flush()
91
  else:
92
+ user.name = name
93
  user.picture = picture
94
  user.last_login = datetime.utcnow()
95
 
96
  db.session.commit()
97
 
 
98
  if is_new or user.settings is None:
99
  seed_user_settings(user)
100
 
 
102
  session["user_id"] = user.id
103
  return redirect(url_for("index"))
104
 
 
105
  @app.route("/auth/logout")
106
  def auth_logout():
107
  session.clear()
108
  return redirect(url_for("login_page"))
109
 
 
 
 
 
110
  @app.route("/login")
111
  def login_page():
112
  if "user_id" in session:
113
  return redirect(url_for("index"))
114
  return render_template("login.html")
115
 
 
116
  @app.route("/")
117
  @login_required
118
  def index():
119
  return render_template("index.html")
120
 
 
 
 
 
121
  @app.route("/api/me")
122
  @login_required
123
  def api_me():
 
126
  return jsonify({"error": "User not found."}), 404
127
  return jsonify(user.to_dict())
128
 
 
 
 
 
129
  @app.route("/api/settings", methods=["GET"])
130
  @login_required
131
  def api_settings_get():
132
  user = get_current_user()
133
+ s = user.settings
134
  if not s:
135
  seed_user_settings(user)
136
  s = user.settings
137
  return jsonify(s.to_dict())
138
 
 
139
  @app.route("/api/settings", methods=["POST"])
140
  @login_required
141
  def api_settings_post():
142
  user = get_current_user()
143
+ s = user.settings
144
  if not s:
145
  seed_user_settings(user)
146
  s = user.settings
 
149
 
150
  if "provider" in data:
151
  s.provider = data["provider"]
 
152
  s.base_url = Config.PROVIDER_URLS.get(data["provider"], s.base_url)
153
 
154
  if "base_url" in data and data["base_url"].strip():
 
170
  db.session.commit()
171
  return jsonify(s.to_dict())
172
 
 
 
 
 
173
  @app.route("/api/chat", methods=["POST"])
174
  @login_required
175
  def api_chat():
176
  user = get_current_user()
177
+ s = user.settings
178
  if not s:
179
+ return jsonify({"error": "Settings not configured."}), 400
180
 
181
+ data = request.get_json(force=True) or {}
182
+ prompt = (data.get("prompt") or "").strip()
183
  messages = data.get("messages", [])
184
+ model = (data.get("model") or s.current_model or "").strip()
 
185
 
186
  if not prompt:
187
  return jsonify({"error": "Prompt is required."}), 400
188
  if not s.api_key:
189
+ return jsonify({"error": "No API key saved."}), 400
190
  if not model:
191
  return jsonify({"error": "No model selected."}), 400
192
 
193
  SYSTEM_PROMPT = (
194
  "You are ORBIT, an intelligent Educational Research Assistant. "
195
  "Provide accurate, well-structured academic answers. "
196
+ "Use Markdown formatting where appropriate. "
197
  "Keep responses concise unless the user requests more detail."
198
  )
199
 
200
+ api_key = s.api_key
201
  base_url = s.base_url
202
  provider = s.provider
203
 
204
  try:
 
205
  if provider == "Google Gemini":
206
+ gemini_url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
 
 
 
207
  contents = []
208
  for m in messages[-10:]:
209
  role = "user" if m.get("role") == "user" else "model"
 
222
  reply = json.dumps(result)
223
  return jsonify({"reply": reply})
224
 
 
225
  headers = {
226
  "Content-Type": "application/json",
227
  "Authorization": f"Bearer {api_key}",
 
236
  composed.append({"role": "user", "content": prompt})
237
 
238
  payload = {"model": model, "messages": composed, "temperature": 0.6, "max_tokens": 1536}
239
+ resp = requests.post(base_url, headers=headers, json=payload, timeout=60)
240
 
241
  if resp.status_code != 200:
242
  try:
 
249
  return jsonify({"reply": reply})
250
 
251
  except requests.exceptions.Timeout:
252
+ return jsonify({"error": "Request timed out."}), 504
253
  except requests.exceptions.ConnectionError:
254
  return jsonify({"error": "Could not connect to the AI provider."}), 503
255
  except Exception as e:
256
  return jsonify({"error": f"Server error: {str(e)}"}), 500
257
 
 
 
 
 
258
  @app.route("/api/upload_pdf", methods=["POST"])
259
  @login_required
260
  def api_upload_pdf():
 
290
 
291
  full_text = "\n\n".join(parts).strip()
292
  if not full_text:
293
+ return jsonify({"error": "Could not extract readable text."}), 422
294
 
295
  return jsonify({"text": full_text, "filename": f.filename, "word_count": word_count})
296
 
 
299
  except Exception as e:
300
  return jsonify({"error": f"PDF error: {str(e)}"}), 500
301
 
 
 
 
 
302
  @app.route("/api/validate_doi", methods=["POST"])
303
  @login_required
304
  def api_validate_doi():
305
+ data = request.get_json(force=True) or {}
306
  raw_doi = (data.get("doi") or "").strip()
307
  if not raw_doi:
308
  return jsonify({"error": "DOI is required."}), 400
 
325
 
326
  work = resp.json().get("message", {})
327
 
328
+ titles = work.get("title", [])
329
+ title = titles[0] if titles else "No title available"
330
 
331
  authors_raw = work.get("author", [])
332
  if authors_raw:
 
345
  break
346
 
347
  containers = work.get("container-title", [])
348
+ journal = containers[0] if containers else work.get("publisher", "N/A")
349
+ pub_type = work.get("type", "journal-article").replace("-", " ").title()
350
 
351
  return jsonify({"doi": raw_doi, "title": title, "authors": authors,
352
  "year": year, "journal": journal, "type": pub_type})
 
358
  except Exception as e:
359
  return jsonify({"error": f"DOI error: {str(e)}"}), 500
360
 
 
 
 
 
361
  if __name__ == "__main__":
 
362
  port = int(os.environ.get("PORT", 7860))
 
 
 
 
363
  app.run(host="0.0.0.0", port=port, debug=False)