Spaces:
Paused
Paused
Upload 2 files
Browse files- app.py +276 -575
- requirements.txt +2 -12
app.py
CHANGED
|
@@ -1,618 +1,319 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 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 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
from fastapi.responses import JSONResponse
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
WORKER_SECRET
|
| 32 |
-
WORKER_ROLE
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
# ──
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
_keys_loaded = threading.Event()
|
| 43 |
-
_keys_lock = threading.Lock()
|
| 44 |
|
| 45 |
def _fetch_keys_sync():
|
| 46 |
-
"""
|
| 47 |
-
global
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
try:
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
)
|
| 61 |
if r.status_code == 200:
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 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
|
| 77 |
-
|
| 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 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 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 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 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 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 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 |
-
|
|
|
|
| 233 |
|
| 234 |
|
| 235 |
-
# ──
|
| 236 |
-
from contextlib import asynccontextmanager
|
| 237 |
|
| 238 |
-
@
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 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 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 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 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 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 |
-
|
| 285 |
-
|
| 286 |
-
|
| 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 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
|
|
|
|
|
|
|
| 327 |
|
| 328 |
-
#
|
| 329 |
-
|
| 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 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
return JSONResponse({
|
| 358 |
-
"ok": True,
|
| 359 |
-
"
|
| 360 |
-
"
|
| 361 |
-
"elapsed":
|
| 362 |
})
|
| 363 |
-
|
| 364 |
-
return JSONResponse({
|
| 365 |
-
|
| 366 |
-
|
| 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 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
|
| 499 |
-
t0 = time.time()
|
| 500 |
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 516 |
-
|
| 517 |
-
|
|
|
|
| 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.
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 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 |
-
|
| 539 |
-
async def
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 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 |
-
|
| 618 |
-
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|