BachDaThan commited on
Commit
6c2e6a4
·
verified ·
1 Parent(s): 6e09d46

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +276 -575
  2. requirements.txt +2 -12
app.py CHANGED
@@ -1,618 +1,319 @@
1
  """
2
- yui_worker_app.py — YUI SATELLITE WORKER v2.0
3
- ==============================================
4
- Dùng cho 4 HF Space phụ.
5
-
6
- KHÔNG CẦN NHẬP KEY TAY:
7
- Worker tự gọi /api/get-keys từ space chính khi khởi động.
8
- Chỉ cần set 2 env vars:
9
- MAIN_SPACE_URL = https://bachdathan-ural-ai.hf.space
10
- WORKER_SECRET = [cùng giá trị với space chính]
11
- WORKER_ROLE = code | strong | creative | backup
12
- SELF_URL = https://[username]-[space-name].hf.space (để self-ping)
13
- WORKER_URLS = url1,url2,url3,url4 (để cross-ping, tùy chọn)
14
-
15
- TÓM TẮT TÍNH NĂNG:
16
- - Chạy code qua Piston (no key) + Judge0 + local sandbox
17
- - AI chain theo role (code/strong/creative/backup)
18
- - Stealth ping khác interval theo role
19
- - Cross-ping workers khác để giữ cluster alive
20
- - UptimeRobot ping /health endpoint
21
- """
22
 
23
- import os, time, random, threading, asyncio
24
- import httpx
25
- from fastapi import FastAPI, Request, HTTPException
 
 
 
 
 
26
  from fastapi.responses import JSONResponse
27
- import uvicorn
28
-
29
- # ── ENV ───────────────────────────────────────────────────────────
30
- MAIN_SPACE_URL = os.getenv("MAIN_SPACE_URL", "").rstrip("/")
31
- WORKER_SECRET = os.getenv("WORKER_SECRET", "")
32
- WORKER_ROLE = os.getenv("WORKER_ROLE", "backup")
33
- SELF_URL = os.getenv("SELF_URL", "").rstrip("/")
34
- WORKER_URLS = [u.strip() for u in os.getenv("WORKER_URLS","").split(",") if u.strip()]
35
-
36
- # ── AUTO-FETCH KEYS ───────────────────────────────────────────────
37
- # Worker tự lấy key từ space chính, không cần nhập tay
38
- _KEYS: dict = {
39
- "gemini": [], "openrouter": [], "cerebras": [],
40
- "mistral": [], "cohere": [], "tavily": [],
41
- }
42
- _keys_loaded = threading.Event()
43
- _keys_lock = threading.Lock()
44
 
45
  def _fetch_keys_sync():
46
- """Gọi /api/get-keys từ main space để lấy keys."""
47
- global _KEYS
48
- if not MAIN_SPACE_URL or not WORKER_SECRET:
49
- print(f"[WORKER-{WORKER_ROLE}] ⚠️ MAIN_SPACE_URL hoặc WORKER_SECRET chưa set!")
50
- _keys_loaded.set()
51
- return
52
-
53
- for attempt in range(5):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  try:
55
- import requests as _rq
56
- r = _rq.post(
57
- f"{MAIN_SPACE_URL}/api/get-keys",
58
- json={"secret": WORKER_SECRET, "role": WORKER_ROLE},
59
- timeout=15
 
 
 
 
 
 
 
 
 
 
60
  )
61
  if r.status_code == 200:
62
- data = r.json()
63
- with _keys_lock:
64
- for k in _KEYS:
65
- v = data.get(k, [])
66
- if isinstance(v, list) and v:
67
- _KEYS[k] = v
68
- print(f"[WORKER-{WORKER_ROLE}] ✅ Keys loaded: "
69
- f"G×{len(_KEYS['gemini'])} OR×{len(_KEYS['openrouter'])} "
70
- f"CB×{len(_KEYS['cerebras'])} M×{len(_KEYS['mistral'])}")
71
- _keys_loaded.set()
72
- return
73
- else:
74
- print(f"[WORKER-{WORKER_ROLE}] ⚠️ get-keys HTTP {r.status_code} — retry {attempt+1}/5")
75
  except Exception as e:
76
- print(f"[WORKER-{WORKER_ROLE}] ⚠️ get-keys lỗi: {e} — retry {attempt+1}/5")
77
- time.sleep(10 * (attempt + 1))
78
 
79
- print(f"[WORKER-{WORKER_ROLE}] ❌ Không lấy được keys sau 5 lần thử")
80
- _keys_loaded.set()
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- def _key_refresh_loop():
84
- """Cập nhật keys mỗi 6 giờ (key mới được thêm sẽ tự sync)."""
85
- time.sleep(21600) # 6 giờ sau lần đầu
86
- while True:
87
- _fetch_keys_sync()
88
- time.sleep(21600)
89
-
90
-
91
- # ── HELPERS ───────────────────────────────────────────────────────
92
- def _rkey(name: str):
93
- with _keys_lock:
94
- pool = _KEYS.get(name, [])
95
- return random.choice(pool) if pool else None
96
-
97
- def _quota_ok(name: str) -> bool:
98
- return time.time() > _quota_hits.get(name, 0)
99
-
100
- _quota_hits: dict = {}
101
- def _quota_hit(name: str, secs: int = 60):
102
- _quota_hits[name] = time.time() + secs
103
- print(f"[WORKER-{WORKER_ROLE}] ⏳ {name} cooldown {secs}s")
104
-
105
-
106
- # ── ROLE CONFIG ───────────────────────────────────────────────────
107
- ROLE_CHAIN = {
108
- "code": [("mistral","codestral-latest"), ("openrouter","deepseek/deepseek-v4-flash:free"),
109
- ("openrouter","qwen/qwen3-coder:free"), ("cerebras","qwen-3-235b-a22b-instruct-2507")],
110
- "strong": [("cerebras","qwen-3-235b-a22b-instruct-2507"), ("cerebras","gpt-oss-120b"),
111
- ("openrouter","deepseek/deepseek-r1-0528:free"), ("gemini","gemini-2.5-flash")],
112
- "creative": [("gemini","gemini-2.5-flash"), ("openrouter","meta-llama/llama-3.3-70b-instruct:free"),
113
- ("openrouter","qwen/qwen3-32b:free"), ("cohere","command-r-plus-08-2024")],
114
- "backup": [("gemini","gemini-2.5-flash"), ("cerebras","qwen-3-235b-a22b-instruct-2507"),
115
- ("openrouter","deepseek/deepseek-r1:free"), ("cohere","command-r-plus-08-2024")],
116
- }
117
- ROLE_TASKS = {
118
- "code": ["code","debug","math","code_review"],
119
- "strong": ["knowledge","science","analysis","reasoning"],
120
- "creative": ["creative","writing","rpg","chat","emotional"],
121
- "backup": ["*"],
122
- }
123
-
124
-
125
- # ── AI CALLERS ────────────────────────────────────────────────────
126
- async def _call(provider: str, model: str, message: str, system: str, history: list) -> str | None:
127
- key = _rkey(provider)
128
- if not key and provider not in ("piston",):
129
- return None
130
- if not _quota_ok(provider):
131
- return None
132
-
133
- msgs = [{"role":"system","content":system}]
134
- for h in history[-4:]:
135
- if isinstance(h,dict): msgs.append(h)
136
- msgs.append({"role":"user","content":message})
137
 
 
138
  try:
139
- if provider == "gemini":
140
- contents = [{"role":"user","parts":[{"text":message}]}]
141
- body = {"system_instruction":{"parts":[{"text":system}]},
142
- "contents":contents,
143
- "generationConfig":{"temperature":0.7,"maxOutputTokens":4096}}
144
- async with httpx.AsyncClient(timeout=45) as cl:
145
- r = await cl.post(
146
- f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}",
147
- json=body)
148
- if r.status_code==429: _quota_hit("gemini",60); return None
149
- if r.status_code!=200: return None
150
- return r.json()["candidates"][0]["content"]["parts"][0]["text"]
151
-
152
- elif provider in ("cerebras","openrouter","mistral","cohere"):
153
- import openai as _oai
154
- base_urls = {
155
- "cerebras": "https://api.cerebras.ai/v1",
156
- "openrouter": "https://openrouter.ai/api/v1",
157
- "mistral": "https://api.mistral.ai/v1",
158
- "cohere": "https://api.cohere.ai/compatibility/v1",
159
- }
160
- cl = _oai.AsyncOpenAI(api_key=key, base_url=base_urls[provider])
161
- resp = await cl.chat.completions.create(
162
- model=model, messages=msgs, temperature=0.3, max_tokens=8192)
163
- return resp.choices[0].message.content
164
-
165
  except Exception as e:
166
- err = str(e).lower()
167
- if "429" in err or "quota" in err or "rate" in err:
168
- _quota_hit(provider, 90)
169
- print(f"[WORKER-{WORKER_ROLE}] {provider} err: {str(e)[:60]}")
170
- return None
171
-
172
-
173
- async def process(message: str, system: str, history: list, task: str) -> str:
174
- chain = ROLE_CHAIN.get(WORKER_ROLE, ROLE_CHAIN["backup"])
175
- for provider, model in chain:
176
- if not _quota_ok(provider): continue
177
- result = await _call(provider, model, message, system, history)
178
- if result and len(result.strip()) > 5:
179
- print(f"[WORKER-{WORKER_ROLE}] ✅ {provider}/{model[:25]}")
180
- return result
181
- return "⚠️ Worker tạm hết quota — space chính xử lý tiếp."
182
-
183
-
184
- # ── CODE EXECUTION (Piston, không cần key) ───────────────────────
185
- PISTON_LANGS = {
186
- "python":"python","py":"python","javascript":"javascript","js":"javascript",
187
- "typescript":"typescript","ts":"typescript","java":"java","c":"c","cpp":"c++",
188
- "c++":"c++","rust":"rust","go":"go","ruby":"ruby","php":"php","bash":"bash",
189
- "lua":"lua","kotlin":"kotlin","r":"r","csharp":"csharp","cs":"csharp",
190
- }
191
- PISTON_VERS = {
192
- "python":"3.10.0","javascript":"18.15.0","typescript":"5.0.3","java":"15.0.2",
193
- "c":"10.2.0","c++":"10.2.0","rust":"1.50.0","go":"1.16.2","ruby":"3.0.1",
194
- "php":"8.2.3","bash":"5.2.0","lua":"5.4.4","kotlin":"1.8.20","r":"4.1.1","csharp":"6.12.0",
195
- }
196
- _RATE_DATA: dict = {}
197
- _RATE_LOCK = threading.Lock()
198
-
199
- def _rate_ok(uid: str, is_admin: bool) -> tuple[bool,str]:
200
- if is_admin: return True,""
201
- now = time.time()
202
- with _RATE_LOCK:
203
- d = _RATE_DATA.setdefault(uid,{"m":[],"h":[]})
204
- d["m"] = [t for t in d["m"] if now-t<60]
205
- d["h"] = [t for t in d["h"] if now-t<3600]
206
- if len(d["m"]) >= 3:
207
- return False, f"⏳ Quá 3 lần/phút — chờ {int(60-(now-d['m'][0]))+1}s!"
208
- if len(d["h"]) >= 20:
209
- return False, f"⏳ Quá 20 lần/giờ — chờ {int(3600-(now-d['h'][0]))//60+1} phút!"
210
- d["m"].append(now); d["h"].append(now)
211
- return True,""
212
-
213
- async def run_code_piston(code: str, lang: str, stdin: str = "") -> dict:
214
- pl = PISTON_LANGS.get(lang.lower())
215
- if not pl: return {"ok":False,"error":f"Piston không hỗ trợ: {lang}","output":""}
216
- ver = PISTON_VERS.get(pl,"0.0.0")
217
  try:
218
- async with httpx.AsyncClient(timeout=20) as cl:
219
- r = await cl.post("https://emkc.org/api/v2/piston/execute",
220
- json={"language":pl,"version":ver,
221
- "files":[{"name":"main","content":code}],"stdin":stdin})
222
- if r.status_code==429: return {"ok":False,"error":"Piston 429 (5/s limit)","output":""}
223
- if r.status_code!=200: return {"ok":False,"error":f"Piston HTTP {r.status_code}","output":""}
224
- run = r.json().get("run",{})
225
- out = (run.get("stdout") or "").strip()
226
- err = (run.get("stderr") or "").strip()
227
- code_ret = run.get("code",0)
228
- if code_ret!=0 and not out:
229
- return {"ok":False,"error":err[:500],"output":""}
230
- return {"ok":True,"output":out[:1500],"error":err[:100],"backend":"piston"}
231
  except Exception as e:
232
- return {"ok":False,"error":str(e)[:80],"output":""}
 
233
 
234
 
235
- # ── FASTAPI APP ───────────────────────────────────────────────────
236
- from contextlib import asynccontextmanager
237
 
238
- @asynccontextmanager
239
- async def lifespan(app_instance):
240
- # Startup
241
- print(f"🚀 Yui Worker [{WORKER_ROLE}] khởi động — URL: {SELF_URL or 'chưa set'}")
242
- for fn in [_fetch_keys_sync, _key_refresh_loop, _self_ping, _cross_ping]:
243
- threading.Thread(target=fn, daemon=True).start()
244
- yield
245
- # Shutdown (nothing to cleanup)
246
 
247
- app = FastAPI(title=f"Yui Worker [{WORKER_ROLE}]", docs_url=None, redoc_url=None, lifespan=lifespan)
248
 
249
- # ══════════════════════════════════════════════════════════════════
250
- # 🔍 A. WEB SEARCH ENDPOINT — dùng Jina Reader + DuckDuckGo (free)
251
- # ══════════════════════════════════════════════════════════════════
252
- @app.post("/api/search")
253
- async def search_endpoint(request: Request):
254
- """Web search worker — Jina Reader + DuckDuckGo, không cần key."""
255
- try: body = await request.json()
256
- except Exception: raise HTTPException(400, "Invalid JSON")
257
- import hmac as _hm_s
258
- if not WORKER_SECRET or not _hm_s.compare_digest(
259
- body.get("secret","").encode(), WORKER_SECRET.encode()
260
- ): raise HTTPException(403, "Unauthorized")
261
-
262
- query = body.get("query", "").strip()
263
- mode = body.get("mode", "search") # "search" | "fetch"
264
- url_in = body.get("url", "")
265
- max_r = min(int(body.get("max_results", 5)), 8)
266
- if not query and not url_in:
267
- raise HTTPException(400, "query hoặc url required")
268
 
 
 
 
 
269
  t0 = time.time()
270
- results = []
271
 
272
- if mode == "fetch" and url_in:
273
- # Jina Reader: fetch nội dung 1 URL cụ thể
274
- try:
275
- async with httpx.AsyncClient(timeout=20) as cl:
276
- r = await cl.get(
277
- f"https://r.jina.ai/{url_in}",
278
- headers={"Accept": "text/plain", "X-No-Cache": "true"},
279
- )
280
- results = [{"title": url_in, "content": r.text[:3000], "url": url_in}]
281
- except Exception as e:
282
- results = [{"error": str(e)[:100]}]
283
 
284
- else:
285
- # DuckDuckGo search (không cần key)
286
- try:
287
- from urllib.parse import quote
288
- async with httpx.AsyncClient(timeout=15) as cl:
289
- r = await cl.get(
290
- "https://api.duckduckgo.com/",
291
- params={"q": query, "format": "json", "no_html": 1,
292
- "skip_disambig": 1, "no_redirect": 1},
293
- headers={"User-Agent": "Mozilla/5.0"},
294
- )
295
- data = r.json()
296
- # AbstractText
297
- if data.get("AbstractText"):
298
- results.append({"title": data.get("Heading",""),
299
- "content": data["AbstractText"][:1000],
300
- "url": data.get("AbstractURL","")})
301
- # RelatedTopics
302
- for t in data.get("RelatedTopics", [])[:max_r]:
303
- if isinstance(t, dict) and t.get("Text"):
304
- results.append({"title": t.get("Text","")[:80],
305
- "content": t.get("Text","")[:500],
306
- "url": t.get("FirstURL","")})
307
- # Fallback: Jina search nếu DDG trả về rỗng
308
- if not results:
309
- async with httpx.AsyncClient(timeout=20) as cl2:
310
- r2 = await cl2.get(
311
- f"https://s.jina.ai/{quote(query)}",
312
- headers={"Accept": "application/json",
313
- "X-Retain-Images": "none"},
314
- )
315
- if r2.status_code == 200:
316
- for line in r2.text.split("\n\n")[:max_r]:
317
- if line.strip():
318
- results.append({"content": line.strip()[:800], "url": ""})
319
- except Exception as e:
320
- results = [{"error": str(e)[:100]}]
321
 
322
- return JSONResponse({
323
- "ok": True, "query": query, "results": results[:max_r],
324
- "elapsed": round(time.time()-t0, 2), "worker": WORKER_ROLE,
325
- })
326
 
 
 
327
 
328
- # ══════════════════════════════════════════════════════════════════
329
- # 🔊 C. TTS ENDPOINT — Edge TTS (Microsoft, hoàn toàn free)
330
- # ══════════════════════════════════════════════════════════════════
331
- @app.post("/api/tts")
332
- async def tts_endpoint(request: Request):
333
- """Text-to-speech — Edge TTS, không cần key, hỗ trợ giọng Việt."""
334
- try: body = await request.json()
335
- except Exception: raise HTTPException(400, "Invalid JSON")
336
- import hmac as _hm_t
337
- if not WORKER_SECRET or not _hm_t.compare_digest(
338
- body.get("secret","").encode(), WORKER_SECRET.encode()
339
- ): raise HTTPException(403, "Unauthorized")
340
-
341
- text = body.get("text", "").strip()[:2000]
342
- voice = body.get("voice", "vi-VN-HoaiMyNeural") # default: giọng nữ Việt
343
- if not text:
344
- raise HTTPException(400, "text required")
345
 
346
- t0 = time.time()
347
- try:
348
- import edge_tts, io
349
- communicate = edge_tts.Communicate(text, voice)
350
- audio_data = io.BytesIO()
351
- async for chunk in communicate.stream():
352
- if chunk["type"] == "audio":
353
- audio_data.write(chunk["data"])
354
- audio_bytes = audio_data.getvalue()
355
- import base64
356
- audio_b64 = base64.b64encode(audio_bytes).decode()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  return JSONResponse({
358
- "ok": True, "audio_b64": audio_b64,
359
- "format": "mp3", "voice": voice,
360
- "chars": len(text),
361
- "elapsed": round(time.time()-t0, 2),
362
  })
363
- except ImportError:
364
- return JSONResponse({"ok": False, "error": "edge_tts chưa cài. Thêm vào requirements.txt"})
365
- except Exception as e:
366
- return JSONResponse({"ok": False, "error": str(e)[:200]})
367
-
368
-
369
- # ══════════════════════════════════════════════════════════════════
370
- # 💾 E. SMART CACHE ENDPOINT — SQLite-based semantic cache
371
- # ══════════════════════════════════════════════════════════════════
372
- import sqlite3, hashlib, json as _json_mod
373
-
374
- _CACHE_DB = "/tmp/yui_cache.db"
375
- _CACHE_LOCK = threading.Lock()
376
-
377
- def _cache_init():
378
- with sqlite3.connect(_CACHE_DB) as db:
379
- db.execute("""CREATE TABLE IF NOT EXISTS cache (
380
- key TEXT PRIMARY KEY,
381
- query TEXT,
382
- reply TEXT,
383
- task TEXT,
384
- hits INTEGER DEFAULT 0,
385
- created REAL,
386
- expires REAL
387
- )""")
388
- db.execute("CREATE INDEX IF NOT EXISTS idx_expires ON cache(expires)")
389
- db.commit()
390
-
391
- def _cache_key(query: str, task: str = "*") -> str:
392
- # Normalize: lowercase, strip spaces thừa
393
- norm = " ".join(query.lower().split())
394
- return hashlib.sha256(f"{task}:{norm}".encode()).hexdigest()[:32]
395
-
396
- def _cache_get(query: str, task: str = "*") -> "str | None":
397
- key = _cache_key(query, task)
398
- now = time.time()
399
- with _CACHE_LOCK:
400
- try:
401
- with sqlite3.connect(_CACHE_DB) as db:
402
- row = db.execute(
403
- "SELECT reply FROM cache WHERE key=? AND expires>?",
404
- (key, now)
405
- ).fetchone()
406
- if row:
407
- db.execute("UPDATE cache SET hits=hits+1 WHERE key=?", (key,))
408
- db.commit()
409
- return row[0]
410
- except Exception:
411
- pass
412
- return None
413
 
414
- def _cache_set(query: str, reply: str, task: str = "*", ttl: int = 3600):
415
- key = _cache_key(query, task)
416
- now = time.time()
417
- with _CACHE_LOCK:
418
- try:
419
- with sqlite3.connect(_CACHE_DB) as db:
420
- db.execute("""INSERT OR REPLACE INTO cache
421
- (key, query, reply, task, hits, created, expires)
422
- VALUES (?,?,?,?,0,?,?)""",
423
- (key, query[:300], reply, task, now, now+ttl))
424
- # Dọn cache hết hạn (max 500 entries)
425
- db.execute("DELETE FROM cache WHERE expires<?", (now,))
426
- db.execute("""DELETE FROM cache WHERE key NOT IN (
427
- SELECT key FROM cache ORDER BY expires DESC LIMIT 500)""")
428
- db.commit()
429
- except Exception:
430
- pass
431
-
432
- # Init cache DB khi startup
433
- _cache_init()
434
-
435
- @app.post("/api/cache")
436
- async def cache_endpoint(request: Request):
437
- """SmartCache — GET hoặc SET cache response."""
438
- try: body = await request.json()
439
- except Exception: raise HTTPException(400, "Invalid JSON")
440
- import hmac as _hm_c
441
- if not WORKER_SECRET or not _hm_c.compare_digest(
442
- body.get("secret","").encode(), WORKER_SECRET.encode()
443
- ): raise HTTPException(403, "Unauthorized")
444
-
445
- action = body.get("action", "get") # "get" | "set" | "stats"
446
- query = body.get("query", "")
447
- task = body.get("task", "*")
448
-
449
- if action == "get":
450
- cached = _cache_get(query, task)
451
- return JSONResponse({"ok": True, "hit": cached is not None,
452
- "reply": cached, "worker": WORKER_ROLE})
453
-
454
- elif action == "set":
455
- reply = body.get("reply", "")
456
- ttl = int(body.get("ttl", 3600))
457
- if query and reply:
458
- _cache_set(query, reply, task, ttl)
459
- return JSONResponse({"ok": True, "stored": bool(query and reply)})
460
-
461
- elif action == "stats":
462
- with _CACHE_LOCK:
463
- try:
464
- with sqlite3.connect(_CACHE_DB) as db:
465
- count = db.execute("SELECT COUNT(*) FROM cache WHERE expires>?",
466
- (time.time(),)).fetchone()[0]
467
- hits = db.execute("SELECT SUM(hits) FROM cache").fetchone()[0] or 0
468
- except Exception:
469
- count, hits = 0, 0
470
- return JSONResponse({"ok": True, "entries": count, "total_hits": hits,
471
- "worker": WORKER_ROLE})
472
-
473
- return JSONResponse({"ok": False, "error": "action phải là get/set/stats"})
474
-
475
-
476
- # ══════════════════════════════════════════════════════════════════
477
- # 🧠 F. BACKUP BRAIN ENDPOINT — Mini pipeline khi space chính bận
478
- # ══════════════════════════════════════════════════════════════════
479
- @app.post("/api/backup-brain")
480
- async def backup_brain_endpoint(request: Request):
481
- """
482
- Backup Brain — xử lý request khi space chính timeout/bận.
483
- Pipeline đơn giản: không FAISS, không IMAD, không image gen.
484
- Chỉ cần trả lời nhanh và đúng.
485
- """
486
- try: body = await request.json()
487
- except Exception: raise HTTPException(400, "Invalid JSON")
488
- import hmac as _hm_b
489
- if not WORKER_SECRET or not _hm_b.compare_digest(
490
- body.get("secret","").encode(), WORKER_SECRET.encode()
491
- ): raise HTTPException(403, "Unauthorized")
492
 
493
- message = body.get("message", "").strip()
494
- history = body.get("history", [])
495
- task = body.get("task", "*")
496
- if not message:
497
- raise HTTPException(400, "message required")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
 
499
- t0 = time.time()
500
 
501
- # 1. Check SmartCache trước (nếu có cache → trả ngay)
502
- cached = _cache_get(message, task)
503
- if cached:
504
- return JSONResponse({"ok": True, "reply": cached, "source": "cache",
505
- "elapsed": round(time.time()-t0, 2)})
 
 
 
 
 
 
 
 
 
 
 
 
506
 
507
- # 2. Gọi AI với pipeline đơn giản
508
- system = (
509
- "Bạn là Yui, AI assistant thông minh của Boss. "
510
- "Trả lời ngắn gọn, chính xác, bằng tiếng Việt. "
511
- "Đây là backup mode — không có tool nào khác."
512
- )
513
- reply = await process(message, system, history, task)
514
 
515
- # 3. Cache kết quả (TTL 30 phút cho backup brain)
516
- if reply and len(reply) > 20:
517
- _cache_set(message, reply, task, ttl=1800)
 
518
 
519
- return JSONResponse({
520
- "ok": True, "reply": reply, "source": "backup_brain",
521
- "role": WORKER_ROLE, "elapsed": round(time.time()-t0, 2),
522
- })
523
 
 
524
 
525
- @app.get("/")
526
- @app.get("/health")
527
- @app.head("/")
528
- @app.head("/health")
529
- async def health():
530
- return {
531
- "status": "ok", "role": WORKER_ROLE,
532
- "keys_loaded": _keys_loaded.is_set(),
533
- "keys": {k: len(v) for k,v in _KEYS.items() if v},
534
- "quota": {k:"cd" if not _quota_ok(k) else "ok" for k in ["gemini","cerebras","openrouter","mistral"]},
535
- "ts": int(time.time()),
536
- }
537
 
538
- @app.post("/api/worker")
539
- async def worker_endpoint(request: Request):
540
- """Nhận request từ space chính."""
541
- try: body = await request.json()
542
- except Exception: raise HTTPException(400,"Invalid JSON")
543
- import hmac as _hm
544
- if not WORKER_SECRET or not _hm.compare_digest(
545
- body.get("secret","").encode(), WORKER_SECRET.encode()
546
- ): raise HTTPException(403,"Unauthorized")
547
- message = body.get("message","")
548
- system = body.get("system","Bạn là Yui, AI assistant thông minh.")
549
- history = body.get("history",[])
550
- task = body.get("task","*")
551
- if not message: raise HTTPException(400,"Empty message")
552
- accepted = ROLE_TASKS.get(WORKER_ROLE,["*"])
553
- if "*" not in accepted and task not in accepted:
554
- return JSONResponse({"ok":False,"reason":f"Worker [{WORKER_ROLE}] không xử lý task '{task}'"})
555
- t0 = time.time()
556
- reply = await process(message, system, history, task)
557
- return JSONResponse({"ok":True,"reply":reply,"role":WORKER_ROLE,"elapsed":round(time.time()-t0,2)})
558
-
559
- @app.post("/api/run-code")
560
- async def run_code_endpoint(request: Request):
561
- """Chạy code qua Piston (no key)."""
562
- try: body = await request.json()
563
- except Exception: raise HTTPException(400,"Invalid JSON")
564
- code = body.get("code","").strip()
565
- lang = body.get("lang","python")
566
- stdin = body.get("stdin","")
567
- user_id = body.get("user_id","unknown")
568
- is_admin = body.get("is_admin",False)
569
- # Admin check
570
- if WORKER_SECRET and body.get("admin_secret","") == WORKER_SECRET:
571
- is_admin = True
572
- if not code: raise HTTPException(400,"Empty code")
573
- ok, msg = _rate_ok(user_id, is_admin)
574
- if not ok: return JSONResponse({"ok":False,"error":msg,"rate_blocked":True})
575
- result = await run_code_piston(code, lang, stdin)
576
- return JSONResponse(result)
577
-
578
- @app.get("/api/quota")
579
- async def quota_status(secret: str = ""):
580
- import hmac as _hm2
581
- if not WORKER_SECRET or not _hm2.compare_digest(secret.encode(), WORKER_SECRET.encode()):
582
- raise HTTPException(403,"Unauthorized")
583
- return {"role":WORKER_ROLE, "quota":{k:"cooldown" if not _quota_ok(k) else "ok" for k in _quota_hits},
584
- "keys":{k:len(v) for k,v in _KEYS.items()}}
585
-
586
-
587
- # ── STEALTH PING ──────────────────────────────────────────────────
588
- _PING_RANGES = {
589
- "code": (240,360), "strong": (270,420),
590
- "creative": (300,480), "backup": (330,540),
591
- }
592
-
593
- def _self_ping():
594
- if not SELF_URL: return
595
- time.sleep(random.randint(30,120)) # Stagger start
596
- lo, hi = _PING_RANGES.get(WORKER_ROLE,(300,480))
597
- while True:
598
- try:
599
- import requests as _rq
600
- _rq.get(f"{SELF_URL}/health", timeout=10)
601
- print(f"[WORKER-{WORKER_ROLE}] 💓 self-ping OK")
602
- except Exception as e:
603
- print(f"[WORKER-{WORKER_ROLE}] ⚠️ self-ping: {e}")
604
- time.sleep(random.randint(lo, hi))
605
-
606
- def _cross_ping():
607
- peers = [u for u in WORKER_URLS if u != SELF_URL]
608
- if not peers: return
609
- time.sleep(random.randint(120,300))
610
- while True:
611
- try:
612
- import requests as _rq
613
- _rq.get(f"{random.choice(peers)}/health", timeout=8)
614
- except Exception: pass
615
- time.sleep(random.randint(600,1200))
616
 
617
- if __name__ == "__main__":
618
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  """
2
+ URAL AI — Worker Space App
3
+ Nhẹ, nhanh, chỉ làm đúng 1 việc: nhận task từ main space và xử lý AI.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
+ ENV cần set trong HF Secrets của mỗi worker:
6
+ WORKER_SECRET = (giống main space)
7
+ WORKER_ROLE = strong | code | creative | backup
8
+ MAIN_SPACE_URL = https://bachdathan-ural-ai.hf.space (URL main space)
9
+ """
10
+ import os, time, random, asyncio
11
+ import requests as _req
12
+ from fastapi import FastAPI, Request
13
  from fastapi.responses import JSONResponse
14
+
15
+ app = FastAPI(title="Yui Worker")
16
+
17
+ # ── Config ────────────────────────────────────────────────────────
18
+ WORKER_SECRET = os.environ.get("WORKER_SECRET", "")
19
+ WORKER_ROLE = os.environ.get("WORKER_ROLE", "backup")
20
+ MAIN_SPACE_URL = os.environ.get("MAIN_SPACE_URL",
21
+ "https://bachdathan-ural-ai.hf.space")
22
+
23
+ # ── Key cache ─────────────────────────────────────────────────────
24
+ _keys: dict = {"gemini": [], "cerebras": [], "openrouter": [],
25
+ "cohere": [], "mistral": []}
26
+ _keys_at: float = 0.0
27
+ _KEY_TTL = 1500 # 25 phút
28
+
 
 
29
 
30
  def _fetch_keys_sync():
31
+ """Lấy API keys từ main space (sync, dùng khi startup)."""
32
+ global _keys, _keys_at
33
+ try:
34
+ r = _req.post(
35
+ f"{MAIN_SPACE_URL}/api/get-keys",
36
+ json={"secret": WORKER_SECRET, "role": WORKER_ROLE},
37
+ timeout=20
38
+ )
39
+ if r.status_code == 200:
40
+ _keys = r.json()
41
+ _keys_at = time.time()
42
+ g = len(_keys.get("gemini", []))
43
+ cb = len(_keys.get("cerebras", []))
44
+ co = len(_keys.get("cohere", []))
45
+ print(f"[WORKER-{WORKER_ROLE.upper()}] ✅ Keys: G×{g} CB×{cb} CO×{co}")
46
+ else:
47
+ print(f"[WORKER] ⚠️ get-keys trả {r.status_code}")
48
+ except Exception as e:
49
+ print(f"[WORKER] ⚠️ get-keys lỗi: {e}")
50
+
51
+
52
+ def _get_keys() -> dict:
53
+ if time.time() - _keys_at > _KEY_TTL:
54
+ _fetch_keys_sync()
55
+ return _keys
56
+
57
+
58
+ # ── Helpers gọi AI ────────────────────────────────────────────────
59
+ def _build_msgs(system: str, history: list, message: str) -> list:
60
+ msgs = []
61
+ if system:
62
+ msgs.append({"role": "system", "content": system[:1500]})
63
+ for turn in history[-6:]:
64
+ if isinstance(turn, (list, tuple)) and len(turn) >= 2:
65
+ msgs.append({"role": "user", "content": str(turn[0])[:800]})
66
+ msgs.append({"role": "assistant", "content": str(turn[1])[:800]})
67
+ msgs.append({"role": "user", "content": message})
68
+ return msgs
69
+
70
+
71
+ def _call_gemini(keys: list, msgs: list, system: str, max_tok: int = 1000) -> str:
72
+ for key in random.sample(keys, min(3, len(keys))):
73
  try:
74
+ contents = [{"role": "user" if m["role"] == "user" else "model",
75
+ "parts": [{"text": m["content"]}]}
76
+ for m in msgs if m["role"] != "system"]
77
+ if not contents:
78
+ continue
79
+ payload: dict = {
80
+ "contents": contents,
81
+ "generationConfig": {"maxOutputTokens": max_tok, "temperature": 0.7}
82
+ }
83
+ if system:
84
+ payload["system_instruction"] = {"parts": [{"text": system[:1000]}]}
85
+ r = _req.post(
86
+ f"https://generativelanguage.googleapis.com/v1beta/models/"
87
+ f"gemini-2.5-flash:generateContent?key={key}",
88
+ json=payload, timeout=30
89
  )
90
  if r.status_code == 200:
91
+ cands = r.json().get("candidates", [])
92
+ if cands:
93
+ text = (cands[0].get("content", {})
94
+ .get("parts", [{}])[0].get("text", ""))
95
+ if text and len(text) > 5:
96
+ return text
 
 
 
 
 
 
 
97
  except Exception as e:
98
+ print(f"[WORKER] Gemini key fail: {type(e).__name__}")
99
+ return ""
100
 
 
 
101
 
102
+ def _call_cerebras(keys: list, msgs: list, max_tok: int = 800) -> str:
103
+ try:
104
+ import openai as _oai
105
+ model = "gpt-oss-120b"
106
+ client = _oai.OpenAI(api_key=random.choice(keys),
107
+ base_url="https://api.cerebras.ai/v1")
108
+ r = client.chat.completions.create(
109
+ model=model, messages=msgs,
110
+ max_tokens=max_tok, temperature=0.7, timeout=25
111
+ )
112
+ return r.choices[0].message.content or ""
113
+ except Exception as e:
114
+ print(f"[WORKER] Cerebras fail: {type(e).__name__}: {e}")
115
+ # Thử model nhẹ hơn
116
+ try:
117
+ import openai as _oai2
118
+ client2 = _oai2.OpenAI(api_key=random.choice(keys),
119
+ base_url="https://api.cerebras.ai/v1")
120
+ r2 = client2.chat.completions.create(
121
+ model="zai-glm-4.7", messages=msgs,
122
+ max_tokens=max_tok, temperature=0.7, timeout=20
123
+ )
124
+ return r2.choices[0].message.content or ""
125
+ except Exception:
126
+ return ""
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ def _call_cohere(keys: list, msgs: list, message: str, system: str) -> str:
130
  try:
131
+ import cohere as _co
132
+ client = _co.Client(api_key=random.choice(keys))
133
+ hist = []
134
+ for m in msgs[:-1]:
135
+ if m["role"] == "system":
136
+ continue
137
+ hist.append({"role": "USER" if m["role"] == "user" else "CHATBOT",
138
+ "message": m["content"][:400]})
139
+ r = client.chat(
140
+ model="command-r-08-2024",
141
+ message=message,
142
+ chat_history=hist or None,
143
+ preamble=system[:500] if system else None,
144
+ max_tokens=600,
145
+ )
146
+ return r.text or ""
 
 
 
 
 
 
 
 
 
 
147
  except Exception as e:
148
+ print(f"[WORKER] Cohere fail: {type(e).__name__}")
149
+ return ""
150
+
151
+
152
+ def _call_openrouter(keys: list, msgs: list, task: str = "chat") -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  try:
154
+ import openai as _oai
155
+ model = ("qwen/qwen3-coder:free" if task in ("code", "math", "debug")
156
+ else "openrouter/owl-alpha")
157
+ client = _oai.OpenAI(api_key=random.choice(keys),
158
+ base_url="https://openrouter.ai/api/v1")
159
+ r = client.chat.completions.create(
160
+ model=model, messages=msgs,
161
+ max_tokens=600, timeout=20
162
+ )
163
+ return r.choices[0].message.content or ""
 
 
 
164
  except Exception as e:
165
+ print(f"[WORKER] OpenRouter fail: {type(e).__name__}")
166
+ return ""
167
 
168
 
169
+ # ── Endpoints ─────────────────────────────────────────────────────
 
170
 
171
+ @app.get("/")
172
+ @app.head("/")
173
+ async def root():
174
+ return {"yui": "worker", "role": WORKER_ROLE, "status": "ready"}
 
 
 
 
175
 
 
176
 
177
+ @app.get("/health")
178
+ @app.head("/health")
179
+ async def health():
180
+ return {"status": "ok", "role": WORKER_ROLE,
181
+ "has_keys": bool(_keys.get("gemini"))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
+
184
+ @app.post("/api/worker")
185
+ async def worker_chat(request: Request):
186
+ """Endpoint chính — nhận task từ main space và trả kết quả."""
187
  t0 = time.time()
 
188
 
189
+ # Parse body
190
+ try:
191
+ body = await request.json()
192
+ except Exception:
193
+ return JSONResponse({"ok": False, "error": "invalid json"}, status_code=400)
 
 
 
 
 
 
194
 
195
+ # Auth
196
+ if not WORKER_SECRET or body.get("secret") != WORKER_SECRET:
197
+ return JSONResponse({"ok": False, "error": "unauthorized"}, status_code=403)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
+ message = body.get("message", "").strip()
200
+ system = body.get("system", "")
201
+ history = body.get("history", [])
202
+ task = body.get("task", "chat")
203
 
204
+ if not message:
205
+ return JSONResponse({"ok": False, "error": "empty message"}, status_code=400)
206
 
207
+ # Lấy keys
208
+ keys = _get_keys()
209
+ msgs = _build_msgs(system, history, message)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
+ reply = ""
212
+
213
+ # Chọn model theo role và task
214
+ gemini = keys.get("gemini", [])
215
+ cerebras = keys.get("cerebras", [])
216
+ cohere = keys.get("cohere", [])
217
+ openrouter = keys.get("openrouter", [])
218
+
219
+ # Gemini Flash — ưu tiên hàng đầu
220
+ if gemini and not reply:
221
+ max_tok = 1500 if task in ("code", "math", "debug") else 800
222
+ reply = _call_gemini(gemini, msgs, system, max_tok)
223
+
224
+ # Cerebras — fallback mạnh
225
+ if cerebras and not reply:
226
+ max_tok = 1200 if task in ("code", "math", "debug") else 700
227
+ reply = _call_cerebras(cerebras, msgs, max_tok)
228
+
229
+ # Cohere — fallback trung bình
230
+ if cohere and not reply:
231
+ reply = _call_cohere(cohere, msgs, message, system)
232
+
233
+ # OpenRouter — fallback cuối
234
+ if openrouter and not reply:
235
+ reply = _call_openrouter(openrouter, msgs, task)
236
+
237
+ elapsed = round(time.time() - t0, 2)
238
+
239
+ if reply:
240
  return JSONResponse({
241
+ "ok": True,
242
+ "reply": reply,
243
+ "role": WORKER_ROLE,
244
+ "elapsed": elapsed
245
  })
246
+ else:
247
+ return JSONResponse({
248
+ "ok": False,
249
+ "error": "all models failed",
250
+ "role": WORKER_ROLE,
251
+ "elapsed": elapsed
252
+ }, status_code=503)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
+ @app.post("/api/tts")
256
+ async def worker_tts(request: Request):
257
+ """TTS endpoint — edge-tts."""
258
+ try:
259
+ body = await request.json()
260
+ if body.get("secret") != WORKER_SECRET:
261
+ return JSONResponse({"ok": False}, status_code=403)
262
+ text = body.get("text", "").strip()
263
+ if not text:
264
+ return JSONResponse({"ok": False, "error": "no text"})
265
+ import edge_tts, base64
266
+ voice = "vi-VN-HoaiMyNeural"
267
+ comm = edge_tts.Communicate(text[:600], voice)
268
+ chunks = []
269
+ async for chunk in comm.stream():
270
+ if chunk["type"] == "audio":
271
+ chunks.append(chunk["data"])
272
+ audio_b64 = base64.b64encode(b"".join(chunks)).decode()
273
+ return JSONResponse({"ok": True, "audio_b64": audio_b64})
274
+ except Exception as e:
275
+ return JSONResponse({"ok": False, "error": str(e)})
276
 
 
277
 
278
+ @app.post("/api/search")
279
+ async def worker_search(request: Request):
280
+ """Search endpoint — Tavily."""
281
+ try:
282
+ body = await request.json()
283
+ if body.get("secret") != WORKER_SECRET:
284
+ return JSONResponse({"ok": False}, status_code=403)
285
+ query = body.get("query", "")
286
+ tavily = os.environ.get("TAVILY_KEY", "")
287
+ if not tavily or not query:
288
+ return JSONResponse({"ok": False, "results": []})
289
+ r = _req.post("https://api.tavily.com/search",
290
+ json={"api_key": tavily, "query": query, "max_results": 3},
291
+ timeout=10)
292
+ return JSONResponse({"ok": True, "results": r.json().get("results", [])})
293
+ except Exception as e:
294
+ return JSONResponse({"ok": False, "results": [], "error": str(e)})
295
 
 
 
 
 
 
 
 
296
 
297
+ @app.post("/api/backup-brain")
298
+ async def worker_backup_brain(request: Request):
299
+ """Backup brain — forward to Cohere/Cerebras khi main space quá tải."""
300
+ return await worker_chat(request)
301
 
 
 
 
 
302
 
303
+ # ── Startup ───────────────────────────────────────────────────────
304
 
305
+ @app.on_event("startup")
306
+ async def startup():
307
+ import threading
308
+ print(f"[WORKER-{WORKER_ROLE.upper()}] 🚀 Khởi động...")
309
+ # Fetch keys ngay lúc startup (sync trong thread để không block)
310
+ threading.Thread(target=_fetch_keys_sync, daemon=True).start()
 
 
 
 
 
 
311
 
312
+ # Background: refresh keys mỗi 25 phút
313
+ async def _refresh_loop():
314
+ while True:
315
+ await asyncio.sleep(1500)
316
+ _fetch_keys_sync()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
+ asyncio.create_task(_refresh_loop())
319
+ print(f"[WORKER-{WORKER_ROLE.upper()}] ✅ Sẵn sàng! Role={WORKER_ROLE}")
requirements.txt CHANGED
@@ -1,19 +1,9 @@
1
- # Worker space requirements — chỉ những gì worker cần
2
- # (nhẹ hơn main space, không cần gradio/playwright/maigret/...)
3
- httpx
4
  fastapi
5
  uvicorn
 
6
  requests
7
- aiohttp
8
  openai
9
  cohere
 
10
  google-generativeai>=0.8.3
11
- sentence-transformers
12
- faiss-cpu
13
- numpy
14
- apscheduler
15
  psutil
16
- python-multipart
17
- pydub
18
- websockets>=12.0
19
- pytz
 
 
 
 
1
  fastapi
2
  uvicorn
3
+ httpx
4
  requests
 
5
  openai
6
  cohere
7
+ edge-tts
8
  google-generativeai>=0.8.3
 
 
 
 
9
  psutil