infinityonline commited on
Commit
69eeb04
·
verified ·
1 Parent(s): 157b8da

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +218 -240
main.py CHANGED
@@ -11,7 +11,8 @@ from fastapi.responses import JSONResponse
11
  # ====================================================================
12
  # Configuration
13
  # ====================================================================
14
- API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-me-secret")
 
15
 
16
  DUCK_MODELS = {
17
  "gpt-5-mini": "GPT-5 mini",
@@ -27,44 +28,39 @@ DUCK_MODELS = {
27
  ALL_MODELS = list(DUCK_MODELS.keys())
28
  DEFAULT_MODEL = "gpt-5-mini"
29
 
 
30
  # ====================================================================
31
- # Browser Engine
32
  # ====================================================================
33
 
34
- class AsyncBrowserThread(threading.Thread):
35
- def __init__(self):
36
- super().__init__(daemon=True)
37
- self.loop = asyncio.new_event_loop()
38
- self.ready_event = threading.Event()
39
- self.browser = None
40
- self.playwright = None
41
- self.persistent_context = None
42
-
43
- def run(self):
44
- asyncio.set_event_loop(self.loop)
45
- self.loop.run_until_complete(self._start_browser())
46
- self.ready_event.set()
47
- print("[SERVER] Browser + Duck.ai ready!")
48
- self.loop.run_forever()
49
 
50
- async def _start_browser(self):
51
- from playwright.async_api import async_playwright
52
- print("[SERVER] Launching Chrome...")
53
- self.playwright = await async_playwright().start()
54
- self.browser = await self.playwright.chromium.launch(
 
 
 
 
 
 
 
55
  headless=True,
56
  channel="chrome",
57
  args=[
58
  "--disable-blink-features=AutomationControlled",
59
- "--no-sandbox",
60
- "--disable-gpu",
61
- "--disable-dev-shm-usage",
62
- "--disable-setuid-sandbox",
63
- "--single-process",
64
- "--no-zygote",
65
  ],
66
  )
67
- self.persistent_context = await self.browser.new_context(
68
  user_agent=(
69
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
70
  "AppleWebKit/537.36 (KHTML, like Gecko) "
@@ -72,106 +68,124 @@ class AsyncBrowserThread(threading.Thread):
72
  ),
73
  viewport={"width": 1920, "height": 1080},
74
  )
75
- await self.persistent_context.add_init_script(
76
- "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
77
  )
78
- # قبول الشروط مرة واحدة
79
- setup_page = await self.persistent_context.new_page()
 
80
  try:
81
- print("[SERVER] Opening duck.ai for setup...")
82
- await setup_page.goto(
83
- "https://duckduckgo.com/aichat", wait_until="domcontentloaded"
84
- )
85
  await asyncio.sleep(5)
86
- agree_btn = setup_page.locator('button:has-text("Agree and Continue")')
87
- await agree_btn.wait_for(state="visible", timeout=12000)
88
- await agree_btn.click()
89
- print("[SERVER] Terms accepted ✓")
90
  await asyncio.sleep(3)
91
- await setup_page.wait_for_selector(
92
- 'textarea[name="user-prompt"]', timeout=20000
93
- )
94
- print("[SERVER] Interface ready ✓")
95
  except Exception as e:
96
- print(f"[SERVER] Setup note: {e}")
97
  finally:
98
- await setup_page.close()
99
 
100
- async def _select_model(self, page, model_label: str):
101
- """
102
- تغيير النموذج عبر:
103
- 1. Ctrl+Alt+M لفتح قائمة النماذج
104
- 2. اختيار النموذج
105
- 3. الضغط على "Start chat"
106
- """
 
 
 
107
  try:
108
- model_btn = page.locator('button[data-testid="model-select-button"]')
109
- current_text = (await model_btn.inner_text()).strip()
110
- print(f"[DUCK] Current model: {current_text}")
111
 
112
- if model_label.lower() in current_text.lower():
113
- print(f"[DUCK] Model already correct ✓")
114
- return True
115
 
116
- # فتح قائمة النماذج
117
- await model_btn.click()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  await asyncio.sleep(2)
119
 
120
- # البحث عن النموذج في القائمة
121
- # من HTML: قائمة فيها اسم النموذج كنص
122
  option = page.locator(
123
  f'li:has-text("{model_label}"), '
124
  f'[role="option"]:has-text("{model_label}"), '
125
  f'button:has-text("{model_label}")'
126
  )
127
-
128
  if await option.count() > 0:
129
  await option.first.click()
130
- print(f"[DUCK] Model selected: {model_label} ✓")
131
  await asyncio.sleep(1)
132
 
133
- # البحث عن زر "Start chat" بعد اختيار النموذج
134
- start_btn = page.locator(
135
- 'button:has-text("Start chat"), '
136
- 'button:has-text("Start new chat")'
137
  )
138
- if await start_btn.count() > 0:
139
- await start_btn.first.click()
140
- print("[DUCK] Start chat clicked ✓")
141
  await asyncio.sleep(2)
142
 
143
- # انتظر textarea بعد التغيير
144
  await page.wait_for_selector(
145
- 'textarea[name="user-prompt"]',
146
- state="visible",
147
- timeout=15000
148
  )
149
- print("[DUCK] Textarea ready after model change ✓")
150
- return True
151
  else:
152
  await page.keyboard.press("Escape")
153
- print(f"[DUCK] Model '{model_label}' not in list, using default")
154
- return False
155
 
156
  except Exception as e:
157
- print(f"[DUCK] Model select error (non-fatal): {e}")
158
- return False
159
-
160
- async def _inject_and_send(self, page, prompt: str) -> bool:
161
- """
162
- حقن النص في textarea وإرساله
163
- """
164
- # حقن النص عبر JavaScript مع React events
165
  await page.evaluate(
166
  """
167
  (text) => {
168
  const ta = document.querySelector('textarea[name="user-prompt"]');
169
  if (!ta) return;
170
- const nativeSetter = Object.getOwnPropertyDescriptor(
171
  window.HTMLTextAreaElement.prototype, 'value'
172
  ).set;
173
- nativeSetter.call(ta, text);
174
- ta.dispatchEvent(new InputEvent('input', { bubbles: true }));
175
  ta.dispatchEvent(new Event('change', { bubbles: true }));
176
  ta.focus();
177
  }
@@ -180,180 +194,144 @@ class AsyncBrowserThread(threading.Thread):
180
  )
181
  await asyncio.sleep(2)
182
 
183
- # محاولة 1: زر Submit
184
  try:
185
- send_btn = page.locator('button[type="submit"][aria-label="Send"]')
186
- is_disabled = await send_btn.get_attribute("disabled")
187
- if is_disabled is None:
188
- await send_btn.click()
189
- print(f"[DUCK] Sent via button ✓ ({len(prompt)} chars)")
190
- return True
191
  except Exception:
192
  pass
193
 
194
- # محاولة 2: Enter على الـ textarea
195
- try:
196
  ta = page.locator('textarea[name="user-prompt"]')
197
  await ta.click()
198
  await asyncio.sleep(0.3)
199
  await page.keyboard.press("Enter")
200
- print(f"[DUCK] Sent via Enter ✓ ({len(prompt)} chars)")
201
- return True
202
- except Exception as e:
203
- print(f"[DUCK] Send failed: {e}")
204
- return False
205
-
206
- async def _wait_for_response(self, page) -> str:
207
- """
208
- انتظار اكتمال الرد واستخراجه
209
- """
210
- # انتظر ظهور الرد
211
  await asyncio.sleep(3)
212
 
213
- # انتظر زر Copy بـ data-copyairesponse="true"
214
- # هذا يظهر فقط بعد اكتمال الرد الأخير
215
  try:
216
- copy_btn = page.locator('button[data-copyairesponse="true"]')
217
- await copy_btn.last.wait_for(state="visible", timeout=90000)
218
- print("[DUCK] Response complete (copy button appeared) ✓")
219
- await asyncio.sleep(1)
220
  except Exception:
221
- print("[DUCK] Copy button not detected, using fallback wait...")
222
  # fallback: انتظر اختفاء Stop button
223
- max_wait = 120
224
- elapsed = 0
225
- while elapsed < max_wait:
226
  await asyncio.sleep(2)
227
- elapsed += 2
228
- stop_active = await page.locator(
229
  'button[aria-label="Stop generating"]:not([disabled])'
230
- ).count()
231
- if stop_active == 0:
232
- print(f"[DUCK] Response complete (stop gone ~{elapsed}s) ✓")
233
  break
234
 
235
  await asyncio.sleep(1)
236
 
237
- # استخراج النص من البنية الحقيقية
238
- # من HTML: div[data-activeresponse="true"] > ... > div.space-y-4
239
- response_text = await page.evaluate("""
240
  () => {
241
- // طريقة 1: div[data-activeresponse="true"] — البنية الحقيقية لـ duck.ai
242
- const activeResp = document.querySelector('[data-activeresponse="true"]');
243
- if (activeResp) {
244
- // النص في div.space-y-4 داخل الرد
245
- const textDiv = activeResp.querySelector('.space-y-4');
246
- if (textDiv && textDiv.innerText.trim().length > 0) {
247
- return textDiv.innerText.trim();
248
- }
249
- // أو النص المباشر
250
- const prose = activeResp.querySelector(
251
- '[class*="whitespace-normal"], [class*="prose"]'
252
- );
253
- if (prose && prose.innerText.trim().length > 0) {
254
- return prose.innerText.trim();
255
- }
256
  }
257
-
258
- // طريقة 2: آخر div[data-activeresponse]
259
- const allResps = document.querySelectorAll('[data-activeresponse]');
260
- if (allResps.length > 0) {
261
- const last = allResps[allResps.length - 1];
262
- const textDiv = last.querySelector('.space-y-4');
263
- if (textDiv) return textDiv.innerText.trim();
264
  return last.innerText.trim();
265
  }
266
-
267
- // طريقة 3: article
268
- const articles = document.querySelectorAll('article');
269
- if (articles.length > 0) {
270
- return articles[articles.length - 1].innerText.trim();
271
  }
272
-
273
- // طريقة 4: div.space-y-4 الأخير
274
- const spaceDivs = document.querySelectorAll('.space-y-4');
275
- if (spaceDivs.length > 0) {
276
- for (let i = spaceDivs.length - 1; i >= 0; i--) {
277
- const text = spaceDivs[i].innerText.trim();
278
- if (text.length > 10) return text;
279
- }
280
- }
281
-
282
  return '';
283
  }
284
  """)
285
 
286
- return response_text.strip()
 
 
 
 
 
 
 
 
287
 
288
- async def _chat(self, model_label: str, prompt: str) -> str:
289
- page = await self.persistent_context.new_page()
290
- try:
291
- page.set_default_timeout(180000)
292
 
293
- # ── 1. فتح الصفحة ──────────────────────────────────────
294
- await page.goto(
295
- "https://duckduckgo.com/aichat", wait_until="domcontentloaded"
296
- )
297
- await asyncio.sleep(3)
298
 
299
- # ── 2. قبول Popup احتياطي ──────────────────────────────
300
- try:
301
- agree = page.locator('button:has-text("Agree and Continue")')
302
- if await agree.count() > 0 and await agree.first.is_visible():
303
- await agree.first.click()
304
- print("[DUCK] Terms re-accepted ✓")
305
- await asyncio.sleep(2)
306
- except Exception:
307
- pass
308
 
309
- # ── 3. انتظار textarea ─────────────────────────────────
310
- await page.wait_for_selector('textarea[name="user-prompt"]')
311
- print("[DUCK] Input ready ✓")
 
 
312
 
313
- # ── 4. تغيير النموذج ───────────────────────────────────
314
- await self._select_model(page, model_label)
 
 
 
 
315
 
316
- # ── 5. إرسال الرسالة ───────────────────────────────────
317
- sent = await self._inject_and_send(page, prompt)
318
- if not sent:
319
- raise RuntimeError("Failed to send message")
320
-
321
- # ── 6. انتظار واستخراج الرد ────────────────────────────
322
- response_text = await self._wait_for_response(page)
323
-
324
- # fallback إذا كان فارغاً
325
- if not response_text or len(response_text.strip()) < 5:
326
- await asyncio.sleep(5)
327
- response_text = await page.evaluate("""
328
- () => {
329
- const arts = document.querySelectorAll('article');
330
- if (arts.length > 0) return arts[arts.length-1].innerText.trim();
331
- const sp = document.querySelectorAll('.space-y-4');
332
- if (sp.length > 0) return sp[sp.length-1].innerText.trim();
333
- return document.body.innerText.slice(0, 5000);
334
- }
335
- """)
336
-
337
- print(f"[DUCK] Extracted {len(response_text)} chars ✓")
338
- return response_text.strip()
339
 
340
- except Exception as e:
341
- print(f"[DUCK] Error: {e}")
342
- raise RuntimeError(f"duck.ai error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  finally:
344
- await page.close()
 
345
 
346
  def process(self, model_label: str, prompt: str) -> str:
347
- if not self.ready_event.wait(timeout=120):
348
- raise RuntimeError("Browser not ready")
349
  future = asyncio.run_coroutine_threadsafe(
350
- self._chat(model_label, prompt), self.loop
351
  )
352
- return future.result(timeout=210)
353
 
354
 
355
- browser_engine = AsyncBrowserThread()
356
- browser_engine.start()
357
 
358
 
359
  # ====================================================================
@@ -363,13 +341,11 @@ browser_engine.start()
363
  def _extract_content(msg: dict) -> str:
364
  content = msg.get("content", "")
365
  if isinstance(content, list):
366
- parts = []
367
- for item in content:
368
- if isinstance(item, dict):
369
- parts.append(item.get("text", item.get("content", str(item))))
370
- else:
371
- parts.append(str(item))
372
- return "\n".join(parts)
373
  return str(content) if content else ""
374
 
375
 
@@ -381,9 +357,7 @@ def _build_prompt(messages: list) -> str:
381
  if not content.strip():
382
  continue
383
  if role == "system":
384
- parts.append(
385
- f"=== SYSTEM INSTRUCTIONS ===\n{content}\n=== END INSTRUCTIONS ==="
386
- )
387
  elif role == "assistant":
388
  parts.append(f"[Assistant]: {content}")
389
  else:
@@ -408,7 +382,7 @@ def _parse_tool_calls(text: str):
408
  raw = parsed["tool_calls"]
409
  if isinstance(raw, list) and raw:
410
  return [{
411
- "id": f"call_{uuid.uuid4().hex[:24]}",
412
  "type": "function",
413
  "function": {
414
  "name": call.get("name", ""),
@@ -485,7 +459,7 @@ async def chat_completions(request: Request):
485
  print(f"[API] /v1/chat/completions → {model} ({model_label})")
486
  try:
487
  text = await asyncio.get_event_loop().run_in_executor(
488
- None, browser_engine.process, model_label, prompt
489
  )
490
  return _make_completion(start_time, model, text, messages, tools)
491
  except Exception as e:
@@ -525,7 +499,7 @@ async def responses(request: Request):
525
  print(f"[API] /v1/responses → {model} ({model_label})")
526
  try:
527
  text = await asyncio.get_event_loop().run_in_executor(
528
- None, browser_engine.process, model_label, prompt
529
  )
530
  p = sum(len(_extract_content(m).split()) for m in messages)
531
  c = len(text.split())
@@ -565,10 +539,14 @@ async def list_models(request: Request):
565
  @app.get("/health")
566
  @app.get("/")
567
  async def health():
 
568
  return {
569
- "status": "running",
570
- "message": "Duck.ai API Server is active!",
571
- "models": ALL_MODELS,
 
 
 
572
  }
573
 
574
 
 
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
  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=[
57
  "--disable-blink-features=AutomationControlled",
58
+ "--no-sandbox", "--disable-gpu",
59
+ "--disable-dev-shm-usage", "--disable-setuid-sandbox",
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) "
 
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")')
82
+ await agree.wait_for(state="visible", timeout=15000)
83
+ await agree.click()
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
96
+ try:
97
+ return await self._do_chat(model_label, prompt)
98
+ finally:
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():
114
+ await agree.first.click()
115
+ await asyncio.sleep(2)
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:
133
+ print(self.tag(f"Error: {e}"))
134
+ raise RuntimeError(f"duck.ai error: {e}")
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"]')
141
+ text = (await btn.inner_text()).strip()
142
+ print(self.tag(f"Current model: {text}"))
143
+ if model_label.lower() in text.lower():
144
+ return
145
+
146
+ await btn.click()
147
  await asyncio.sleep(2)
148
 
 
 
149
  option = page.locator(
150
  f'li:has-text("{model_label}"), '
151
  f'[role="option"]:has-text("{model_label}"), '
152
  f'button:has-text("{model_label}")'
153
  )
 
154
  if await option.count() > 0:
155
  await option.first.click()
156
+ print(self.tag(f"Model {model_label} ✓"))
157
  await asyncio.sleep(1)
158
 
159
+ start = page.locator(
160
+ 'button:has-text("Start chat"), button:has-text("Start new chat")'
 
 
161
  )
162
+ if await start.count() > 0:
163
+ await start.first.click()
164
+ print(self.tag("Start chat ✓"))
165
  await asyncio.sleep(2)
166
 
 
167
  await page.wait_for_selector(
168
+ 'textarea[name="user-prompt"]', state="visible", timeout=15000
 
 
169
  )
170
+ print(self.tag("Textarea ready ✓"))
 
171
  else:
172
  await page.keyboard.press("Escape")
173
+ print(self.tag("Model not found, using default"))
 
174
 
175
  except Exception as e:
176
+ print(self.tag(f"Model select (non-fatal): {e}"))
177
+
178
+ async def _send(self, page, prompt: str):
 
 
 
 
 
179
  await page.evaluate(
180
  """
181
  (text) => {
182
  const ta = document.querySelector('textarea[name="user-prompt"]');
183
  if (!ta) return;
184
+ const setter = Object.getOwnPropertyDescriptor(
185
  window.HTMLTextAreaElement.prototype, 'value'
186
  ).set;
187
+ setter.call(ta, text);
188
+ ta.dispatchEvent(new InputEvent('input', { bubbles: true }));
189
  ta.dispatchEvent(new Event('change', { bubbles: true }));
190
  ta.focus();
191
  }
 
194
  )
195
  await asyncio.sleep(2)
196
 
197
+ sent = False
198
  try:
199
+ btn = page.locator('button[type="submit"][aria-label="Send"]')
200
+ if await btn.get_attribute("disabled") is None:
201
+ await btn.click()
202
+ sent = True
203
+ print(self.tag(f"Sent via button ✓ ({len(prompt)} chars)"))
 
204
  except Exception:
205
  pass
206
 
207
+ if not sent:
 
208
  ta = page.locator('textarea[name="user-prompt"]')
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(
 
227
  'button[aria-label="Stop generating"]:not([disabled])'
228
+ ).count() == 0:
229
+ print(self.tag("Response complete (fallback) ✓"))
 
230
  break
231
 
232
  await asyncio.sleep(1)
233
 
234
+ text = await page.evaluate("""
 
 
235
  () => {
236
+ const active = document.querySelector('[data-activeresponse="true"]');
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) {
243
+ const last = all[all.length - 1];
244
+ const sp = last.querySelector('.space-y-4');
245
+ if (sp) return sp.innerText.trim();
 
 
246
  return last.innerText.trim();
247
  }
248
+ const arts = document.querySelectorAll('article');
249
+ if (arts.length > 0) return arts[arts.length - 1].innerText.trim();
250
+ const divs = document.querySelectorAll('.space-y-4');
251
+ for (let i = divs.length - 1; i >= 0; i--) {
252
+ if (divs[i].innerText.trim().length > 10) return divs[i].innerText.trim();
253
  }
 
 
 
 
 
 
 
 
 
 
254
  return '';
255
  }
256
  """)
257
 
258
+ if not text or len(text.strip()) < 5:
259
+ await asyncio.sleep(5)
260
+ text = await page.evaluate("""
261
+ () => {
262
+ const sp = document.querySelectorAll('.space-y-4');
263
+ if (sp.length > 0) return sp[sp.length-1].innerText.trim();
264
+ return document.body.innerText.slice(0, 5000);
265
+ }
266
+ """)
267
 
268
+ print(self.tag(f"Extracted {len(text)} chars ✓"))
269
+ return text.strip()
 
 
270
 
 
 
 
 
 
271
 
272
+ # ====================================================================
273
+ # Browser Pool Manager
274
+ # ====================================================================
 
 
 
 
 
 
275
 
276
+ class BrowserPool:
277
+ """
278
+ Pool من المتصفحات المتوازية.
279
+ POOL_SIZE = عدد الطلبات المتزامنة الممكنة.
280
+ """
281
 
282
+ def __init__(self):
283
+ self.loop = asyncio.new_event_loop()
284
+ self.workers: list[BrowserWorker] = []
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)
295
+ self.loop.run_until_complete(self._init_pool())
296
+ self.ready_event.set()
297
+ print(f"[POOL] All {POOL_SIZE} browsers ready! ✓")
298
+ self.loop.run_forever()
299
+
300
+ async def _init_pool(self):
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:
325
+ if not self.ready_event.wait(timeout=180):
326
+ raise RuntimeError("Pool not ready")
327
  future = asyncio.run_coroutine_threadsafe(
328
+ self._process(model_label, prompt), self.loop
329
  )
330
+ return future.result(timeout=240)
331
 
332
 
333
+ pool = BrowserPool()
334
+ pool.start()
335
 
336
 
337
  # ====================================================================
 
341
  def _extract_content(msg: dict) -> str:
342
  content = msg.get("content", "")
343
  if isinstance(content, list):
344
+ return "\n".join(
345
+ item.get("text", item.get("content", str(item)))
346
+ if isinstance(item, dict) else str(item)
347
+ for item in content
348
+ )
 
 
349
  return str(content) if content else ""
350
 
351
 
 
357
  if not content.strip():
358
  continue
359
  if role == "system":
360
+ parts.append(f"=== SYSTEM INSTRUCTIONS ===\n{content}\n=== END INSTRUCTIONS ===")
 
 
361
  elif role == "assistant":
362
  parts.append(f"[Assistant]: {content}")
363
  else:
 
382
  raw = parsed["tool_calls"]
383
  if isinstance(raw, list) and raw:
384
  return [{
385
+ "id": f"call_{uuid.uuid4().hex[:24]}",
386
  "type": "function",
387
  "function": {
388
  "name": call.get("name", ""),
 
459
  print(f"[API] /v1/chat/completions → {model} ({model_label})")
460
  try:
461
  text = await asyncio.get_event_loop().run_in_executor(
462
+ None, pool.process, model_label, prompt
463
  )
464
  return _make_completion(start_time, model, text, messages, tools)
465
  except Exception as e:
 
499
  print(f"[API] /v1/responses → {model} ({model_label})")
500
  try:
501
  text = await asyncio.get_event_loop().run_in_executor(
502
+ None, pool.process, model_label, prompt
503
  )
504
  p = sum(len(_extract_content(m).split()) for m in messages)
505
  c = len(text.split())
 
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