ginigen-ai commited on
Commit
2ab06f9
·
verified ·
1 Parent(s): 8281d19

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +29 -1399
app.py CHANGED
@@ -1,1403 +1,33 @@
1
  import os
2
- os.environ['TZ'] = 'Asia/Seoul'
3
- try:
4
- import time as _tz_time
5
- _tz_time.tzset()
6
- except: pass
7
- import json
8
- import base64
9
- import asyncio
10
- import httpx
11
- import uvicorn
12
- import threading
13
- from datetime import datetime, timezone, timedelta
14
- from collections import Counter, defaultdict
15
- from pathlib import Path
16
- from fastapi import FastAPI, Request, BackgroundTasks
17
- import aiosqlite, shutil
18
- from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response, StreamingResponse
19
- from fastapi.middleware.cors import CORSMiddleware
20
- app = FastAPI()
21
- app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
22
- @app.exception_handler(Exception)
23
- async def global_exception_handler(request, exc):
24
- import traceback
25
- traceback.print_exc()
26
- return JSONResponse(
27
- status_code=500,
28
- content={"error": {"message": str(exc)[:200], "type": "server_error"}},
29
- )
30
- from shared import (
31
- GROQ_API_KEY, FAL_KEY, HF_TOKEN, ADMIN_EMAIL, SA_BACKUP_REPO, DATASET_ID,
32
- CLIP_BUCKET, CLIP_MAX_BYTES, GROQ_URL, SA_DB_PATH,
33
- _init_write_queue, _db_write, _db_write_sync, _db_read, _db_read_one,
34
- _db_enqueue, _sa_backup_db, _sanitize_text, _sanitize_messages
35
- )
36
- import asyncio as _asyncio
37
- _PERSISTENT_DIR = "/data"
38
- _LOCAL_DIR = "./data"
39
- def _ensure_persistent_storage():
40
- _hf_token = os.environ.get("HF_TOKEN", "")
41
- if not _hf_token:
42
- print("⚠️ HF_TOKEN 없음 — 영구 스토리지 자동 활성화 불가")
43
- return
44
- if os.path.exists(_PERSISTENT_DIR):
45
- try:
46
- tf = os.path.join(_PERSISTENT_DIR, ".write_test")
47
- with open(tf, "w") as f: f.write("ok")
48
- os.remove(tf)
49
- print("✅ /data 이미 활성화됨")
50
  return
51
- except:
52
- pass
53
- try:
54
- from huggingface_hub import HfApi
55
- api = HfApi(token=_hf_token)
56
- space_id = os.environ.get("SPACE_ID", "")
57
- if not space_id:
58
- space_id = "ginigen-ai/siteagent"
59
- api.request_space_storage(space_id, "small")
60
- print(f"🔧 영구 스토리지 활성화 요청 완료: {space_id} (small)")
61
- print(" → Space가 자동 재시작됩니다. 재시작 후 /data 사용 가능.")
62
- except Exception as e:
63
- err_str = str(e)
64
- if "already" in err_str.lower() or "409" in err_str:
65
- print(f"✅ 영구 스토리지 이미 활성화 상태 (API 확인)")
66
- else:
67
- print(f"⚠️ 영구 스토리지 활성화 실패: {e}")
68
- _ensure_persistent_storage()
69
- async def _sa_restore_db():
70
- if not HF_TOKEN: return False
71
- if os.path.exists(SA_DB_PATH) and os.path.getsize(SA_DB_PATH) > 0:
72
- try:
73
- import sqlite3
74
- conn = sqlite3.connect(SA_DB_PATH, timeout=10)
75
- conn.execute("PRAGMA busy_timeout=5000")
76
- conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
77
- result = conn.execute("PRAGMA integrity_check").fetchone()
78
- conn.close()
79
- if result[0] == 'ok':
80
- print("📍 [SA-restore] Existing DB healthy, skip restore")
81
- return False
82
- else:
83
- print(f"⚠️ [SA-restore] Existing DB corrupt, restoring...")
84
- os.remove(SA_DB_PATH)
85
- except Exception as e:
86
- print(f"⚠️ [SA-restore] DB check failed: {e}, restoring...")
87
- try: os.remove(SA_DB_PATH)
88
- except: pass
89
- def _sync():
90
- from huggingface_hub import HfApi, hf_hub_download
91
- try:
92
- api = HfApi(token=HF_TOKEN)
93
- api.create_repo(repo_id=SA_BACKUP_REPO, repo_type="dataset", private=True, exist_ok=True)
94
- except: pass
95
- return hf_hub_download(repo_id=SA_BACKUP_REPO, filename="latest/siteagent.db", repo_type="dataset", token=HF_TOKEN)
96
- try:
97
- import asyncio
98
- downloaded = await asyncio.to_thread(_sync)
99
- import sqlite3
100
- conn = sqlite3.connect(downloaded)
101
- result = conn.execute("PRAGMA integrity_check").fetchone()
102
- conn.close()
103
- if result[0] != 'ok':
104
- print("🚨 [SA-restore] Hub backup corrupt, starting fresh")
105
- return False
106
- for suffix in ['-wal', '-shm']:
107
- wf = SA_DB_PATH + suffix
108
- if os.path.exists(wf): os.remove(wf)
109
- shutil.copy(downloaded, SA_DB_PATH)
110
- print("✅ [SA-restore] DB restored from Hub")
111
- return True
112
  except Exception as e:
113
- print(f"⚠️ [SA-restore] failed (first run?): {e}")
114
- return False
115
- async def init_siteagent_db():
116
- await _sa_restore_db()
117
- async with aiosqlite.connect(SA_DB_PATH, timeout=30.0) as db:
118
- await db.execute("PRAGMA journal_mode=WAL")
119
- await db.execute("PRAGMA busy_timeout=5000")
120
- await db.execute("""CREATE TABLE IF NOT EXISTS users (
121
- id INTEGER PRIMARY KEY AUTOINCREMENT,
122
- email TEXT UNIQUE NOT NULL,
123
- nickname TEXT,
124
- role TEXT DEFAULT 'user',
125
- visit_count INTEGER DEFAULT 0,
126
- last_seen_at REAL,
127
- created_at REAL
128
- )""")
129
- await db.execute("""CREATE TABLE IF NOT EXISTS page_visits (
130
- id INTEGER PRIMARY KEY AUTOINCREMENT,
131
- email TEXT NOT NULL,
132
- url_hash TEXT NOT NULL,
133
- url TEXT,
134
- title TEXT,
135
- domain TEXT,
136
- visit_count INTEGER DEFAULT 1,
137
- total_duration_sec REAL DEFAULT 0,
138
- last_visited_at REAL,
139
- created_at REAL,
140
- UNIQUE(email, url_hash)
141
- )""")
142
- await db.execute("""CREATE TABLE IF NOT EXISTS user_inputs (
143
- id INTEGER PRIMARY KEY AUTOINCREMENT,
144
- email TEXT NOT NULL,
145
- url_hash TEXT,
146
- url TEXT,
147
- input_type TEXT,
148
- input_text TEXT,
149
- feature TEXT,
150
- result_length INTEGER DEFAULT 0,
151
- created_at REAL
152
- )""")
153
- await db.execute("""CREATE TABLE IF NOT EXISTS feature_usage (
154
- id INTEGER PRIMARY KEY AUTOINCREMENT,
155
- email TEXT NOT NULL,
156
- feature TEXT NOT NULL,
157
- use_count INTEGER DEFAULT 1,
158
- last_used_at REAL,
159
- UNIQUE(email, feature)
160
- )""")
161
- await db.execute("CREATE INDEX IF NOT EXISTS idx_visits_email ON page_visits(email, last_visited_at DESC)")
162
- await db.execute("CREATE INDEX IF NOT EXISTS idx_inputs_email ON user_inputs(email, created_at DESC)")
163
- await db.execute("CREATE INDEX IF NOT EXISTS idx_usage_email ON feature_usage(email, use_count DESC)")
164
- await db.execute("""CREATE TABLE IF NOT EXISTS waitlist (
165
- id INTEGER PRIMARY KEY AUTOINCREMENT,
166
- email TEXT UNIQUE NOT NULL,
167
- name TEXT,
168
- interest TEXT,
169
- source TEXT DEFAULT 'landing',
170
- created_at REAL
171
- )""")
172
- await db.execute("CREATE INDEX IF NOT EXISTS idx_waitlist_created ON waitlist(created_at DESC)")
173
- await db.execute("CREATE INDEX IF NOT EXISTS idx_visits_domain ON page_visits(domain, visit_count DESC)")
174
- await db.execute("""CREATE TABLE IF NOT EXISTS sessions (
175
- email TEXT PRIMARY KEY,
176
- chat TEXT DEFAULT '',
177
- cmd TEXT DEFAULT '',
178
- url TEXT DEFAULT '',
179
- title TEXT DEFAULT '',
180
- updated_at REAL DEFAULT 0
181
- )""")
182
- await db.execute("""CREATE TABLE IF NOT EXISTS analytics (
183
- id INTEGER PRIMARY KEY AUTOINCREMENT,
184
- email TEXT DEFAULT '',
185
- event TEXT NOT NULL,
186
- feature TEXT DEFAULT '',
187
- domain TEXT DEFAULT '',
188
- url TEXT DEFAULT '',
189
- provider TEXT DEFAULT 'demo',
190
- client TEXT DEFAULT '',
191
- model TEXT DEFAULT '',
192
- mode TEXT DEFAULT '',
193
- engine TEXT DEFAULT '',
194
- response_ms INTEGER DEFAULT 0,
195
- tokens_used INTEGER DEFAULT 0,
196
- success INTEGER DEFAULT 1,
197
- error_msg TEXT DEFAULT '',
198
- screen TEXT DEFAULT '',
199
- os TEXT DEFAULT '',
200
- browser TEXT DEFAULT '',
201
- language TEXT DEFAULT '',
202
- timezone TEXT DEFAULT '',
203
- referrer TEXT DEFAULT '',
204
- country TEXT DEFAULT '',
205
- session_id TEXT DEFAULT '',
206
- meta TEXT DEFAULT '',
207
- created_at REAL DEFAULT 0
208
- )""")
209
- await db.execute("CREATE INDEX IF NOT EXISTS idx_analytics_email ON analytics(email, created_at DESC)")
210
- await db.execute("CREATE INDEX IF NOT EXISTS idx_analytics_event ON analytics(event, created_at DESC)")
211
- await db.execute("CREATE INDEX IF NOT EXISTS idx_analytics_feature ON analytics(feature, created_at DESC)")
212
- for col, typ in [
213
- ("last_browser", "TEXT DEFAULT ''"),
214
- ("last_os", "TEXT DEFAULT ''"),
215
- ("last_screen", "TEXT DEFAULT ''"),
216
- ("last_language", "TEXT DEFAULT ''"),
217
- ("last_timezone", "TEXT DEFAULT ''"),
218
- ("last_country", "TEXT DEFAULT ''"),
219
- ("last_provider", "TEXT DEFAULT 'demo'"),
220
- ("last_client", "TEXT DEFAULT ''"),
221
- ("first_referrer", "TEXT DEFAULT ''"),
222
- ("total_ai_calls", "INTEGER DEFAULT 0"),
223
- ("total_ai_errors", "INTEGER DEFAULT 0"),
224
- ]:
225
- try:
226
- await db.execute(f"ALTER TABLE users ADD COLUMN {col} {typ}")
227
- except:
228
- pass # 이미 존재하면 무시
229
- import time
230
- cursor = await db.execute("SELECT email FROM users WHERE email=?", (ADMIN_EMAIL,))
231
- if not await cursor.fetchone():
232
- await db.execute("INSERT INTO users (email,nickname,role,created_at) VALUES (?,?,?,?)",
233
- (ADMIN_EMAIL, "🔑관리자", "admin", time.time()))
234
- # 🔐 시크릿 메신저 테이블 (암호문만 저장, 평문 접근 불가)
235
- await db.execute("""CREATE TABLE IF NOT EXISTS sec_keys (
236
- email_hash TEXT PRIMARY KEY,
237
- pubkey_jwk TEXT NOT NULL,
238
- registered_at REAL
239
- )""")
240
- await db.execute("""CREATE TABLE IF NOT EXISTS sec_messages (
241
- id TEXT PRIMARY KEY,
242
- from_hash TEXT NOT NULL,
243
- to_hash TEXT NOT NULL,
244
- ciphertext TEXT NOT NULL,
245
- iv TEXT NOT NULL,
246
- group_id TEXT DEFAULT '',
247
- ts REAL NOT NULL,
248
- delivered INTEGER DEFAULT 0
249
- )""")
250
- await db.execute("CREATE INDEX IF NOT EXISTS idx_sec_msg_to ON sec_messages(to_hash, ts)")
251
- await db.execute("CREATE INDEX IF NOT EXISTS idx_sec_msg_ts ON sec_messages(ts)")
252
- await db.commit()
253
- count = await db.execute("SELECT COUNT(*) FROM users")
254
- cnt = (await count.fetchone())[0]
255
- sec_cnt = await db.execute("SELECT COUNT(*) FROM sec_keys")
256
- sec_n = (await sec_cnt.fetchone())[0]
257
- print(f"✅ SiteAgent DB initialized: {SA_DB_PATH} ({cnt} users, sec_keys: {sec_n})")
258
- LOG_FILE = "/tmp/sa_events.jsonl"
259
- import re as _re
260
- def _strip_response_tables(data):
261
- try:
262
- if isinstance(data, dict):
263
- for ch in data.get("choices", []):
264
- msg = ch.get("message", {})
265
- if isinstance(msg.get("content"), str):
266
- msg["content"] = _strip_md_table(msg["content"])
267
- except Exception:
268
- pass
269
- return data
270
- def _strip_md_table(text):
271
- if not isinstance(text, str) or '|' not in text:
272
- return text
273
- lines = text.split('\n')
274
- out = []
275
- tbl = []
276
- in_tbl = False
277
- for line in lines:
278
- if _re.match(r'^\s*\|.+\|\s*$', line):
279
- in_tbl = True
280
- tbl.append(line)
281
- else:
282
- if in_tbl:
283
- _flush_table(tbl, out)
284
- tbl = []
285
- in_tbl = False
286
- out.append(line)
287
- if in_tbl:
288
- _flush_table(tbl, out)
289
- return '\n'.join(out)
290
- def _flush_table(tbl, out):
291
- if len(tbl) < 2:
292
- out.extend(tbl)
293
- return
294
- headers = [c.strip() for c in tbl[0].strip('|').split('|')]
295
- for i, tr in enumerate(tbl):
296
- cells = [c.strip() for c in tr.strip('|').split('|')]
297
- if i == 0:
298
- out.append('**' + ' · '.join(c for c in cells if c) + '**')
299
- elif i == 1 and _re.match(r'^[\s:\-|]+$', tr):
300
- continue
301
- else:
302
- parts = []
303
- for j, cell in enumerate(cells):
304
- if cell:
305
- if j < len(headers) and headers[j] and headers[j] != cell:
306
- parts.append(f'**{headers[j]}**: {cell}')
307
- else:
308
- parts.append(cell)
309
- out.append('• ' + ' / '.join(parts))
310
- _PAGE_AGENT_JS: bytes = b""
311
- def _load_js():
312
- global _PAGE_AGENT_JS
313
- candidates = [
314
- Path(__file__).parent / "secure-pageagent.extend.js",
315
- Path(".") / "secure-pageagent.extend.js",
316
- Path("/home/user/app") / "secure-pageagent.extend.js",
317
- ]
318
- for local in candidates:
319
- try:
320
- if local.exists():
321
- _PAGE_AGENT_JS = local.read_bytes()
322
- print(f"✅ secure-pageagent.extend.js loaded ({len(_PAGE_AGENT_JS):,} bytes) from {local}")
323
- return
324
- except:
325
- continue
326
- print("❌ secure-pageagent.extend.js not found")
327
- _load_js()
328
- # ── sa-secret.js 합체 (시크릿 메신저 클라이언트) ──
329
- def _load_secret_js():
330
- global _PAGE_AGENT_JS
331
- if not _PAGE_AGENT_JS:
332
- return
333
- for d in [Path(__file__).parent, Path("."), Path("/home/user/app")]:
334
- f = d / "sa-secret.js"
335
- try:
336
- if f.exists():
337
- secret = f.read_bytes()
338
- _PAGE_AGENT_JS = _PAGE_AGENT_JS + b"\n;\n" + secret
339
- print(f"✅ sa-secret.js appended ({len(secret):,} bytes) → total {len(_PAGE_AGENT_JS):,} bytes")
340
- return
341
- except:
342
- continue
343
- print("⚠️ sa-secret.js not found (시크릿 메신저 비활성)")
344
- _load_secret_js()
345
- @app.get("/api/agent-script")
346
- async def agent_script():
347
- if not _PAGE_AGENT_JS:
348
- return JSONResponse({"error": "agent script not loaded"}, status_code=500)
349
- return JSONResponse({
350
- "js": base64.b64encode(_PAGE_AGENT_JS).decode(),
351
- "size": len(_PAGE_AGENT_JS)
352
- })
353
- MAX_RETRIES = 2
354
- # ── _chat_log_bg 미정의 방지 ──
355
- async def _chat_log_bg(*args, **kwargs):
356
- pass
357
- # ═══════════════════════════════════════════════════════
358
- # 축 1: 의도 분류 (Classify → Route)
359
- # ═══════════════════════════════════════════════════════
360
- _READ_KEYWORDS = [
361
- "요약","번역","분석","설명","알려","뭐야","뭔가","무엇","읽어","해석","정리","리뷰",
362
- "핵심","키워드","내용","뜻","의미","비교해","차이","장단점",
363
- "summarize","translate","analyze","explain","what is","what are","compare","review","tell me"
364
- ]
365
- _ACT_KEYWORDS = [
366
- "클릭","눌러","누르","입력","적어","써줘","작성","검색해","찾아","열어","이동","접속",
367
- "다운로드","업로드","제출","전송","보내","삭제","로그인","가입","장바구니","담아","구매",
368
- "예약","신청","발급","선택","체크","활성","비활성","토글","스크롤","내려","올려",
369
- "click","type","input","search","open","go to","navigate","download","submit","login","book"
370
- ]
371
- def _classify_intent(user_text: str) -> str:
372
- """사용자 입력을 READ / ACT 중 하나로 분류. 앞 150자만 사용(DOM 키워드 오염 방지)"""
373
- t = user_text[:150].lower().strip()
374
- if not t:
375
- return "READ"
376
- act_score = sum(1 for k in _ACT_KEYWORDS if k in t)
377
- read_score = sum(1 for k in _READ_KEYWORDS if k in t)
378
- # ACT 키워드가 있고, READ보다 같거나 많으면 ACT (동점이면 행동 우선)
379
- if act_score >= 1 and act_score >= read_score:
380
- return "ACT"
381
- # ACT 키워드가 있지만 READ가 더 많으면 READ
382
- # ACT 키워드가 없으면 READ
383
- return "READ"
384
-
385
- # ═══════════════════════════════════════════════════════
386
- # 축 2 + 축 3: 모드별 시스템 프롬프트
387
- # ═══════════════════════════════════════════════════════
388
-
389
- # ── 공통 JSON 포맷 + 액션 목록 ──
390
- _ACTION_FORMAT = """JSON만 출력. 코드블록 금지. done.text는 한국어.
391
- {"evaluation_previous_goal":"1줄","memory":"핵심만","next_goal":"1줄","action":{액션}}
392
- 액션: done{text,success} | click_element_by_index{index} | input_text{index,text} | select_dropdown_option{index,text} | scroll{down,num_pages} | scroll_horizontally{right,pixels} | wait{seconds} | execute_javascript{script} | ask_user{question}
393
- ★모든 필드는 최대한 짧게. evaluation/memory/next_goal 각각 20자 이내."""
394
-
395
- # ── READ 모드 ──
396
- PROMPT_READ = _ACTION_FORMAT + """
397
- ★READ: DOM 텍스트만으로 즉시 done. scroll/wait/click 절대 금지. 답 없으면 "해당 정보 없음"으로 done."""
398
-
399
- # ── ACT 모드 ──
400
- PROMPT_ACT = _ACTION_FORMAT + """
401
- ★ACT: 최소 스텝. 속도 최우선.
402
- - 클릭 전 index 확인. contenteditable→click먼저→input_text.
403
- - 입력 후 제출: 다음 스텝에서 반드시 즉시 검색/제출 버튼 클릭. DOM 분석하지 말고 바로 클릭.
404
- - ★결과 도착 후: 보이는 텍스트에서 핵심만 뽑아 3문장 이내로 즉시 done. 긴 분석/정리/마크다운 포맷 금지. 짧고 빠르게.
405
- - 복합작업: memory에 간단한 계획 적고 순서대로.
406
- - 예약/검색 시 시간 필드 설정(미설정=00:00).
407
- - 실패→반복금지. ①유사요소 ②JS실행 ③scroll1회 ④ask_user.
408
- - wait 1회(2초)만. 3스텝 진전없음→결과로 done."""
409
-
410
- # ── PLAN은 ACT에 흡수 (별도 모드 불필요) ──
411
- PROMPT_PLAN = PROMPT_ACT
412
-
413
- # ── DOM 크기 제한 ──
414
- _MAX_DOM_READ = 15000
415
- _MAX_DOM_ACT = 10000
416
- def _truncate_large_dom(msgs: list, intent: str = "READ") -> list:
417
- limit = _MAX_DOM_ACT if intent in ("ACT", "PLAN") else _MAX_DOM_READ
418
- for m in reversed(msgs):
419
- if m.get("role") != "user":
420
- continue
421
- content = m.get("content", "")
422
- if isinstance(content, str) and len(content) > limit:
423
- m["content"] = content[:limit] + "\n[...DOM 생략. 보이는 영역 중심으로 즉시 판단하라.]"
424
- print(f"[proxy] ⚠️ DOM truncated: {len(content)} → {limit} ({intent})")
425
- break
426
- return msgs
427
- @app.post("/api/chat/completions")
428
- async def chat_proxy(request: Request, background_tasks: BackgroundTasks):
429
- try:
430
- body = await request.json()
431
- except Exception:
432
- return JSONResponse({"error": "invalid json"}, status_code=400)
433
- if not GROQ_API_KEY:
434
- return JSONResponse({"error": "GROQ_API_KEY not set"}, status_code=500)
435
- original_tools = body.pop("tools", None)
436
- body.pop("tool_choice", None)
437
- body.pop("parallel_tool_calls", None)
438
- sa_mode = body.pop("sa_mode", None)
439
- _sa_email_val = body.pop("sa_email", "")
440
- _sa_url_val = body.pop("sa_url", "")
441
- if "messages" in body:
442
- body["messages"] = _sanitize_messages(body["messages"])
443
- import time as _time, hashlib as _hl
444
- try:
445
- _ref = request.headers.get("referer", "") or request.headers.get("origin", "")
446
- _msgs = body.get("messages", [])
447
- _last_user = ""
448
- for _m in reversed(_msgs):
449
- if _m.get("role") == "user":
450
- _ct = _m.get("content", "")
451
- if isinstance(_ct, str):
452
- _last_user = _ct[:500]
453
- elif isinstance(_ct, list):
454
- for _p in _ct:
455
- if isinstance(_p, dict) and _p.get("type") == "text":
456
- _last_user = _p.get("text", "")[:500]
457
- break
458
- break
459
- _email = _sa_email_val or ""
460
- if not _email:
461
- try:
462
- _row = await _db_read_one("SELECT email FROM users ORDER BY last_seen_at DESC LIMIT 1")
463
- if _row: _email = dict(_row).get("email", "")
464
- except: pass
465
- if not _email: _email = ADMIN_EMAIL
466
- _feat = sa_mode if sa_mode else "chat"
467
- _uh = _hl.sha256(_ref.encode()).hexdigest()[:16] if _ref else "no_ref"
468
- print(f"[log] ★ email={_email}, feat={_feat}, user_msg={bool(_last_user)}, ref={_ref[:50]}")
469
- background_tasks.add_task(_chat_log_bg, _email, _uh, _ref, _last_user, _feat, _time.time())
470
- except Exception as _le:
471
  import traceback
472
- print(f"[log] ❌ ERROR: {_le}")
473
- traceback.print_exc()
474
- if not sa_mode:
475
- sa_mode = request.headers.get("X-SA-Mode", "")
476
- body["model"] = "openai/gpt-oss-120b"
477
- body["stream"] = False
478
- body["temperature"] = 1
479
- body["top_p"] = 1
480
- body["stop"] = None
481
- for k in ["max_tokens", "thinking", "verbosity"]:
482
- body.pop(k, None)
483
- if sa_mode == "search":
484
- body["tools"] = [{"type": "browser_search"}]
485
- body["tool_choice"] = "required"
486
- body["max_completion_tokens"] = 4096
487
- body["reasoning_effort"] = "low"
488
- original_tools = None # tool_call 래핑 안 함
489
- msgs = body.get("messages", [])
490
- if msgs and msgs[0].get("role") == "system":
491
- msgs[0]["content"] += "\n\n[중요] 반드시 자연스러운 한국어로 답변하라."
492
- elif msgs:
493
- msgs.insert(0, {"role": "system", "content": "반드시 자연스러운 한국어로 답변하라."})
494
- body["messages"] = msgs
495
- print("[proxy] mode: browser_search")
496
- elif sa_mode == "calc":
497
- body["tools"] = [{"type": "code_interpreter"}]
498
- body["tool_choice"] = "required"
499
- body["max_completion_tokens"] = 4096
500
- body["reasoning_effort"] = "medium"
501
- original_tools = None
502
- msgs = body.get("messages", [])
503
- if msgs and msgs[0].get("role") == "system":
504
- msgs[0]["content"] += "\n\n[중요] 계산 결과를 자연스러운 한국어로 설명하라."
505
- elif msgs:
506
- msgs.insert(0, {"role": "system", "content": "계산 결과를 자연스러운 한국어로 설명하라."})
507
- body["messages"] = msgs
508
- print("[proxy] mode: code_interpreter")
509
- elif original_tools:
510
- msgs = body.get("messages", [])
511
- # ── 사용자 마지막 텍스트 추출 ──
512
- user_msgs = [m for m in msgs if m.get("role") == "user"]
513
- _last_txt = ""
514
- for _um in reversed(user_msgs):
515
- _umc = _um.get("content","")
516
- if isinstance(_umc,str): _last_txt=_umc; break
517
- elif isinstance(_umc,list):
518
- for _p in _umc:
519
- if isinstance(_p,dict) and _p.get("type")=="text": _last_txt=_p.get("text",""); break
520
- break
521
- # ── 축 1: 의도 분류 (READ / ACT) ──
522
- _intent = _classify_intent(_last_txt)
523
- # ── DOM 크기 제한 (모드별 차등) ──
524
- msgs = _truncate_large_dom(msgs, _intent)
525
- # ── 모드별 프롬프트 + reasoning 전부 low ──
526
- if _intent == "READ":
527
- _prompt = PROMPT_READ
528
- body["max_completion_tokens"] = 4096
529
- else: # ACT
530
- _prompt = PROMPT_ACT
531
- body["max_completion_tokens"] = 1024
532
- body["reasoning_effort"] = "low"
533
- # ── 시스템 프롬프트 주입 ──
534
- if msgs and msgs[0].get("role") == "system":
535
- msgs[0]["content"] = msgs[0]["content"] + "\n" + _prompt
536
- else:
537
- msgs.insert(0, {"role": "system", "content": _prompt})
538
- body["messages"] = msgs
539
- print(f"[proxy] mode: dom_action/{_intent} (reasoning: {body['reasoning_effort']}, steps: {len(user_msgs)})")
540
- else:
541
- body["max_completion_tokens"] = 4096
542
- body["reasoning_effort"] = "low"
543
- msgs = body.get("messages", [])
544
- if msgs and msgs[0].get("role") == "system":
545
- if "한국어" not in msgs[0]["content"]:
546
- msgs[0]["content"] += "\n\n[중요] 최종 답변은 반드시 자연스러운 한국어로 작성하라."
547
- body["messages"] = msgs
548
- print("[proxy] mode: text")
549
- # ═══ 메모리 컨텍스트 주입 ═══
550
- _memory_injected = False
551
- if _email and sa_mode not in ("search", "calc") and not original_tools:
552
- try:
553
- mem_ctx = await build_memory_context(_email, _last_user)
554
- if mem_ctx:
555
- msgs = body.get("messages", [])
556
- if msgs and msgs[0].get("role") == "system":
557
- msgs[0]["content"] = msgs[0]["content"] + "\n\n" + mem_ctx
558
- else:
559
- msgs.insert(0, {"role": "system", "content": "당신은 SiteAgent AI 어시스턴트입니다. 사용자의 기억과 이력을 활용해 개인화된 도움을 제공합니다.\n\n" + mem_ctx})
560
- body["messages"] = msgs
561
- _memory_injected = True
562
- print(f"[memory] ✅ injected for {_email} ({len(mem_ctx)} chars)")
563
- except Exception as me:
564
- print(f"[memory] context error: {me}")
565
- groq_headers = {
566
- "Authorization": f"Bearer {GROQ_API_KEY}",
567
- "Content-Type": "application/json",
568
- }
569
- data = None
570
- status = 500
571
- for attempt in range(MAX_RETRIES + 1):
572
- try:
573
- async with httpx.AsyncClient(timeout=180) as client:
574
- resp = await client.post(GROQ_URL, headers=groq_headers, json=body)
575
- try:
576
- data = resp.json()
577
- except Exception:
578
- print(f"[proxy] Groq returned non-JSON: {resp.text[:200]}")
579
- data = {"error": {"message": "Groq returned invalid response", "type": "proxy_error"}}
580
- status = resp.status_code
581
- if status == 429:
582
- wait = 2 * (attempt + 1)
583
- print(f"[proxy] 429 → retry {attempt+1}/{MAX_RETRIES} ({wait}s)")
584
- if attempt < MAX_RETRIES:
585
- await asyncio.sleep(wait)
586
- continue
587
- elif status >= 500:
588
- err_body = str(data)[:300] if data else resp.text[:300]
589
- print(f"[proxy] {status} body={err_body} → retry {attempt+1}/{MAX_RETRIES}")
590
- if attempt == 0 and "reasoning_effort" in body:
591
- removed = body.pop("reasoning_effort", None)
592
- print(f"[proxy] removed reasoning_effort={removed} for retry")
593
- if attempt < MAX_RETRIES:
594
- await asyncio.sleep(1)
595
- continue
596
- break
597
- except Exception as e:
598
- print(f"[proxy] exception: {e}")
599
- if attempt < MAX_RETRIES:
600
- await asyncio.sleep(1)
601
- continue
602
- return JSONResponse({"error": {"message": str(e)}}, status_code=502)
603
- if status != 200:
604
- return JSONResponse(data or {"error": "unknown"}, status_code=status)
605
- if sa_mode in ("search", "calc"):
606
- try:
607
- content = data["choices"][0]["message"].get("content", "")
608
- executed = data["choices"][0]["message"].get("executed_tools", [])
609
- if executed:
610
- print(f"[proxy] executed_tools: {len(executed)} tools used")
611
- import re
612
- try:
613
- msg = data["choices"][0]["message"]
614
- if msg.get("content"):
615
- msg["content"] = re.sub(r'【[^】]*】', '', msg["content"]).strip()
616
- except: pass
617
- return JSONResponse(data)
618
- except Exception as e:
619
- print(f"[proxy] search/calc extract error: {e}")
620
- return JSONResponse(data)
621
- if original_tools:
622
- raw_text = ""
623
- try:
624
- raw_text = data["choices"][0]["message"]["content"] or ""
625
- except Exception:
626
- pass
627
- print(f"[proxy] raw ({len(raw_text)}): {raw_text[:300]}...")
628
- agent_output = _extract_agent_output(raw_text)
629
- wrapped = {
630
- "id": data.get("id", "sa"),
631
- "object": "chat.completion",
632
- "created": data.get("created", 0),
633
- "model": "openai/gpt-oss-120b",
634
- "choices": [{
635
- "index": 0,
636
- "message": {
637
- "role": "assistant",
638
- "tool_calls": [{
639
- "id": "call_sa_" + str(data.get("created", 0)),
640
- "type": "function",
641
- "function": {
642
- "name": "AgentOutput",
643
- "arguments": agent_output
644
- }
645
- }]
646
- },
647
- "finish_reason": "tool_calls"
648
- }],
649
- "usage": data.get("usage", {})
650
- }
651
- return JSONResponse(wrapped)
652
- _strip_response_tables(data)
653
- # ═══ 대화 저장 + 기억 추출 (백그라운드) ═══
654
- if _email and sa_mode not in ("dom_action",):
655
- try:
656
- _ai_reply = ""
657
- try: _ai_reply = data.get("choices", [{}])[0].get("message", {}).get("content", "")[:2000]
658
- except: pass
659
- if _last_user:
660
- background_tasks.add_task(save_conversation, _email, "user", _last_user, _sa_url_val, "")
661
- if _ai_reply:
662
- background_tasks.add_task(save_conversation, _email, "assistant", _ai_reply, _sa_url_val, "")
663
- if _last_user and _ai_reply:
664
- background_tasks.add_task(extract_memories_from_conversation, _email, _last_user, _ai_reply, _sa_url_val)
665
- if _last_user and _feat:
666
- background_tasks.add_task(save_task, _email, _feat, _last_user[:500], (_ai_reply or "")[:500], _sa_url_val, "")
667
- except Exception as _me:
668
- print(f"[memory] save error: {_me}")
669
- return JSONResponse(data)
670
- def _extract_agent_output(raw: str) -> str:
671
- text = raw.strip()
672
- if text.startswith("```"):
673
- text = text.split("\n", 1)[-1] if "\n" in text else text[3:]
674
- if text.endswith("```"):
675
- text = text[:-3].strip()
676
- if text.startswith("json"):
677
- text = text[4:].strip()
678
- parsed = None
679
- try:
680
- parsed = json.loads(text)
681
- except json.JSONDecodeError:
682
- start = text.find("{")
683
- end = text.rfind("}")
684
- if start >= 0 and end > start:
685
- try:
686
- parsed = json.loads(text[start:end+1])
687
- except json.JSONDecodeError:
688
- pass
689
- if parsed and isinstance(parsed, dict) and "action" in parsed:
690
- action = parsed["action"]
691
- if isinstance(action, dict) and "done" in action:
692
- done = action["done"]
693
- if isinstance(done, dict) and done.get("text"):
694
- print(f"[proxy] → done: {done['text'][:80]}...")
695
- elif isinstance(done, dict) and not done.get("text"):
696
- fill = parsed.get("memory", "") or parsed.get("next_goal", "")
697
- if fill:
698
- done["text"] = fill
699
- parsed["action"]["done"] = done
700
- for act_name in ["click_element_by_index", "input_text", "scroll",
701
- "select_dropdown_option", "wait", "scroll_horizontally",
702
- "execute_javascript", "ask_user"]:
703
- if act_name in action:
704
- print(f"[proxy] → action: {act_name} {action[act_name]}")
705
- return json.dumps(parsed, ensure_ascii=False)
706
- final_text = raw.strip()
707
- if parsed and isinstance(parsed, dict):
708
- for key in ["text", "content", "answer", "response", "result"]:
709
- if key in parsed and isinstance(parsed[key], str):
710
- final_text = parsed[key]
711
- break
712
- print(f"[proxy] → fallback done: {final_text[:80]}...")
713
- return json.dumps({
714
- "evaluation_previous_goal": "Processed",
715
- "memory": "",
716
- "next_goal": "",
717
- "action": {"done": {"text": final_text or "요청을 처리했습니다.", "success": True}}
718
- }, ensure_ascii=False)
719
- @app.post("/api/search")
720
- async def search_api(request: Request):
721
- try:
722
- body = await request.json()
723
- except Exception:
724
- return JSONResponse({"error": "invalid json"}, status_code=400)
725
- query = body.get("query", "")
726
- if not query:
727
- return JSONResponse({"error": "query required"}, status_code=400)
728
- if not GROQ_API_KEY:
729
- return JSONResponse({"error": "GROQ_API_KEY not set"}, status_code=500)
730
- payload = {
731
- "model": "openai/gpt-oss-120b",
732
- "messages": [
733
- {"role": "system", "content": "반드시 자연스러운 한국어로 답변하라. 핵심만 간결하게. 표(table)는 절대 사용하지 마라. 개조식 서술형으로 작성하라."},
734
- {"role": "user", "content": query}
735
- ],
736
- "tools": [{"type": "browser_search"}],
737
- "tool_choice": "required",
738
- "max_completion_tokens": 4096,
739
- "reasoning_effort": "low",
740
- "stream": False
741
- }
742
- headers = {"Authorization": f"Bearer {GROQ_API_KEY}", "Content-Type": "application/json"}
743
- try:
744
- async with httpx.AsyncClient(timeout=120) as client:
745
- resp = await client.post(GROQ_URL, headers=headers, json=payload)
746
- data = resp.json()
747
- content = data.get("choices", [{}])[0].get("message", {}).get("content", "검색 결과를 가져올 수 없습니다.")
748
- import re
749
- content = re.sub(r'【[^】]*】', '', content).strip()
750
- content = re.sub(r'\s{2,}', ' ', content)
751
- return JSONResponse({"result": content})
752
- except Exception as e:
753
- return JSONResponse({"error": str(e)}, status_code=502)
754
- @app.post("/api/calc")
755
- async def calc_api(request: Request):
756
- try:
757
- body = await request.json()
758
- except Exception:
759
- return JSONResponse({"error": "invalid json"}, status_code=400)
760
- query = body.get("query", "")
761
- if not query:
762
- return JSONResponse({"error": "query required"}, status_code=400)
763
- if not GROQ_API_KEY:
764
- return JSONResponse({"error": "GROQ_API_KEY not set"}, status_code=500)
765
- payload = {
766
- "model": "openai/gpt-oss-120b",
767
- "messages": [
768
- {"role": "system", "content": "계산 결과를 자연스러운 한국어로 설명하라."},
769
- {"role": "user", "content": query}
770
- ],
771
- "tools": [{"type": "code_interpreter"}],
772
- "tool_choice": "required",
773
- "max_completion_tokens": 4096,
774
- "reasoning_effort": "medium",
775
- "stream": False
776
- }
777
- headers = {"Authorization": f"Bearer {GROQ_API_KEY}", "Content-Type": "application/json"}
778
- try:
779
- async with httpx.AsyncClient(timeout=120) as client:
780
- resp = await client.post(GROQ_URL, headers=headers, json=payload)
781
- data = resp.json()
782
- content = data.get("choices", [{}])[0].get("message", {}).get("content", "계산 결과를 가져올 수 없습니다.")
783
- return JSONResponse({"result": content})
784
- except Exception as e:
785
- return JSONResponse({"error": str(e)}, status_code=502)
786
- _log_lock = threading.Lock()
787
- def push_event_sync(event: dict):
788
- line = json.dumps(event, ensure_ascii=False) + "\n"
789
- with _log_lock:
790
- with open(LOG_FILE, "a", encoding="utf-8") as f:
791
- f.write(line)
792
- if not HF_TOKEN:
793
- return
794
- try:
795
- from huggingface_hub import HfApi, hf_hub_download
796
- api = HfApi(token=HF_TOKEN)
797
- date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
798
- path_in_repo = f"logs/{date_str}.jsonl"
799
- existing = ""
800
- try:
801
- local = hf_hub_download(repo_id=DATASET_ID, filename=path_in_repo,
802
- repo_type="dataset", token=HF_TOKEN)
803
- with open(local, encoding="utf-8") as f:
804
- existing = f.read()
805
- except Exception:
806
- pass
807
- api.upload_file(
808
- path_or_fileobj=(existing + line).encode("utf-8"),
809
- path_in_repo=path_in_repo, repo_id=DATASET_ID,
810
- repo_type="dataset", token=HF_TOKEN,
811
- commit_message=f"track {date_str}")
812
- except Exception as e:
813
- print(f"[push_event] {e}")
814
- def calc_stats():
815
- events = []
816
- if os.path.exists(LOG_FILE):
817
- with _log_lock:
818
- with open(LOG_FILE, encoding="utf-8") as f:
819
- for line in f:
820
- line = line.strip()
821
- if line:
822
- try:
823
- events.append(json.loads(line))
824
- except Exception:
825
- pass
826
- if not events:
827
- return {"total_sessions":0,"total_commands":0,"total_installs":0,
828
- "providers":{},"top_domains":[],"daily_active":[],"events_today":0}
829
- sessions=set(); commands=installs=0
830
- providers=Counter(); domains=Counter(); daily=defaultdict(set)
831
- for e in events:
832
- sid=e.get("session_id","")
833
- if sid: sessions.add(sid)
834
- ev=e.get("event","")
835
- if ev=="command": commands+=1
836
- if ev=="install": installs+=1
837
- if ev=="settings_save":
838
- prov=e.get("provider","")
839
- if prov: providers[prov]+=1
840
- domain=e.get("domain","")
841
- if domain: domains[domain]+=1
842
- ts=e.get("ts","")
843
- if ts and sid: daily[ts[:10]].add(sid)
844
- today=datetime.now(timezone.utc).date()
845
- daily_active=[{"date":(today-timedelta(days=i)).isoformat(),
846
- "users":len(daily.get((today-timedelta(days=i)).isoformat(),set()))}
847
- for i in range(13,-1,-1)]
848
- today_str=today.isoformat()
849
- return {"total_sessions":len(sessions),"total_commands":commands,"total_installs":installs,
850
- "providers":dict(providers.most_common(10)),
851
- "top_domains":[{"domain":d,"count":c} for d,c in domains.most_common(10)],
852
- "daily_active":daily_active,
853
- "events_today":sum(1 for e in events if e.get("ts","")[:10]==today_str)}
854
- @app.on_event("startup")
855
- async def startup_init_db():
856
- await init_siteagent_db()
857
- await init_memory_tables()
858
- # ── DB 안정성: 기본 busy_timeout을 파일 레벨로 설정 ──
859
- import sqlite3
860
- try:
861
- conn = sqlite3.connect(SA_DB_PATH, timeout=10)
862
- conn.execute("PRAGMA busy_timeout=5000")
863
- conn.execute("PRAGMA journal_mode=WAL")
864
- conn.execute("PRAGMA synchronous=NORMAL")
865
- conn.execute("PRAGMA wal_autocheckpoint=500")
866
- conn.close()
867
- print("✅ DB PRAGMA set: busy_timeout=5s, WAL, synchronous=NORMAL")
868
- except Exception as e:
869
- print(f"⚠️ DB PRAGMA warning: {e}")
870
- await _init_write_queue()
871
- import asyncio
872
- async def _safe_backup():
873
- """WAL 체크포인트 후 백업 — DB 일관성 보장"""
874
- try:
875
- async with aiosqlite.connect(SA_DB_PATH, timeout=15.0) as db:
876
- await db.execute("PRAGMA busy_timeout=5000")
877
- await db.execute("PRAGMA wal_checkpoint(PASSIVE)")
878
- except Exception as e:
879
- print(f"[backup] WAL checkpoint warning: {e}")
880
- try:
881
- await _sa_backup_db()
882
- except Exception as e:
883
- print(f"[backup] error: {e}")
884
- async def _periodic_backup():
885
- await asyncio.sleep(30)
886
- while True:
887
- try:
888
- await _safe_backup()
889
- except Exception as e:
890
- print(f"⚠️ Periodic backup error: {e}")
891
- await asyncio.sleep(180)
892
- asyncio.create_task(_periodic_backup())
893
- @app.on_event("startup")
894
- def restore_logs_from_hf():
895
- if not HF_TOKEN:
896
- return
897
- try:
898
- from huggingface_hub import HfApi, hf_hub_download
899
- api = HfApi(token=HF_TOKEN)
900
- files = [
901
- f.rfilename for f in api.list_repo_tree(
902
- repo_id=DATASET_ID, repo_type="dataset", token=HF_TOKEN
903
- )
904
- if f.rfilename.startswith("logs/") and f.rfilename.endswith(".jsonl")
905
- ]
906
- restored = 0
907
- with open(LOG_FILE, "a", encoding="utf-8") as out:
908
- for fname in sorted(files):
909
- try:
910
- local = hf_hub_download(
911
- repo_id=DATASET_ID, filename=fname,
912
- repo_type="dataset", token=HF_TOKEN
913
- )
914
- with open(local, encoding="utf-8") as f:
915
- for line in f:
916
- line = line.strip()
917
- if line:
918
- out.write(line + "\n")
919
- restored += 1
920
- except Exception as e:
921
- print(f"[restore] skip {fname}: {e}")
922
- print(f"[restore] {restored} events restored from HF dataset")
923
- except Exception as e:
924
- print(f"[restore] failed: {e}")
925
- @app.post("/api/track")
926
- async def track(request: Request, background_tasks: BackgroundTasks):
927
- try:
928
- body = await request.json()
929
- except Exception:
930
- return JSONResponse({"ok": False}, status_code=400)
931
- allowed = {"event","session_id","provider","domain","ts","browser","os","lang"}
932
- event = {k:v for k,v in body.items() if k in allowed}
933
- event["ts"] = event.get("ts") or datetime.now(timezone.utc).isoformat()
934
- background_tasks.add_task(push_event_sync, event)
935
- return JSONResponse({"ok": True})
936
- @app.post("/api/waitlist")
937
- async def waitlist_signup(request: Request):
938
- import time
939
- try:
940
- body = await request.json()
941
- email = body.get("email", "").strip().lower()
942
- if not email or "@" not in email:
943
- return JSONResponse({"error": "유효한 이메일을 입력해주세요."}, status_code=400)
944
- name = body.get("name", "").strip()[:50]
945
- interest = body.get("interest", "").strip()[:100]
946
- source = body.get("source", "landing")[:20]
947
- existing = await _db_read_one("SELECT id FROM waitlist WHERE email=?", (email,))
948
- if existing:
949
- return {"status": "already", "message": "이미 등록된 이메일입니다. 곧 연락드리겠습니다!"}
950
- await _db_write(
951
- "INSERT INTO waitlist (email,name,interest,source,created_at) VALUES (?,?,?,?,?)",
952
- (email, name, interest, source, time.time())
953
- )
954
- print(f"[waitlist] ✅ New signup: {email}")
955
- return {"status": "ok", "message": "등록 완료!"}
956
- except Exception as e:
957
- return JSONResponse({"error": str(e)[:100]}, status_code=500)
958
- @app.get("/api/waitlist/count")
959
- async def waitlist_count(request: Request):
960
- admin = request.query_params.get("admin", "")
961
- if admin != ADMIN_EMAIL:
962
- return JSONResponse({"error": "unauthorized"}, status_code=403)
963
- count = await _db_read_one("SELECT COUNT(*) as cnt FROM waitlist")
964
- return {"count": dict(count)["cnt"] if count else 0}
965
- @app.get("/api/admin/waitlist")
966
- async def admin_waitlist(request: Request):
967
- admin = request.query_params.get("admin", "")
968
- if admin != ADMIN_EMAIL:
969
- return JSONResponse({"error": "unauthorized"}, status_code=403)
970
- rows = await _db_read("SELECT id,email,name,interest,source,created_at FROM waitlist ORDER BY created_at DESC")
971
- return {"waitlist": [dict(r) for r in rows]}
972
- @app.post("/api/user/register")
973
- async def user_register(request: Request):
974
- import time
975
- try:
976
- body = await request.json()
977
- email = body.get("email", "").strip().lower()
978
- if not email or "@" not in email:
979
- return JSONResponse({"error": "invalid email"}, status_code=400)
980
- existing = await _db_read_one("SELECT id,role FROM users WHERE email=?", (email,))
981
- if existing:
982
- await _db_write("UPDATE users SET visit_count=visit_count+1, last_seen_at=? WHERE email=?",
983
- (time.time(), email))
984
- return {"status": "exists", "role": dict(existing)["role"]}
985
- import random
986
- animals = ["🐱고양이","🐶강아지","🦊여우","🐻곰","🐼팬더","🐨코알라","🦁사자","🐯호랑이","🐰토끼","🦝너구리","🐸개구리","🦉올빼미","🦋나비","🐬돌고래","🦈상어","🐧��귄","🦜앵무새","🐢거북이"]
987
- nick = "익명" + random.choice(animals) + str(random.randint(10,99))
988
- role = "admin" if email == ADMIN_EMAIL else "user"
989
- await _db_write("INSERT OR IGNORE INTO users (email,nickname,role,visit_count,last_seen_at,created_at) VALUES (?,?,?,1,?,?)",
990
- (email, nick, role, time.time(), time.time()))
991
- _asyncio.create_task(_sa_backup_db())
992
- return {"status": "created", "nickname": nick, "role": role}
993
- except Exception as e:
994
- return JSONResponse({"error": str(e)[:100]}, status_code=500)
995
- @app.post("/api/user/log")
996
- async def user_log_activity(request: Request):
997
- import time, hashlib
998
- try:
999
- body = await request.json()
1000
- email = body.get("email", "").strip().lower()
1001
- if not email: return JSONResponse({"error": "email required"}, status_code=400)
1002
- action = body.get("action", "")
1003
- url = body.get("url", "")
1004
- title = body.get("title", "")
1005
- input_text = _sanitize_text(body.get("input_text", ""))
1006
- feature = body.get("feature", "")
1007
- url_hash = hashlib.sha256(url.encode()).hexdigest()[:16] if url else ""
1008
- domain = ""
1009
- if url:
1010
- try:
1011
- from urllib.parse import urlparse
1012
- domain = urlparse(url).netloc
1013
- except: pass
1014
- now = time.time()
1015
- if action == "visit" and url:
1016
- await _db_write("INSERT INTO page_visits (email,url_hash,url,title,domain,visit_count,last_visited_at,created_at) VALUES (?,?,?,?,?,1,?,?) ON CONFLICT(email,url_hash) DO UPDATE SET visit_count=visit_count+1, title=excluded.title, last_visited_at=excluded.last_visited_at",
1017
- (email, url_hash, url[:500], title[:200] if title else None, domain, now, now))
1018
- elif action == "input" and input_text:
1019
- await _db_write("INSERT INTO user_inputs (email,url_hash,url,input_type,input_text,feature,result_length,created_at) VALUES (?,?,?,?,?,?,?,?)",
1020
- (email, url_hash, url[:500] if url else None, body.get("input_type","chat"), input_text[:1000], feature, body.get("result_length",0), now))
1021
- if feature:
1022
- await _db_write("INSERT INTO feature_usage (email,feature,use_count,last_used_at) VALUES (?,?,1,?) ON CONFLICT(email,feature) DO UPDATE SET use_count=use_count+1, last_used_at=excluded.last_used_at",
1023
- (email, feature, now))
1024
- return {"status": "ok"}
1025
- except Exception as e:
1026
- print(f"[user-log] error: {e}")
1027
- return JSONResponse({"error": str(e)[:100]}, status_code=500)
1028
- @app.get("/api/user/profile")
1029
- async def user_profile(email: str = ""):
1030
- email = email.strip().lower()
1031
- if not email: return JSONResponse({"error": "email required"}, status_code=400)
1032
- user = await _db_read_one("SELECT * FROM users WHERE email=?", (email,))
1033
- if not user: return JSONResponse({"error": "user not found"}, status_code=404)
1034
- user_dict = dict(user)
1035
- visits = await _db_read("SELECT domain, SUM(visit_count) as cnt FROM page_visits WHERE email=? GROUP BY domain ORDER BY cnt DESC LIMIT 10", (email,))
1036
- user_dict["top_domains"] = [{"domain": dict(v)["domain"], "count": dict(v)["cnt"]} for v in visits]
1037
- usage = await _db_read("SELECT feature, use_count FROM feature_usage WHERE email=? ORDER BY use_count DESC", (email,))
1038
- user_dict["feature_usage"] = [{"feature": dict(u)["feature"], "count": dict(u)["use_count"]} for u in usage]
1039
- inputs = await _db_read("SELECT input_text, feature, url, created_at FROM user_inputs WHERE email=? ORDER BY created_at DESC LIMIT 10", (email,))
1040
- user_dict["recent_inputs"] = [dict(i) for i in inputs]
1041
- return user_dict
1042
- import time as _time
1043
- _session_save_cache = {} # email → (timestamp, body)
1044
- _SESSION_THROTTLE = 5 # 초
1045
- @app.post("/api/session/save")
1046
- async def session_save(request: Request):
1047
- try:
1048
- body = await request.json()
1049
- email = (body.get("email") or "").strip().lower()
1050
- if not email:
1051
- return JSONResponse({"ok": False}, status_code=400)
1052
- # 스로틀: 같은 이메일은 5초에 1번만 실제 DB 기록
1053
- import time as _t
1054
- now = _t.time()
1055
- last = _session_save_cache.get(email, 0)
1056
- if now - last < _SESSION_THROTTLE:
1057
- return {"ok": True, "throttled": True}
1058
- _session_save_cache[email] = now
1059
- chat = (body.get("chat") or "")[:50000]
1060
- cmd = (body.get("cmd") or "")[:500]
1061
- url = (body.get("url") or "")[:2000]
1062
- title = (body.get("title") or "")[:500]
1063
- await _db_write("""
1064
- INSERT INTO sessions (email, chat, cmd, url, title, updated_at)
1065
- VALUES (?, ?, ?, ?, ?, ?)
1066
- ON CONFLICT(email) DO UPDATE SET
1067
- chat=excluded.chat, cmd=excluded.cmd,
1068
- url=excluded.url, title=excluded.title,
1069
- updated_at=excluded.updated_at
1070
- """, (email, chat, cmd, url, title, now))
1071
- return {"ok": True}
1072
- except Exception as e:
1073
- return JSONResponse({"ok": False, "error": str(e)[:100]}, status_code=500)
1074
- @app.get("/api/session/load")
1075
- async def session_load(email: str = ""):
1076
- email = email.strip().lower()
1077
- if not email:
1078
- return JSONResponse({"ok": False}, status_code=400)
1079
- row = await _db_read_one(
1080
- "SELECT chat, cmd, url, title, updated_at FROM sessions WHERE email=?", (email,))
1081
- if not row:
1082
- return {"ok": True, "chat": "", "cmd": "", "url": "", "title": "", "ts": 0}
1083
- d = dict(row)
1084
- return {"ok": True, "chat": d.get("chat",""), "cmd": d.get("cmd",""),
1085
- "url": d.get("url",""), "title": d.get("title",""),
1086
- "ts": d.get("updated_at", 0)}
1087
- CLIP_BUCKET = "ginigen-ai/siteagent"
1088
- from clips import router as clips_router
1089
- from imagine import router as imagine_router
1090
- from services import router as services_router
1091
- from admin import router as admin_router
1092
- from marl_routes import router as marl_router
1093
- from saju_routes import router as saju_router
1094
- from face_routes import router as face_router
1095
- from memory import router as memory_router, init_memory_tables, save_conversation, build_memory_context, extract_memories_from_conversation, save_task
1096
- app.include_router(clips_router)
1097
- app.include_router(imagine_router)
1098
- app.include_router(services_router)
1099
- app.include_router(admin_router)
1100
- app.include_router(marl_router)
1101
- app.include_router(saju_router)
1102
- app.include_router(face_router)
1103
- app.include_router(memory_router)
1104
- @app.get("/api/stats")
1105
- async def stats():
1106
- return JSONResponse(calc_stats())
1107
- @app.get("/api/marl-test")
1108
- async def marl_test():
1109
- _init_marl()
1110
- result = {"marl_available": MARL_AVAILABLE, "groq_key_set": bool(GROQ_API_KEY)}
1111
- if MARL_AVAILABLE:
1112
- try:
1113
- config = MarlConfig(mode="insight", return_final_only=True, language="ko", budget_scale=0.5)
1114
- ml = Marl(call_fn=_groq_call_fn, config=config)
1115
- import asyncio
1116
- r = await asyncio.to_thread(ml.run, "1+1=? 한 줄로 답하라.", "한국어로 답변")
1117
- result["test_answer"] = r.answer[:200] if r.answer else "EMPTY"
1118
- result["test_elapsed"] = round(r.elapsed, 2)
1119
- result["test_ok"] = bool(r.answer and not r.answer.startswith("[ERROR"))
1120
- except Exception as e:
1121
- result["test_error"] = str(e)
1122
- result["test_ok"] = False
1123
- else:
1124
- result["test_ok"] = False
1125
- result["test_error"] = "MARL not available"
1126
- return JSONResponse(result)
1127
- # ═══════════════════════════════════════════════════════
1128
- # 🔐 시크릿 메신저 — E2E 암호화, DB 암호문 보관, 포렌식 차단
1129
- # ═══════════════════════════════════════════════════════
1130
- import secrets as _sec_secrets
1131
- _sec_streams = {} # email_hash → [asyncio.Queue] (SSE)
1132
- _sec_pubcache = {} # email_hash → pubkey_jwk (메모리 캐시)
1133
- _SEC_TTL = 604800 # 7일
1134
-
1135
- @app.post("/api/sec/register")
1136
- async def sec_register(request: Request):
1137
- """공개키 등록 → DB + 메모리 캐시"""
1138
- try: body = await request.json()
1139
- except: return JSONResponse({"error": "invalid json"}, status_code=400)
1140
- email_hash = body.get("email_hash", "").strip()
1141
- pubkey_jwk = body.get("pubkey_jwk")
1142
- if not email_hash or not pubkey_jwk or len(email_hash) != 64:
1143
- return JSONResponse({"error": "email_hash(sha256) and pubkey_jwk required"}, status_code=400)
1144
- import time, json
1145
- _sec_pubcache[email_hash] = pubkey_jwk
1146
- await _db_write("""INSERT INTO sec_keys (email_hash, pubkey_jwk, registered_at)
1147
- VALUES (?,?,?) ON CONFLICT(email_hash) DO UPDATE SET pubkey_jwk=excluded.pubkey_jwk, registered_at=excluded.registered_at""",
1148
- (email_hash, json.dumps(pubkey_jwk), time.time()))
1149
- total = await _db_read_one("SELECT COUNT(*) as c FROM sec_keys")
1150
- n = total["c"] if total else 0
1151
- print(f"[sec] 🔑 registered: {email_hash[:12]}... (total: {n})")
1152
- return {"ok": True, "total_users": n}
1153
-
1154
- @app.get("/api/sec/pubkey")
1155
- async def sec_get_pubkey(target: str = ""):
1156
- """공개키 조회 → 메모리 캐시 → DB 폴백"""
1157
- target = target.strip()
1158
- if not target: return JSONResponse({"error": "target required"}, status_code=400)
1159
- if target in _sec_pubcache:
1160
- return {"ok": True, "pubkey_jwk": _sec_pubcache[target]}
1161
- import json
1162
- row = await _db_read_one("SELECT pubkey_jwk FROM sec_keys WHERE email_hash=?", (target,))
1163
- if not row:
1164
- print(f"[sec] ❌ pubkey MISS: {target[:12]}...")
1165
- return JSONResponse({"error": "user not found"}, status_code=404)
1166
- jwk = json.loads(row["pubkey_jwk"])
1167
- _sec_pubcache[target] = jwk
1168
- return {"ok": True, "pubkey_jwk": jwk}
1169
-
1170
- @app.post("/api/sec/send")
1171
- async def sec_send(request: Request):
1172
- """메시지 릴레이 → DB 저장 + SSE 푸시"""
1173
- try: body = await request.json()
1174
- except: return JSONResponse({"error": "invalid json"}, status_code=400)
1175
- to_hash = body.get("to", "").strip()
1176
- from_hash = body.get("from", "").strip()
1177
- ciphertext = body.get("ciphertext", "")
1178
- iv = body.get("iv", "")
1179
- group_id = body.get("group_id", "")
1180
- if not to_hash or not from_hash or not ciphertext or not iv:
1181
- return JSONResponse({"error": "to, from, ciphertext, iv required"}, status_code=400)
1182
- # 수신자 등록 확인 (캐시 → DB)
1183
- if to_hash not in _sec_pubcache:
1184
- row = await _db_read_one("SELECT email_hash FROM sec_keys WHERE email_hash=?", (to_hash,))
1185
- if not row:
1186
- return JSONResponse({"error": "recipient not registered"}, status_code=404)
1187
- import time
1188
- msg_id = _sec_secrets.token_hex(8)
1189
- ts = time.time()
1190
- # DB에 암호문 저장 (서버 재시작해도 보존)
1191
- await _db_write("INSERT OR IGNORE INTO sec_messages (id,from_hash,to_hash,ciphertext,iv,group_id,ts) VALUES (?,?,?,?,?,?,?)",
1192
- (msg_id, from_hash, to_hash, ciphertext, iv, group_id, ts))
1193
- print(f"[sec] ✉️ {from_hash[:8]}→{to_hash[:8]} ({len(ciphertext)}B) id={msg_id}")
1194
- # SSE 실시간 푸시
1195
- if to_hash in _sec_streams:
1196
- import json as _sj
1197
- payload = _sj.dumps({"id":msg_id,"from":from_hash,"ciphertext":ciphertext,"iv":iv,"group_id":group_id,"ts":ts})
1198
- for q in _sec_streams[to_hash]:
1199
- try: q.put_nowait(payload)
1200
- except: pass
1201
- return {"ok": True, "id": msg_id, "ts": ts}
1202
-
1203
- @app.get("/api/sec/recv")
1204
- async def sec_recv(email_hash: str = "", since: float = 0):
1205
- """수신 메시지 조회 (DB 기반, 서버 재시작해도 보존)"""
1206
- email_hash = email_hash.strip()
1207
- if not email_hash: return {"ok": True, "messages": []}
1208
- rows = await _db_read(
1209
- "SELECT id, from_hash, ciphertext, iv, group_id, ts FROM sec_messages WHERE to_hash=? AND ts>? ORDER BY ts ASC LIMIT 200",
1210
- (email_hash, since))
1211
- msgs = [{"id":r["id"],"from":r["from_hash"],"ciphertext":r["ciphertext"],"iv":r["iv"],"group_id":r["group_id"],"ts":r["ts"]} for r in rows]
1212
- if msgs:
1213
- print(f"[sec] 📨 {email_hash[:8]}← {len(msgs)} msgs")
1214
- return {"ok": True, "messages": msgs}
1215
-
1216
- @app.get("/api/sec/stream")
1217
- async def sec_stream(email_hash: str = ""):
1218
- """SSE 실시간 스트림"""
1219
- import asyncio, json as _sj
1220
- email_hash = email_hash.strip()
1221
- if not email_hash: return JSONResponse({"error": "email_hash required"}, status_code=400)
1222
- q = asyncio.Queue(maxsize=50)
1223
- if email_hash not in _sec_streams: _sec_streams[email_hash] = []
1224
- _sec_streams[email_hash].append(q)
1225
- print(f"[sec] 📡 SSE connected: {email_hash[:8]}...")
1226
- async def event_gen():
1227
- try:
1228
- yield f"data: {_sj.dumps({'type':'connected'})}\n\n"
1229
- while True:
1230
- try:
1231
- msg = await asyncio.wait_for(q.get(), timeout=25)
1232
- yield f"data: {msg}\n\n"
1233
- except asyncio.TimeoutError:
1234
- yield ": keepalive\n\n"
1235
- except asyncio.CancelledError: pass
1236
- finally:
1237
- if email_hash in _sec_streams and q in _sec_streams[email_hash]:
1238
- _sec_streams[email_hash].remove(q)
1239
- print(f"[sec] 📡 SSE disconnected: {email_hash[:8]}...")
1240
- from starlette.responses import StreamingResponse
1241
- return StreamingResponse(event_gen(), media_type="text/event-stream",
1242
- headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no"})
1243
-
1244
- @app.get("/api/sec/status")
1245
- async def sec_status(target: str = ""):
1246
- """온라인 상태 확인"""
1247
- target = target.strip()
1248
- registered = target in _sec_pubcache
1249
- if not registered:
1250
- row = await _db_read_one("SELECT email_hash FROM sec_keys WHERE email_hash=?", (target,))
1251
- registered = bool(row)
1252
- online = target in _sec_streams and len(_sec_streams[target]) > 0
1253
- return {"ok": True, "registered": registered, "online": online}
1254
-
1255
- @app.get("/api/sec/contacts")
1256
- async def sec_contacts(email_hash: str = ""):
1257
- """대화 상대 목록 (DB에서 실제 대화한 상대 조회)"""
1258
- email_hash = email_hash.strip()
1259
- if not email_hash: return {"ok": True, "contacts": []}
1260
- rows = await _db_read("""
1261
- SELECT partner, MAX(ts) as last_ts, COUNT(*) as msg_count FROM (
1262
- SELECT from_hash as partner, ts FROM sec_messages WHERE to_hash=?
1263
- UNION ALL
1264
- SELECT to_hash as partner, ts FROM sec_messages WHERE from_hash=?
1265
- ) GROUP BY partner ORDER BY last_ts DESC""", (email_hash, email_hash))
1266
- contacts = []
1267
- for r in rows:
1268
- p = r["partner"]
1269
- online = p in _sec_streams and len(_sec_streams.get(p,[])) > 0
1270
- registered = p in _sec_pubcache
1271
- contacts.append({"hash":p, "online":online, "registered":registered,
1272
- "last_ts":r["last_ts"], "msg_count":r["msg_count"]})
1273
- return {"ok": True, "contacts": contacts}
1274
-
1275
- @app.post("/api/sec/group/create")
1276
- async def sec_group_create(request: Request):
1277
- """그룹 생성"""
1278
- try: body = await request.json()
1279
- except: return JSONResponse({"error": "invalid json"}, status_code=400)
1280
- group_id = body.get("group_id", _sec_secrets.token_hex(16))
1281
- return {"ok": True, "group_id": group_id}
1282
-
1283
- @app.post("/api/sec/group/send")
1284
- async def sec_group_send(request: Request):
1285
- """그룹 메시지 — 각 멤버에게 개별 전송"""
1286
- try: body = await request.json()
1287
- except: return JSONResponse({"error": "invalid json"}, status_code=400)
1288
- group_id = body.get("group_id", "")
1289
- from_hash = body.get("from", "")
1290
- per_member = body.get("per_member", {})
1291
- if not group_id or not from_hash or not per_member:
1292
- return JSONResponse({"error": "group_id, from, per_member required"}, status_code=400)
1293
- import time
1294
- msg_id = _sec_secrets.token_hex(8)
1295
- ts = time.time()
1296
- delivered = 0
1297
- for member_hash, encrypted in per_member.items():
1298
- if member_hash == from_hash: continue
1299
- await _db_write("INSERT OR IGNORE INTO sec_messages (id,from_hash,to_hash,ciphertext,iv,group_id,ts) VALUES (?,?,?,?,?,?,?)",
1300
- (msg_id + "_" + member_hash[:8], from_hash, member_hash, encrypted.get("ciphertext",""), encrypted.get("iv",""), group_id, ts))
1301
- # SSE 푸시
1302
- if member_hash in _sec_streams:
1303
- import json as _sj
1304
- payload = _sj.dumps({"id":msg_id,"from":from_hash,"ciphertext":encrypted.get("ciphertext",""),"iv":encrypted.get("iv",""),"group_id":group_id,"ts":ts})
1305
- for q in _sec_streams[member_hash]:
1306
- try: q.put_nowait(payload)
1307
- except: pass
1308
- delivered += 1
1309
- return {"ok": True, "id": msg_id, "delivered": delivered}
1310
-
1311
- # ── 🔐 시크릿AI 메신저 — 공유 랜딩 페이지 ──
1312
- _EDGE_URL = "https://microsoftedge.microsoft.com/addons/detail/siteagent-ai-web-assist/okphfpmfmmhkmhmjgbemlniahoobhfib"
1313
-
1314
- @app.get("/share")
1315
- async def share_landing():
1316
- """시크릿AI 메신저 소개 + 설치 랜딩 페이지"""
1317
- html = f"""<!DOCTYPE html><html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
1318
- <title>🔐 시크릿AI 메신저</title>
1319
- <meta property="og:title" content="🔐 시크릿AI 메신저 — 포렌식 불가 E2E 암호화">
1320
- <meta property="og:description" content="화이트해커가 만든 비밀 메신저. 설치하고 비밀 대화를 시작하세요.">
1321
- <style>
1322
- *{{margin:0;padding:0;box-sizing:border-box}}
1323
- body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0a0a0f;color:#e0e0e0;min-height:100vh;display:flex;align-items:center;justify-content:center}}
1324
- .card{{background:#12121a;border:1px solid #1e1e2e;border-radius:20px;padding:40px 32px;max-width:420px;width:90%;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,.5)}}
1325
- .logo{{font-size:56px;margin-bottom:16px}}
1326
- h1{{font-size:24px;font-weight:800;color:#00d4aa;margin-bottom:8px}}
1327
- .sub{{font-size:13px;color:#888;line-height:1.7;margin-bottom:24px}}
1328
- .features{{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:24px;text-align:left}}
1329
- .feat{{background:rgba(0,212,170,.05);border:1px solid #1e1e2e;border-radius:10px;padding:10px 12px}}
1330
- .feat b{{color:#00d4aa;font-size:11px;display:block;margin-bottom:2px}}
1331
- .feat span{{color:#888;font-size:9px;line-height:1.4}}
1332
- .btn{{display:block;width:100%;padding:14px;border:none;border-radius:12px;font-size:14px;font-weight:700;cursor:pointer;text-decoration:none;margin-bottom:10px;transition:opacity .15s}}
1333
- .btn:hover{{opacity:.85}}
1334
- .btn-edge{{background:linear-gradient(135deg,#0078d4,#00bcf2);color:#fff}}
1335
- .btn-web{{background:linear-gradient(135deg,#00d4aa,#007a6a);color:#fff}}
1336
- .steps{{text-align:left;margin-top:20px;padding:16px;background:rgba(0,212,170,.05);border-radius:12px;font-size:12px;line-height:2;color:#aaa}}
1337
- .steps b{{color:#00d4aa}}
1338
- .footer{{margin-top:20px;font-size:9px;color:#444;line-height:1.6}}
1339
- </style></head><body><div class="card">
1340
- <div class="logo">🔐</div>
1341
- <h1>시크릿AI 메신저</h1>
1342
- <div class="sub">화이트해커가 만든 포렌식 불가 메신저<br>E2E 암호화 · 메모리 전용 · 서버 무저장</div>
1343
- <div class="features">
1344
- <div class="feat"><b>🔒 E2E 암호화</b><span>AES-256-GCM + ECDH</span></div>
1345
- <div class="feat"><b>🕵️ 포렌식 차단</b><span>디스크 기록 ZERO</span></div>
1346
- <div class="feat"><b>🤖 AI 내장</b><span>웹 요약·번역·분석</span></div>
1347
- <div class="feat"><b>👻 은닉</b><span>AI 도우미로 위장</span></div>
1348
- </div>
1349
- <a href="{_EDGE_URL}" class="btn btn-edge" target="_blank">🌐 Edge 확장 설치</a>
1350
- <a href="https://ginigen-ai-siteagent.hf.space" class="btn btn-web" target="_blank">🚀 웹에서 바로 시작</a>
1351
- <div class="steps">
1352
- <b>3단계로 시작:</b><br>
1353
- 1️⃣ SiteAgent 설치 → 아무 웹페이지에서 패널 열기<br>
1354
- 2️⃣ <b>🔐 시크릿AI</b> 버튼 → 이메일 + 패스프레이즈로 잠금 해제<br>
1355
- 3️⃣ <b>+ 대화</b> → 상대방 이메일 입력 → 비밀 대�� 시작!
1356
- </div>
1357
- <div class="footer">
1358
- 🔐 대법원 특수감정인 출신 화이트해커가 설계<br>
1359
- AES-256-GCM · ECDH P-256 · Web Crypto API<br>
1360
- Powered by Ginigen AI · SiteAgent
1361
- </div></div></body></html>"""
1362
- return Response(html, media_type="text/html")
1363
-
1364
- @app.get("/secure-pageagent.extend.js")
1365
- async def serve_js():
1366
- if _PAGE_AGENT_JS:
1367
- return Response(_PAGE_AGENT_JS, media_type="application/javascript", headers={"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0"})
1368
- return JSONResponse({"error": "js not found"}, status_code=404)
1369
- @app.get("/style.css")
1370
- async def serve_css():
1371
- return Response("", media_type="text/css")
1372
- @app.get("/{img_name}.png")
1373
- async def serve_img(img_name: str):
1374
- """설치 가이드 이미지 서빙"""
1375
- if not img_name.isalnum() or len(img_name) > 10:
1376
- return JSONResponse({"error": "not found"}, status_code=404)
1377
- for d in [Path(__file__).parent, Path("."), Path("/home/user/app")]:
1378
- f = d / f"{img_name}.png"
1379
- try:
1380
- if f.exists():
1381
- return Response(f.read_bytes(), media_type="image/png", headers={"Cache-Control": "public, max-age=86400"})
1382
- except: continue
1383
- return JSONResponse({"error": "not found"}, status_code=404)
1384
- @app.get("/siteagent-extension-v7.zip")
1385
- async def serve_extension_zip():
1386
- """확장 프로그램 ZIP 다운로드"""
1387
- for d in [Path(__file__).parent, Path("."), Path("/home/user/app")]:
1388
- f = d / "siteagent-extension-v7.zip"
1389
- try:
1390
- if f.exists():
1391
- return Response(f.read_bytes(), media_type="application/zip",
1392
- headers={"Content-Disposition": "attachment; filename=siteagent-extension-v7.zip", "Cache-Control": "no-cache"})
1393
- except: continue
1394
- return JSONResponse({"error": "zip not found"}, status_code=404)
1395
- @app.get("/")
1396
- @app.get("/index.html")
1397
- async def serve_index():
1398
- path = "index.html"
1399
- if not os.path.isfile(path):
1400
- return JSONResponse({"error": "index.html not found"}, status_code=404)
1401
- return FileResponse(path, media_type="text/html")
1402
- uvicorn.run(app, host="0.0.0.0", port=7860)
1403
-
 
1
  import os
2
+ import sys
3
+
4
+ def main():
5
+ try:
6
+ # Get the code from secrets
7
+ code = os.environ.get("MAIN_CODE")
8
+
9
+ if not code:
10
+ # Fallback: create a simple error display
11
+ import gradio as gr
12
+ with gr.Blocks() as demo:
13
+ gr.Markdown("# ⚠️ Error")
14
+ gr.Markdown("The application code wasn't found in secrets. Please add the MAIN_CODE secret.")
15
+ demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  return
17
+
18
+ # Execute the code directly
19
+ exec(compile(code, '<string>', 'exec'), globals())
20
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  except Exception as e:
22
+ import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  import traceback
24
+ error_msg = traceback.format_exc()
25
+
26
+ with gr.Blocks() as demo:
27
+ gr.Markdown("# ⚠️ Error Loading Application")
28
+ gr.Markdown(f"**Error:** {str(e)}")
29
+ gr.Code(error_msg, language="python", label="Traceback")
30
+ demo.launch()
31
+
32
+ if __name__ == "__main__":
33
+ main()