Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
|
@@ -13,8 +13,9 @@ from fastapi.responses import JSONResponse
|
|
| 13 |
# Configuration
|
| 14 |
# ====================================================================
|
| 15 |
API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-me-secret")
|
| 16 |
-
POOL_SIZE = int(os.getenv("POOL_SIZE", "
|
| 17 |
-
MAX_REQUESTS = int(os.getenv("MAX_REQUESTS", "
|
|
|
|
| 18 |
|
| 19 |
DUCK_MODELS = {
|
| 20 |
"gpt-5-mini": "GPT-5 mini",
|
|
@@ -135,10 +136,10 @@ class BrowserWorker:
|
|
| 135 |
self.busy = False
|
| 136 |
|
| 137 |
async def _do_chat(self, model_label: str, prompt: str) -> str:
|
| 138 |
-
#
|
| 139 |
await asyncio.sleep(random.uniform(0.5, 2.0))
|
| 140 |
|
| 141 |
-
#
|
| 142 |
self._request_count += 1
|
| 143 |
self._total_count += 1
|
| 144 |
if self._request_count >= MAX_REQUESTS:
|
|
@@ -148,11 +149,9 @@ class BrowserWorker:
|
|
| 148 |
try:
|
| 149 |
page.set_default_timeout(180000)
|
| 150 |
|
| 151 |
-
# โโ 1. ูุชุญ ุงูุตูุญุฉ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 152 |
await page.goto("https://duckduckgo.com/aichat", wait_until="domcontentloaded")
|
| 153 |
await asyncio.sleep(3)
|
| 154 |
|
| 155 |
-
# โโ 2. Popup ุงุญุชูุงุทู โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 156 |
try:
|
| 157 |
agree = page.locator('button:has-text("Agree and Continue")')
|
| 158 |
if await agree.count() > 0 and await agree.first.is_visible():
|
|
@@ -161,17 +160,11 @@ class BrowserWorker:
|
|
| 161 |
except Exception:
|
| 162 |
pass
|
| 163 |
|
| 164 |
-
# โโ 3. ุงูุชุธุงุฑ textarea โโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 165 |
await page.wait_for_selector('textarea[name="user-prompt"]')
|
| 166 |
print(self.tag(f"Input ready (req #{self._total_count}) โ"))
|
| 167 |
|
| 168 |
-
# โโ 4. ุชุบููุฑ ุงููู
ูุฐุฌ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 169 |
await self._select_model(page, model_label)
|
| 170 |
-
|
| 171 |
-
# โโ 5. ุฅุฑุณุงู โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 172 |
await self._send(page, prompt)
|
| 173 |
-
|
| 174 |
-
# โโ 6. ุงุณุชุฎุฑุงุฌ ุงูุฑุฏ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 175 |
return await self._extract(page)
|
| 176 |
|
| 177 |
except Exception as e:
|
|
@@ -325,16 +318,18 @@ class BrowserWorker:
|
|
| 325 |
class BrowserPool:
|
| 326 |
|
| 327 |
def __init__(self):
|
| 328 |
-
self.loop
|
| 329 |
self.workers: list[BrowserWorker] = []
|
| 330 |
-
self.ready_event
|
| 331 |
-
self._thread
|
| 332 |
self._queue: asyncio.Queue | None = None
|
| 333 |
self._total_requests = 0
|
|
|
|
| 334 |
|
| 335 |
def start(self):
|
| 336 |
self._thread.start()
|
| 337 |
-
print(f"[POOL] Starting {POOL_SIZE} browsers
|
|
|
|
| 338 |
|
| 339 |
def _run(self):
|
| 340 |
asyncio.set_event_loop(self.loop)
|
|
@@ -355,8 +350,23 @@ class BrowserPool:
|
|
| 355 |
|
| 356 |
async def _process(self, model_label: str, prompt: str) -> str:
|
| 357 |
self._total_requests += 1
|
| 358 |
-
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
try:
|
| 361 |
return await worker.chat(model_label, prompt)
|
| 362 |
finally:
|
|
@@ -369,7 +379,7 @@ class BrowserPool:
|
|
| 369 |
future = asyncio.run_coroutine_threadsafe(
|
| 370 |
self._process(model_label, prompt), self.loop
|
| 371 |
)
|
| 372 |
-
return future.result(timeout=240)
|
| 373 |
|
| 374 |
|
| 375 |
pool = BrowserPool()
|
|
@@ -399,7 +409,9 @@ def _build_prompt(messages: list) -> str:
|
|
| 399 |
if not content.strip():
|
| 400 |
continue
|
| 401 |
if role == "system":
|
| 402 |
-
parts.append(
|
|
|
|
|
|
|
| 403 |
elif role == "assistant":
|
| 404 |
parts.append(f"[Assistant]: {content}")
|
| 405 |
else:
|
|
@@ -506,7 +518,8 @@ async def chat_completions(request: Request):
|
|
| 506 |
return _make_completion(start_time, model, text, messages, tools)
|
| 507 |
except Exception as e:
|
| 508 |
print(f"[API] ERROR: {e}")
|
| 509 |
-
|
|
|
|
| 510 |
|
| 511 |
|
| 512 |
@app.post("/v1/responses")
|
|
@@ -565,7 +578,8 @@ async def responses(request: Request):
|
|
| 565 |
}
|
| 566 |
except Exception as e:
|
| 567 |
print(f"[API] ERROR: {e}")
|
| 568 |
-
|
|
|
|
| 569 |
|
| 570 |
|
| 571 |
@app.get("/v1/models")
|
|
@@ -583,19 +597,25 @@ async def list_models(request: Request):
|
|
| 583 |
async def health():
|
| 584 |
busy = sum(1 for w in pool.workers if w.busy)
|
| 585 |
stats = [
|
| 586 |
-
{
|
| 587 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
for w in pool.workers
|
| 589 |
]
|
| 590 |
return {
|
| 591 |
-
"status":
|
| 592 |
-
"message":
|
| 593 |
-
"models":
|
| 594 |
-
"pool_size":
|
| 595 |
-
"workers_busy":
|
| 596 |
-
"workers_free":
|
| 597 |
-
"total_requests":
|
| 598 |
-
"
|
|
|
|
|
|
|
| 599 |
}
|
| 600 |
|
| 601 |
|
|
|
|
| 13 |
# Configuration
|
| 14 |
# ====================================================================
|
| 15 |
API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-me-secret")
|
| 16 |
+
POOL_SIZE = int(os.getenv("POOL_SIZE", "5"))
|
| 17 |
+
MAX_REQUESTS = int(os.getenv("MAX_REQUESTS", "30"))
|
| 18 |
+
QUEUE_TIMEOUT = int(os.getenv("QUEUE_TIMEOUT", "300")) # ุซูุงูู ุงูุชุธุงุฑ Queue
|
| 19 |
|
| 20 |
DUCK_MODELS = {
|
| 21 |
"gpt-5-mini": "GPT-5 mini",
|
|
|
|
| 136 |
self.busy = False
|
| 137 |
|
| 138 |
async def _do_chat(self, model_label: str, prompt: str) -> str:
|
| 139 |
+
# ุชุฃุฎูุฑ ุนุดูุงุฆู ูุชุจุฏู ุงูุทูุจุงุช ุทุจูุนูุฉ
|
| 140 |
await asyncio.sleep(random.uniform(0.5, 2.0))
|
| 141 |
|
| 142 |
+
# ุชุฌุฏูุฏ context ุฏูุฑูุงู
|
| 143 |
self._request_count += 1
|
| 144 |
self._total_count += 1
|
| 145 |
if self._request_count >= MAX_REQUESTS:
|
|
|
|
| 149 |
try:
|
| 150 |
page.set_default_timeout(180000)
|
| 151 |
|
|
|
|
| 152 |
await page.goto("https://duckduckgo.com/aichat", wait_until="domcontentloaded")
|
| 153 |
await asyncio.sleep(3)
|
| 154 |
|
|
|
|
| 155 |
try:
|
| 156 |
agree = page.locator('button:has-text("Agree and Continue")')
|
| 157 |
if await agree.count() > 0 and await agree.first.is_visible():
|
|
|
|
| 160 |
except Exception:
|
| 161 |
pass
|
| 162 |
|
|
|
|
| 163 |
await page.wait_for_selector('textarea[name="user-prompt"]')
|
| 164 |
print(self.tag(f"Input ready (req #{self._total_count}) โ"))
|
| 165 |
|
|
|
|
| 166 |
await self._select_model(page, model_label)
|
|
|
|
|
|
|
| 167 |
await self._send(page, prompt)
|
|
|
|
|
|
|
| 168 |
return await self._extract(page)
|
| 169 |
|
| 170 |
except Exception as e:
|
|
|
|
| 318 |
class BrowserPool:
|
| 319 |
|
| 320 |
def __init__(self):
|
| 321 |
+
self.loop = asyncio.new_event_loop()
|
| 322 |
self.workers: list[BrowserWorker] = []
|
| 323 |
+
self.ready_event = threading.Event()
|
| 324 |
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
| 325 |
self._queue: asyncio.Queue | None = None
|
| 326 |
self._total_requests = 0
|
| 327 |
+
self._rejected = 0 # ุนุฏุฏ ุงูุทูุจุงุช ุงูู
ุฑููุถุฉ ุจุณุจุจ timeout
|
| 328 |
|
| 329 |
def start(self):
|
| 330 |
self._thread.start()
|
| 331 |
+
print(f"[POOL] Starting {POOL_SIZE} browsers "
|
| 332 |
+
f"(rotation every {MAX_REQUESTS} req, queue timeout {QUEUE_TIMEOUT}s)...")
|
| 333 |
|
| 334 |
def _run(self):
|
| 335 |
asyncio.set_event_loop(self.loop)
|
|
|
|
| 350 |
|
| 351 |
async def _process(self, model_label: str, prompt: str) -> str:
|
| 352 |
self._total_requests += 1
|
| 353 |
+
|
| 354 |
+
# ุงูุชุธุฑ worker ุญุฑ โ ุจุญุฏ ุฃูุตู QUEUE_TIMEOUT ุซุงููุฉ
|
| 355 |
+
try:
|
| 356 |
+
worker: BrowserWorker = await asyncio.wait_for(
|
| 357 |
+
self._queue.get(), timeout=QUEUE_TIMEOUT
|
| 358 |
+
)
|
| 359 |
+
except asyncio.TimeoutError:
|
| 360 |
+
self._rejected += 1
|
| 361 |
+
print(f"[POOL] โ ๏ธ Queue timeout! All {POOL_SIZE} workers busy. "
|
| 362 |
+
f"Rejected: {self._rejected}")
|
| 363 |
+
raise RuntimeError(
|
| 364 |
+
f"All {POOL_SIZE} workers are busy. "
|
| 365 |
+
f"Please retry in a moment. (rejected total: {self._rejected})"
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
print(f"[POOL] Assigned W{worker.id} "
|
| 369 |
+
f"(total req: {self._total_requests}) โ")
|
| 370 |
try:
|
| 371 |
return await worker.chat(model_label, prompt)
|
| 372 |
finally:
|
|
|
|
| 379 |
future = asyncio.run_coroutine_threadsafe(
|
| 380 |
self._process(model_label, prompt), self.loop
|
| 381 |
)
|
| 382 |
+
return future.result(timeout=QUEUE_TIMEOUT + 240)
|
| 383 |
|
| 384 |
|
| 385 |
pool = BrowserPool()
|
|
|
|
| 409 |
if not content.strip():
|
| 410 |
continue
|
| 411 |
if role == "system":
|
| 412 |
+
parts.append(
|
| 413 |
+
f"=== SYSTEM INSTRUCTIONS ===\n{content}\n=== END INSTRUCTIONS ==="
|
| 414 |
+
)
|
| 415 |
elif role == "assistant":
|
| 416 |
parts.append(f"[Assistant]: {content}")
|
| 417 |
else:
|
|
|
|
| 518 |
return _make_completion(start_time, model, text, messages, tools)
|
| 519 |
except Exception as e:
|
| 520 |
print(f"[API] ERROR: {e}")
|
| 521 |
+
status = 503 if "busy" in str(e).lower() else 500
|
| 522 |
+
return JSONResponse(status_code=status, content={"error": {"message": str(e)}})
|
| 523 |
|
| 524 |
|
| 525 |
@app.post("/v1/responses")
|
|
|
|
| 578 |
}
|
| 579 |
except Exception as e:
|
| 580 |
print(f"[API] ERROR: {e}")
|
| 581 |
+
status = 503 if "busy" in str(e).lower() else 500
|
| 582 |
+
return JSONResponse(status_code=status, content={"error": {"message": str(e)}})
|
| 583 |
|
| 584 |
|
| 585 |
@app.get("/v1/models")
|
|
|
|
| 597 |
async def health():
|
| 598 |
busy = sum(1 for w in pool.workers if w.busy)
|
| 599 |
stats = [
|
| 600 |
+
{
|
| 601 |
+
"id": w.id,
|
| 602 |
+
"busy": w.busy,
|
| 603 |
+
"total_requests": w._total_count,
|
| 604 |
+
"requests_until_rotation": MAX_REQUESTS - w._request_count,
|
| 605 |
+
}
|
| 606 |
for w in pool.workers
|
| 607 |
]
|
| 608 |
return {
|
| 609 |
+
"status": "running",
|
| 610 |
+
"message": "Duck.ai API Pool Server is active!",
|
| 611 |
+
"models": ALL_MODELS,
|
| 612 |
+
"pool_size": POOL_SIZE,
|
| 613 |
+
"workers_busy": busy,
|
| 614 |
+
"workers_free": POOL_SIZE - busy,
|
| 615 |
+
"total_requests": pool._total_requests,
|
| 616 |
+
"rejected_requests": pool._rejected,
|
| 617 |
+
"queue_timeout_sec": QUEUE_TIMEOUT,
|
| 618 |
+
"workers": stats,
|
| 619 |
}
|
| 620 |
|
| 621 |
|