infinityonline commited on
Commit
aca7d3d
ยท
verified ยท
1 Parent(s): 69eeb04

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +102 -53
main.py CHANGED
@@ -1,6 +1,7 @@
1
  import os
2
  import uuid
3
  import time
 
4
  import asyncio
5
  import threading
6
  import json
@@ -11,8 +12,9 @@ from fastapi.responses import JSONResponse
11
  # ====================================================================
12
  # Configuration
13
  # ====================================================================
14
- API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-me-secret")
15
- POOL_SIZE = int(os.getenv("POOL_SIZE", "3")) # ุนุฏุฏ ุงู„ู…ุชุตูุญุงุช ุงู„ู…ุชูˆุงุฒูŠุฉ
 
16
 
17
  DUCK_MODELS = {
18
  "gpt-5-mini": "GPT-5 mini",
@@ -28,29 +30,42 @@ DUCK_MODELS = {
28
  ALL_MODELS = list(DUCK_MODELS.keys())
29
  DEFAULT_MODEL = "gpt-5-mini"
30
 
 
 
 
 
 
 
 
 
 
31
 
32
  # ====================================================================
33
- # Single Browser Worker
34
  # ====================================================================
35
 
36
  class BrowserWorker:
37
- """
38
- ู…ุชุตูุญ ู…ุณุชู‚ู„ ูƒุงู…ู„ โ€” context + ู‚ุจูˆู„ ุงู„ุดุฑูˆุท.
39
- ูƒู„ worker ูŠุนู…ู„ ุจุดูƒู„ ู…ุณุชู‚ู„ ุชู…ุงู…ุงู‹ ุนู† ุงู„ุจู‚ูŠุฉ.
40
- """
41
 
42
  def __init__(self, worker_id: int, loop: asyncio.AbstractEventLoop):
43
- self.id = worker_id
44
- self.loop = loop
45
- self.context = None
46
- self.busy = False
47
- self._lock = asyncio.Lock()
 
 
 
48
 
49
  def tag(self, msg: str) -> str:
50
  return f"[W{self.id}] {msg}"
51
 
 
 
 
 
52
  async def init(self, playwright):
53
- browser = await playwright.chromium.launch(
 
54
  headless=True,
55
  channel="chrome",
56
  args=[
@@ -60,22 +75,25 @@ class BrowserWorker:
60
  "--single-process", "--no-zygote",
61
  ],
62
  )
63
- self.context = await browser.new_context(
64
- user_agent=(
65
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
66
- "AppleWebKit/537.36 (KHTML, like Gecko) "
67
- "Chrome/124.0.0.0 Safari/537.36"
68
- ),
 
 
69
  viewport={"width": 1920, "height": 1080},
70
  )
71
  await self.context.add_init_script(
72
  "Object.defineProperty(navigator,'webdriver',{get:()=>undefined})"
73
  )
 
74
 
75
- # ู‚ุจูˆู„ ุงู„ุดุฑูˆุท
76
  page = await self.context.new_page()
77
  try:
78
- print(self.tag("Opening duck.ai for setup..."))
79
  await page.goto("https://duckduckgo.com/aichat", wait_until="domcontentloaded")
80
  await asyncio.sleep(5)
81
  agree = page.locator('button:has-text("Agree and Continue")')
@@ -84,12 +102,30 @@ class BrowserWorker:
84
  print(self.tag("Terms accepted โœ“"))
85
  await asyncio.sleep(3)
86
  await page.wait_for_selector('textarea[name="user-prompt"]', timeout=20000)
87
- print(self.tag("Ready โœ“"))
88
  except Exception as e:
89
- print(self.tag(f"Setup note: {e}"))
90
  finally:
91
  await page.close()
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  async def chat(self, model_label: str, prompt: str) -> str:
94
  async with self._lock:
95
  self.busy = True
@@ -99,15 +135,24 @@ class BrowserWorker:
99
  self.busy = False
100
 
101
  async def _do_chat(self, model_label: str, prompt: str) -> str:
 
 
 
 
 
 
 
 
 
102
  page = await self.context.new_page()
103
  try:
104
  page.set_default_timeout(180000)
105
 
106
- # โ”€โ”€ 1. ูุชุญ ุงู„ุตูุญุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
107
  await page.goto("https://duckduckgo.com/aichat", wait_until="domcontentloaded")
108
  await asyncio.sleep(3)
109
 
110
- # โ”€โ”€ 2. Popup ุงุญุชูŠุงุทูŠ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
111
  try:
112
  agree = page.locator('button:has-text("Agree and Continue")')
113
  if await agree.count() > 0 and await agree.first.is_visible():
@@ -116,17 +161,17 @@ class BrowserWorker:
116
  except Exception:
117
  pass
118
 
119
- # โ”€โ”€ 3. ุงู†ุชุธุงุฑ textarea โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
120
  await page.wait_for_selector('textarea[name="user-prompt"]')
121
- print(self.tag(f"Input ready โœ“"))
122
 
123
- # โ”€โ”€ 4. ุชุบูŠูŠุฑ ุงู„ู†ู…ูˆุฐุฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
124
  await self._select_model(page, model_label)
125
 
126
- # โ”€โ”€ 5. ุฅุฑุณุงู„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
127
  await self._send(page, prompt)
128
 
129
- # โ”€โ”€ 6. ุงุณุชุฎุฑุงุฌ ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
130
  return await self._extract(page)
131
 
132
  except Exception as e:
@@ -135,6 +180,10 @@ class BrowserWorker:
135
  finally:
136
  await page.close()
137
 
 
 
 
 
138
  async def _select_model(self, page, model_label: str):
139
  try:
140
  btn = page.locator('button[data-testid="model-select-button"]')
@@ -209,18 +258,16 @@ class BrowserWorker:
209
  await ta.click()
210
  await asyncio.sleep(0.3)
211
  await page.keyboard.press("Enter")
212
- print(self.tag(f"Sent via Enter โœ“"))
213
 
214
  async def _extract(self, page) -> str:
215
  await asyncio.sleep(3)
216
 
217
- # ุงู†ุชุธุฑ ุฒุฑ Copy โ€” ูŠุธู‡ุฑ ูู‚ุท ุจุนุฏ ุงูƒุชู…ุงู„ ุงู„ุฑุฏ
218
  try:
219
  copy = page.locator('button[data-copyairesponse="true"]')
220
  await copy.last.wait_for(state="visible", timeout=90000)
221
  print(self.tag("Response complete โœ“"))
222
  except Exception:
223
- # fallback: ุงู†ุชุธุฑ ุงุฎุชูุงุก Stop button
224
  for _ in range(75):
225
  await asyncio.sleep(2)
226
  if await page.locator(
@@ -237,6 +284,8 @@ class BrowserWorker:
237
  if (active) {
238
  const sp = active.querySelector('.space-y-4');
239
  if (sp && sp.innerText.trim().length > 0) return sp.innerText.trim();
 
 
240
  }
241
  const all = document.querySelectorAll('[data-activeresponse]');
242
  if (all.length > 0) {
@@ -274,10 +323,6 @@ class BrowserWorker:
274
  # ====================================================================
275
 
276
  class BrowserPool:
277
- """
278
- Pool ู…ู† ุงู„ู…ุชุตูุญุงุช ุงู„ู…ุชูˆุงุฒูŠุฉ.
279
- POOL_SIZE = ุนุฏุฏ ุงู„ุทู„ุจุงุช ุงู„ู…ุชุฒุงู…ู†ุฉ ุงู„ู…ู…ูƒู†ุฉ.
280
- """
281
 
282
  def __init__(self):
283
  self.loop = asyncio.new_event_loop()
@@ -285,10 +330,11 @@ class BrowserPool:
285
  self.ready_event = threading.Event()
286
  self._thread = threading.Thread(target=self._run, daemon=True)
287
  self._queue: asyncio.Queue | None = None
 
288
 
289
  def start(self):
290
  self._thread.start()
291
- print(f"[POOL] Starting {POOL_SIZE} browsers...")
292
 
293
  def _run(self):
294
  asyncio.set_event_loop(self.loop)
@@ -301,24 +347,20 @@ class BrowserPool:
301
  from playwright.async_api import async_playwright
302
  self._queue = asyncio.Queue()
303
  pw = await async_playwright().start()
304
-
305
- # ุฅู†ุดุงุก ูƒู„ ุงู„ู…ุชุตูุญุงุช ุจุงู„ุชูˆุงุฒูŠ
306
  workers = [BrowserWorker(i + 1, self.loop) for i in range(POOL_SIZE)]
307
  await asyncio.gather(*[w.init(pw) for w in workers])
308
-
309
  self.workers = workers
310
  for w in workers:
311
  await self._queue.put(w)
312
 
313
  async def _process(self, model_label: str, prompt: str) -> str:
314
- # ุงู†ุชุธุฑ worker ุญุฑ ู…ู† ุงู„ู€ queue
315
  worker: BrowserWorker = await self._queue.get()
316
- print(f"[POOL] Assigned W{worker.id} โœ“")
317
  try:
318
- result = await worker.chat(model_label, prompt)
319
- return result
320
  finally:
321
- await self._queue.put(worker) # ุฃุนุฏ ุงู„ู€ worker ู„ู„ู€ pool
322
  print(f"[POOL] W{worker.id} returned to pool โœ“")
323
 
324
  def process(self, model_label: str, prompt: str) -> str:
@@ -539,14 +581,21 @@ async def list_models(request: Request):
539
  @app.get("/health")
540
  @app.get("/")
541
  async def health():
542
- busy = sum(1 for w in pool.workers if w.busy)
 
 
 
 
 
543
  return {
544
- "status": "running",
545
- "message": "Duck.ai API Pool Server is active!",
546
- "models": ALL_MODELS,
547
- "pool_size": POOL_SIZE,
548
- "workers_busy": busy,
549
- "workers_free": POOL_SIZE - busy,
 
 
550
  }
551
 
552
 
 
1
  import os
2
  import uuid
3
  import time
4
+ import random
5
  import asyncio
6
  import threading
7
  import json
 
12
  # ====================================================================
13
  # Configuration
14
  # ====================================================================
15
+ API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-me-secret")
16
+ POOL_SIZE = int(os.getenv("POOL_SIZE", "3"))
17
+ MAX_REQUESTS = int(os.getenv("MAX_REQUESTS", "50")) # ุชุฌุฏูŠุฏ context ูƒู„ N ุทู„ุจ
18
 
19
  DUCK_MODELS = {
20
  "gpt-5-mini": "GPT-5 mini",
 
30
  ALL_MODELS = list(DUCK_MODELS.keys())
31
  DEFAULT_MODEL = "gpt-5-mini"
32
 
33
+ USER_AGENTS = [
34
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
35
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
36
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
37
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
38
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
39
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
40
+ ]
41
+
42
 
43
  # ====================================================================
44
+ # Browser Worker
45
  # ====================================================================
46
 
47
  class BrowserWorker:
 
 
 
 
48
 
49
  def __init__(self, worker_id: int, loop: asyncio.AbstractEventLoop):
50
+ self.id = worker_id
51
+ self.loop = loop
52
+ self.context = None
53
+ self._browser = None
54
+ self.busy = False
55
+ self._lock = asyncio.Lock()
56
+ self._request_count = 0
57
+ self._total_count = 0
58
 
59
  def tag(self, msg: str) -> str:
60
  return f"[W{self.id}] {msg}"
61
 
62
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
63
+ # Init
64
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
65
+
66
  async def init(self, playwright):
67
+ self._playwright = playwright
68
+ self._browser = await playwright.chromium.launch(
69
  headless=True,
70
  channel="chrome",
71
  args=[
 
75
  "--single-process", "--no-zygote",
76
  ],
77
  )
78
+ await self._create_context()
79
+ await self._accept_terms()
80
+ print(self.tag("Ready โœ“"))
81
+
82
+ async def _create_context(self, ua: str = None):
83
+ user_agent = ua or random.choice(USER_AGENTS)
84
+ self.context = await self._browser.new_context(
85
+ user_agent=user_agent,
86
  viewport={"width": 1920, "height": 1080},
87
  )
88
  await self.context.add_init_script(
89
  "Object.defineProperty(navigator,'webdriver',{get:()=>undefined})"
90
  )
91
+ print(self.tag(f"Context created (UA: ...{user_agent[-30:]}) โœ“"))
92
 
93
+ async def _accept_terms(self):
94
  page = await self.context.new_page()
95
  try:
96
+ print(self.tag("Accepting terms..."))
97
  await page.goto("https://duckduckgo.com/aichat", wait_until="domcontentloaded")
98
  await asyncio.sleep(5)
99
  agree = page.locator('button:has-text("Agree and Continue")')
 
102
  print(self.tag("Terms accepted โœ“"))
103
  await asyncio.sleep(3)
104
  await page.wait_for_selector('textarea[name="user-prompt"]', timeout=20000)
 
105
  except Exception as e:
106
+ print(self.tag(f"Terms note: {e}"))
107
  finally:
108
  await page.close()
109
 
110
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
111
+ # Context Rotation
112
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
113
+
114
+ async def _rotate_context(self):
115
+ print(self.tag(f"Rotating context after {self._request_count} requests..."))
116
+ try:
117
+ await self.context.close()
118
+ except Exception:
119
+ pass
120
+ await self._create_context(ua=random.choice(USER_AGENTS))
121
+ await self._accept_terms()
122
+ self._request_count = 0
123
+ print(self.tag("Context rotated โœ“"))
124
+
125
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
126
+ # Chat
127
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
128
+
129
  async def chat(self, model_label: str, prompt: str) -> str:
130
  async with self._lock:
131
  self.busy = True
 
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
+ # โ”€โ”€ ุชุฌุฏูŠุฏ context ุฏูˆุฑูŠุงู‹ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
142
+ self._request_count += 1
143
+ self._total_count += 1
144
+ if self._request_count >= MAX_REQUESTS:
145
+ await self._rotate_context()
146
+
147
  page = await self.context.new_page()
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
  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:
 
180
  finally:
181
  await page.close()
182
 
183
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
184
+ # Helpers
185
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
186
+
187
  async def _select_model(self, page, model_label: str):
188
  try:
189
  btn = page.locator('button[data-testid="model-select-button"]')
 
258
  await ta.click()
259
  await asyncio.sleep(0.3)
260
  await page.keyboard.press("Enter")
261
+ print(self.tag("Sent via Enter โœ“"))
262
 
263
  async def _extract(self, page) -> str:
264
  await asyncio.sleep(3)
265
 
 
266
  try:
267
  copy = page.locator('button[data-copyairesponse="true"]')
268
  await copy.last.wait_for(state="visible", timeout=90000)
269
  print(self.tag("Response complete โœ“"))
270
  except Exception:
 
271
  for _ in range(75):
272
  await asyncio.sleep(2)
273
  if await page.locator(
 
284
  if (active) {
285
  const sp = active.querySelector('.space-y-4');
286
  if (sp && sp.innerText.trim().length > 0) return sp.innerText.trim();
287
+ const prose = active.querySelector('[class*="whitespace-normal"],[class*="prose"]');
288
+ if (prose && prose.innerText.trim().length > 0) return prose.innerText.trim();
289
  }
290
  const all = document.querySelectorAll('[data-activeresponse]');
291
  if (all.length > 0) {
 
323
  # ====================================================================
324
 
325
  class BrowserPool:
 
 
 
 
326
 
327
  def __init__(self):
328
  self.loop = asyncio.new_event_loop()
 
330
  self.ready_event = threading.Event()
331
  self._thread = threading.Thread(target=self._run, daemon=True)
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 (rotation every {MAX_REQUESTS} req)...")
338
 
339
  def _run(self):
340
  asyncio.set_event_loop(self.loop)
 
347
  from playwright.async_api import async_playwright
348
  self._queue = asyncio.Queue()
349
  pw = await async_playwright().start()
 
 
350
  workers = [BrowserWorker(i + 1, self.loop) for i in range(POOL_SIZE)]
351
  await asyncio.gather(*[w.init(pw) for w in workers])
 
352
  self.workers = workers
353
  for w in workers:
354
  await self._queue.put(w)
355
 
356
  async def _process(self, model_label: str, prompt: str) -> str:
357
+ self._total_requests += 1
358
  worker: BrowserWorker = await self._queue.get()
359
+ print(f"[POOL] Assigned W{worker.id} (total req: {self._total_requests}) โœ“")
360
  try:
361
+ return await worker.chat(model_label, prompt)
 
362
  finally:
363
+ await self._queue.put(worker)
364
  print(f"[POOL] W{worker.id} returned to pool โœ“")
365
 
366
  def process(self, model_label: str, prompt: str) -> str:
 
581
  @app.get("/health")
582
  @app.get("/")
583
  async def health():
584
+ busy = sum(1 for w in pool.workers if w.busy)
585
+ stats = [
586
+ {"id": w.id, "busy": w.busy, "total_requests": w._total_count,
587
+ "requests_until_rotation": MAX_REQUESTS - w._request_count}
588
+ for w in pool.workers
589
+ ]
590
  return {
591
+ "status": "running",
592
+ "message": "Duck.ai API Pool Server is active!",
593
+ "models": ALL_MODELS,
594
+ "pool_size": POOL_SIZE,
595
+ "workers_busy": busy,
596
+ "workers_free": POOL_SIZE - busy,
597
+ "total_requests": pool._total_requests,
598
+ "workers": stats,
599
  }
600
 
601