infinityonline commited on
Commit
bd2c875
ยท
verified ยท
1 Parent(s): 9892596

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +133 -91
main.py CHANGED
@@ -13,8 +13,6 @@ from fastapi.responses import JSONResponse
13
  # ====================================================================
14
  API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-me-secret")
15
 
16
- # key = ู…ุง ูŠุฑุณู„ู‡ ุงู„ู…ุณุชุฎุฏู… ููŠ "model"
17
- # value = ุงู„ู†ุต ุงู„ุฐูŠ ูŠุธู‡ุฑ ููŠ ุฒุฑ model-select-button ููŠ duck.ai
18
  DUCK_MODELS = {
19
  "gpt-5-mini": "GPT-5 mini",
20
  "gpt-5": "GPT-5",
@@ -26,7 +24,7 @@ DUCK_MODELS = {
26
  "mistral-small-4": "Mistral Small 4",
27
  }
28
 
29
- ALL_MODELS = list(DUCK_MODELS.keys())
30
  DEFAULT_MODEL = "gpt-5-mini"
31
 
32
  # ====================================================================
@@ -36,21 +34,22 @@ DEFAULT_MODEL = "gpt-5-mini"
36
  class AsyncBrowserThread(threading.Thread):
37
  def __init__(self):
38
  super().__init__(daemon=True)
39
- self.loop = asyncio.new_event_loop()
40
- self.ready_event = threading.Event()
41
- self.browser = None
42
- self.playwright = None
 
43
 
44
  def run(self):
45
  asyncio.set_event_loop(self.loop)
46
  self.loop.run_until_complete(self._start_browser())
47
  self.ready_event.set()
48
- print("[DUCK] Browser ready!")
49
  self.loop.run_forever()
50
 
51
  async def _start_browser(self):
52
  from playwright.async_api import async_playwright
53
- print("[DUCK] Launching Chrome...")
54
  self.playwright = await async_playwright().start()
55
  self.browser = await self.playwright.chromium.launch(
56
  headless=True,
@@ -65,14 +64,9 @@ class AsyncBrowserThread(threading.Thread):
65
  "--no-zygote",
66
  ],
67
  )
68
- print("[DUCK] Chrome launched!")
69
 
70
- async def _chat(self, model_label: str, prompt: str) -> str:
71
- """
72
- model_label: ุงู„ู†ุต ุงู„ุธุงู‡ุฑ ููŠ ูˆุงุฌู‡ุฉ duck.ai ู…ุซู„ 'GPT-5 mini'
73
- prompt: ุงู„ู†ุต ุงู„ูƒุงู…ู„ ู„ู„ุฅุฑุณุงู„
74
- """
75
- context = await self.browser.new_context(
76
  user_agent=(
77
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
78
  "AppleWebKit/537.36 (KHTML, like Gecko) "
@@ -80,118 +74,164 @@ class AsyncBrowserThread(threading.Thread):
80
  ),
81
  viewport={"width": 1920, "height": 1080},
82
  )
83
- await context.add_init_script(
84
  "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
85
  )
86
- page = await context.new_page()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  try:
89
  page.set_default_timeout(120000)
90
 
91
- # โ”€โ”€ 1. ูุชุญ duck.ai โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
92
- await page.goto("https://duckduckgo.com/aichat", wait_until="domcontentloaded")
93
- await asyncio.sleep(5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
- # โ”€โ”€ 2. ุฅุบู„ุงู‚ ุฃูŠ banner/notification โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
96
- # ุฅุบู„ุงู‚ ุฅุดุนุงุฑ PDF ุฅู† ุธู‡ุฑ
97
  try:
98
- close_btns = page.locator("button[aria-label='Close'], button.XkSxBJ8ofSQsZmGZs6qx")
99
- if await close_btns.count() > 0:
100
- await close_btns.first.click()
 
 
 
 
101
  await asyncio.sleep(1)
102
  except Exception:
103
  pass
104
 
105
- # โ”€โ”€ 3. ุงู†ุชุธุงุฑ ุตู†ุฏูˆู‚ ุงู„ูƒุชุงุจุฉ (ุงู„ู€ selector ุงู„ุญู‚ูŠู‚ูŠ) โ”€โ”€โ”€โ”€โ”€
106
- # ู…ู† HTML: <textarea name="user-prompt" ...>
107
- await page.wait_for_selector('textarea[name="user-prompt"]', timeout=30000)
108
- print("[DUCK] Chat input found โœ“")
 
109
 
110
- # โ”€โ”€ 4. ุชุบูŠูŠุฑ ุงู„ู†ู…ูˆุฐุฌ ุฅุฐุง ูƒุงู† ู…ุฎุชู„ูุงู‹ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
111
- # ู…ู† HTML: <button data-testid="model-select-button"><span>GPT-5</span>
112
  try:
113
- model_btn = page.locator('button[data-testid="model-select-button"]')
114
- current_model_text = await model_btn.inner_text()
115
- print(f"[DUCK] Current model: {current_model_text.strip()}")
116
 
117
- # ุชุญู‚ู‚ ุฅู† ูƒุงู† ุงู„ู†ู…ูˆุฐุฌ ู…ุฎุชู„ู
118
- if model_label.lower() not in current_model_text.lower():
119
  await model_btn.click()
120
  await asyncio.sleep(1.5)
121
 
122
- # ุงู„ุจุญุซ ุนู† ุงู„ู†ู…ูˆุฐุฌ ุงู„ู…ุทู„ูˆุจ ููŠ ุงู„ู‚ุงุฆู…ุฉ
123
- option = page.locator(f"li:has-text('{model_label}'), button:has-text('{model_label}'), [role='option']:has-text('{model_label}')")
 
 
 
124
  if await option.count() > 0:
125
  await option.first.click()
126
  await asyncio.sleep(1)
127
- print(f"[DUCK] Model changed to: {model_label} โœ“")
128
  else:
129
- # ุฃุบู„ู‚ ุงู„ู‚ุงุฆู…ุฉ
130
  await page.keyboard.press("Escape")
131
- print(f"[DUCK] Model '{model_label}' not found in list, using default")
132
  except Exception as e:
133
- print(f"[DUCK] Model selection error (non-fatal): {e}")
134
 
135
- # โ”€โ”€ 5. ูƒุชุงุจุฉ ุงู„ุฑุณุงู„ุฉ ูˆุฅุฑุณุงู„ู‡ุง โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
136
  textarea = page.locator('textarea[name="user-prompt"]')
137
  await textarea.click()
138
  await textarea.fill(prompt)
139
  await asyncio.sleep(0.5)
140
 
141
- # ู…ู† HTML: <button type="submit" aria-label="Send" ...>
142
- # ุฒุฑ ุงู„ุฅุฑุณุงู„ ูŠูƒูˆู† disabled=true ุญุชู‰ ูŠูˆุฌุฏ ู†ุตุŒ ุจุนุฏ fill ูŠุตุจุญ enabled
143
  send_btn = page.locator('button[type="submit"][aria-label="Send"]')
144
  await send_btn.wait_for(state="enabled", timeout=10000)
145
  await send_btn.click()
146
- print(f"[DUCK] Message sent ({len(prompt)} chars) โœ“")
147
 
148
  # โ”€โ”€ 6. ุงู†ุชุธุงุฑ ุจุฏุก ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
149
  await asyncio.sleep(3)
150
-
151
- # ุงู†ุชุธุฑ ุธู‡ูˆุฑ ุฒุฑ "Stop generating" (ูŠุนู†ูŠ ุงู„ุฑุฏ ุจุฏุฃ)
152
- # ู…ู† HTML: <button aria-label="Stop generating" ...>
153
  try:
154
  stop_btn = page.locator('button[aria-label="Stop generating"]')
155
  await stop_btn.wait_for(state="visible", timeout=20000)
156
  print("[DUCK] Response started โœ“")
157
  except Exception:
158
- print("[DUCK] Stop button not detected, waiting...")
159
 
160
  # โ”€โ”€ 7. ุงู†ุชุธุงุฑ ุงูƒุชู…ุงู„ ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
161
- # ุงู„ุฑุฏ ุงูƒุชู…ู„ ุนู†ุฏู…ุง ูŠุฎุชููŠ "Stop generating" ูˆูŠุธู‡ุฑ "Send" ู…ุฑุฉ ุฃุฎุฑู‰
162
- max_wait = 120 # ุซุงู†ูŠุฉ
163
  elapsed = 0
164
  while elapsed < max_wait:
165
  await asyncio.sleep(2)
166
  elapsed += 2
167
- # ุชุญู‚ู‚ ู‡ู„ Stop button ุงุฎุชูู‰ (= ุงู„ุฑุฏ ุงู†ุชู‡ู‰)
168
- stop_visible = await page.locator('button[aria-label="Stop generating"]:not([disabled])').count()
169
- if stop_visible == 0:
170
- print(f"[DUCK] Response complete after ~{elapsed}s โœ“")
 
171
  break
172
 
173
- await asyncio.sleep(1.5) # ุงู†ุชุธุงุฑ ุฅุถุงููŠ ู„ู„ุชุฃูƒุฏ ู…ู† ุงูƒุชู…ุงู„ ุงู„ุฑุณู…
174
 
175
- # โ”€โ”€ 8. ุงุณุชุฎุฑุงุฌ ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
176
  response_text = await page.evaluate("""
177
  () => {
178
- // duck.ai ูŠุถุน ุฑุฏูˆุฏ ุงู„ู€ AI ููŠ ุนู†ุงุตุฑ article ุฃูˆ divs ุฎุงุตุฉ
179
- // ู†ุจุญุซ ุนู† ุขุฎุฑ ุฑุฏ ููŠ ุงู„ุตูุญุฉ
180
-
181
- // ุทุฑูŠู‚ุฉ 1: article elements (ุงู„ุทุฑูŠู‚ุฉ ุงู„ุฃูƒุซุฑ ู…ูˆุซูˆู‚ูŠุฉ)
182
  const articles = document.querySelectorAll('article');
183
  if (articles.length > 0) {
184
  return articles[articles.length - 1].innerText.trim();
185
  }
186
 
187
- // ุทุฑูŠู‚ุฉ 2: divs ู…ุน role="presentation" ุฃูˆ class ุชุญุชูˆูŠ ุนู„ู‰ message
188
  const msgDivs = document.querySelectorAll(
189
- '[class*="message"]:not([class*="user"]):not([class*="User"]):not([class*="input"])'
 
190
  );
191
  if (msgDivs.length > 0) {
192
- // ูู„ุชุฑ ุงู„ุนู†ุงุตุฑ ุงู„ุชูŠ ุชุญุชูˆูŠ ุนู„ู‰ ู†ุต ุญู‚ูŠู‚ูŠ
193
  const textDivs = [...msgDivs].filter(el =>
194
- el.innerText && el.innerText.trim().length > 20 &&
 
195
  !el.querySelector('textarea')
196
  );
197
  if (textDivs.length > 0) {
@@ -199,7 +239,7 @@ class AsyncBrowserThread(threading.Thread):
199
  }
200
  }
201
 
202
- // ุทุฑูŠู‚ุฉ 3: ุงู„ุจุญุซ ุนู† ุฃุทูˆู„ div ุจุงู„ุตูุญุฉ ู„ุง ูŠุญุชูˆูŠ ุนู„ู‰ textarea
203
  const allDivs = [...document.querySelectorAll('div')].filter(el =>
204
  el.children.length < 10 &&
205
  el.innerText &&
@@ -208,16 +248,15 @@ class AsyncBrowserThread(threading.Thread):
208
  !el.querySelector('button[type="submit"]')
209
  );
210
  if (allDivs.length > 0) {
211
- // ุฃุฎุฐ ุขุฎุฑ div ุจู†ุต ุทูˆูŠู„
212
- const sorted = allDivs.sort((a, b) => b.innerText.length - a.innerText.length);
213
- return sorted[0].innerText.trim();
214
  }
215
-
216
  return '';
217
  }
218
  """)
219
 
220
- # ุฅุฐุง ูุงุฑุบุŒ ุงู†ุชุธุฑ ุฃูƒุซุฑ ูˆุญุงูˆู„ ู…ุฑุฉ ุซุงู†ูŠุฉ
221
  if not response_text or len(response_text.strip()) < 10:
222
  await asyncio.sleep(5)
223
  response_text = await page.evaluate("""
@@ -226,7 +265,6 @@ class AsyncBrowserThread(threading.Thread):
226
  if (articles.length > 0) {
227
  return articles[articles.length - 1].innerText.trim();
228
  }
229
- // ุขุฎุฑ ุญู„: ู†ุต ุงู„ุตูุญุฉ ูƒุงู…ู„ ูˆู‚ุต ุงู„ู…ู†ุงุทู‚ ุงู„ู…ุนุฑูˆูุฉ
230
  return document.body.innerText.slice(0, 5000);
231
  }
232
  """)
@@ -239,11 +277,11 @@ class AsyncBrowserThread(threading.Thread):
239
  raise RuntimeError(f"duck.ai error: {e}")
240
  finally:
241
  await page.close()
242
- await context.close()
243
 
 
244
  def process(self, model_label: str, prompt: str) -> str:
245
- if not self.ready_event.wait(timeout=90):
246
- raise RuntimeError("Browser not ready after 90s")
247
  future = asyncio.run_coroutine_threadsafe(
248
  self._chat(model_label, prompt), self.loop
249
  )
@@ -272,10 +310,6 @@ def _extract_content(msg: dict) -> str:
272
 
273
 
274
  def _build_prompt(messages: list) -> str:
275
- """
276
- ูŠุจู†ูŠ prompt ู†ุตูŠ ูˆุงุถุญ ู…ู† ู‚ุงุฆู…ุฉ messages
277
- ูŠุฏู…ุฌ system prompt + ุงู„ุชุงุฑูŠุฎ + ุงู„ุณุคุงู„ ุงู„ุฃุฎูŠุฑ
278
- """
279
  parts = []
280
  for msg in messages:
281
  role = msg.get("role", "user")
@@ -308,7 +342,7 @@ def _parse_tool_calls(text: str):
308
  raw = parsed["tool_calls"]
309
  if isinstance(raw, list) and raw:
310
  return [{
311
- "id": f"call_{uuid.uuid4().hex[:24]}",
312
  "type": "function",
313
  "function": {
314
  "name": call.get("name", ""),
@@ -386,7 +420,7 @@ async def chat_completions(request: Request):
386
  model_label = _get_model_label(model)
387
  prompt = _build_prompt(messages)
388
 
389
- print(f"[API] /v1/chat/completions โ†’ model={model} ({model_label})")
390
 
391
  try:
392
  text = await asyncio.get_event_loop().run_in_executor(
@@ -429,7 +463,7 @@ async def responses(request: Request):
429
  model_label = _get_model_label(model)
430
  prompt = _build_prompt(messages)
431
 
432
- print(f"[API] /v1/responses โ†’ model={model} ({model_label})")
433
 
434
  try:
435
  text = await asyncio.get_event_loop().run_in_executor(
@@ -442,20 +476,28 @@ async def responses(request: Request):
442
 
443
  if tc:
444
  return {
445
- "id": f"resp-{uuid.uuid4().hex[:29]}", "object": "response",
446
- "created_at": int(start_time), "model": model, "status": "completed",
 
 
 
447
  "output": [{
448
- "type": "function_call", "id": t["id"], "call_id": t["id"],
449
- "name": t["function"]["name"],
 
 
450
  "arguments": t["function"]["arguments"],
451
- "status": "completed",
452
  } for t in tc],
453
  "usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},
454
  }
455
 
456
  return {
457
- "id": f"resp-{uuid.uuid4().hex[:29]}", "object": "response",
458
- "created_at": int(start_time), "model": model, "status": "completed",
 
 
 
459
  "output": [{"type": "message", "role": "assistant",
460
  "content": [{"type": "output_text", "text": text}]}],
461
  "usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},
 
13
  # ====================================================================
14
  API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-me-secret")
15
 
 
 
16
  DUCK_MODELS = {
17
  "gpt-5-mini": "GPT-5 mini",
18
  "gpt-5": "GPT-5",
 
24
  "mistral-small-4": "Mistral Small 4",
25
  }
26
 
27
+ ALL_MODELS = list(DUCK_MODELS.keys())
28
  DEFAULT_MODEL = "gpt-5-mini"
29
 
30
  # ====================================================================
 
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,
 
64
  "--no-zygote",
65
  ],
66
  )
 
67
 
68
+ # โ”€โ”€ Context ุฏุงุฆู…: cookies ุชูุญูุธ ุจูŠู† ุงู„ู€ requests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
69
+ self.persistent_context = await self.browser.new_context(
 
 
 
 
70
  user_agent=(
71
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
72
  "AppleWebKit/537.36 (KHTML, like Gecko) "
 
74
  ),
75
  viewport={"width": 1920, "height": 1080},
76
  )
77
+ await self.persistent_context.add_init_script(
78
  "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
79
  )
80
+
81
+ # โ”€โ”€ ู‚ุจูˆู„ ุดุฑูˆุท duck.ai ู…ุฑุฉ ูˆุงุญุฏุฉ ุนู†ุฏ ุงู„ุฅู‚ู„ุงุน โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
82
+ setup_page = await self.persistent_context.new_page()
83
+ try:
84
+ print("[SERVER] Opening duck.ai for first-time setup...")
85
+ await setup_page.goto(
86
+ "https://duckduckgo.com/aichat", wait_until="domcontentloaded"
87
+ )
88
+ await asyncio.sleep(5)
89
+
90
+ # ุฒุฑ "Agree and Continue" ู…ู† ุงู„ุตูˆุฑุฉ ุงู„ุญู‚ูŠู‚ูŠุฉ
91
+ agree_btn = setup_page.locator('button:has-text("Agree and Continue")')
92
+ await agree_btn.wait_for(state="visible", timeout=12000)
93
+ await agree_btn.click()
94
+ print("[SERVER] Duck.ai terms accepted โœ“")
95
+ await asyncio.sleep(3)
96
+
97
+ # ุงู†ุชุธุฑ ุธู‡ูˆุฑ ุตู†ุฏูˆู‚ ุงู„ูƒุชุงุจุฉ = ุงู„ู…ูˆู‚ุน ุฌุงู‡ุฒ
98
+ await setup_page.wait_for_selector(
99
+ 'textarea[name="user-prompt"]', timeout=20000
100
+ )
101
+ print("[SERVER] Duck.ai chat interface ready โœ“")
102
+
103
+ except Exception as e:
104
+ print(f"[SERVER] Setup note (non-fatal): {e}")
105
+ finally:
106
+ await setup_page.close()
107
+
108
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
109
+ # ุงู„ุฏุงู„ุฉ ุงู„ุฑุฆูŠุณูŠุฉ: ุฅุฑุณุงู„ prompt ูˆุงุณุชู‚ุจุงู„ ุงู„ุฑุฏ
110
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
111
+ async def _chat(self, model_label: str, prompt: str) -> str:
112
+ # ูƒู„ request ูŠุฃุฎุฐ page ู…ู†ูุตู„ุฉ ู…ู† ู†ูุณ ุงู„ู€ context ุงู„ู…ุญููˆุธ
113
+ page = await self.persistent_context.new_page()
114
 
115
  try:
116
  page.set_default_timeout(120000)
117
 
118
+ # โ”€โ”€ 1. ูุชุญ ุตูุญุฉ ุฌุฏูŠุฏุฉ ู†ุธูŠูุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
119
+ await page.goto(
120
+ "https://duckduckgo.com/aichat", wait_until="domcontentloaded"
121
+ )
122
+ await asyncio.sleep(3)
123
+
124
+ # โ”€โ”€ 2. ุฅุบู„ุงู‚ ุฃูŠ popup/banner ุฅุถุงููŠ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
125
+ # Agree and Continue ุฅู† ุธู‡ุฑ ู…ุฌุฏุฏุงู‹ (ุงุญุชูŠุงุท)
126
+ try:
127
+ agree_btn = page.locator('button:has-text("Agree and Continue")')
128
+ if await agree_btn.count() > 0:
129
+ is_visible = await agree_btn.first.is_visible()
130
+ if is_visible:
131
+ await agree_btn.first.click()
132
+ print("[DUCK] Terms re-accepted โœ“")
133
+ await asyncio.sleep(2)
134
+ except Exception:
135
+ pass
136
 
137
+ # ุฅุบู„ุงู‚ ุฅุดุนุงุฑ PDF
 
138
  try:
139
+ close_btn = page.locator(
140
+ 'button.XkSxBJ8ofSQsZmGZs6qx, '
141
+ 'li.HmVD0odzmaobZhTx3jzd button[aria-label="Close"], '
142
+ 'button[aria-label="Close"]'
143
+ )
144
+ if await close_btn.count() > 0:
145
+ await close_btn.first.click()
146
  await asyncio.sleep(1)
147
  except Exception:
148
  pass
149
 
150
+ # โ”€โ”€ 3. ุงู†ุชุธุงุฑ ุตู†ุฏูˆู‚ ุงู„ูƒุชุงุจุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
151
+ await page.wait_for_selector(
152
+ 'textarea[name="user-prompt"]', timeout=30000
153
+ )
154
+ print("[DUCK] Input ready โœ“")
155
 
156
+ # โ”€โ”€ 4. ุชุบูŠูŠุฑ ุงู„ู†ู…ูˆุฐุฌ ุฅุฐุง ู„ุฒู… โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
157
  try:
158
+ model_btn = page.locator('button[data-testid="model-select-button"]')
159
+ current_text = await model_btn.inner_text()
160
+ print(f"[DUCK] Current model: {current_text.strip()}")
161
 
162
+ if model_label.lower() not in current_text.lower():
 
163
  await model_btn.click()
164
  await asyncio.sleep(1.5)
165
 
166
+ option = page.locator(
167
+ f"li:has-text('{model_label}'), "
168
+ f"button:has-text('{model_label}'), "
169
+ f"[role='option']:has-text('{model_label}')"
170
+ )
171
  if await option.count() > 0:
172
  await option.first.click()
173
  await asyncio.sleep(1)
174
+ print(f"[DUCK] Model โ†’ {model_label} โœ“")
175
  else:
 
176
  await page.keyboard.press("Escape")
177
+ print(f"[DUCK] Model '{model_label}' not found, using default")
178
  except Exception as e:
179
+ print(f"[DUCK] Model select (non-fatal): {e}")
180
 
181
+ # โ”€โ”€ 5. ูƒุชุงุจุฉ ูˆุฅุฑุณุงู„ ุงู„ุฑุณุงู„ุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
182
  textarea = page.locator('textarea[name="user-prompt"]')
183
  await textarea.click()
184
  await textarea.fill(prompt)
185
  await asyncio.sleep(0.5)
186
 
 
 
187
  send_btn = page.locator('button[type="submit"][aria-label="Send"]')
188
  await send_btn.wait_for(state="enabled", timeout=10000)
189
  await send_btn.click()
190
+ print(f"[DUCK] Sent ({len(prompt)} chars) โœ“")
191
 
192
  # โ”€โ”€ 6. ุงู†ุชุธุงุฑ ุจุฏุก ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
193
  await asyncio.sleep(3)
 
 
 
194
  try:
195
  stop_btn = page.locator('button[aria-label="Stop generating"]')
196
  await stop_btn.wait_for(state="visible", timeout=20000)
197
  print("[DUCK] Response started โœ“")
198
  except Exception:
199
+ print("[DUCK] Stop button not detected, continuing...")
200
 
201
  # โ”€โ”€ 7. ุงู†ุชุธุงุฑ ุงูƒุชู…ุงู„ ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
202
+ # ุงู„ุฑุฏ ุงูƒุชู…ู„ = ุฒุฑ "Stop generating" ุงุฎุชูู‰ ุฃูˆ ุฃุตุจุญ disabled
203
+ max_wait = 120
204
  elapsed = 0
205
  while elapsed < max_wait:
206
  await asyncio.sleep(2)
207
  elapsed += 2
208
+ stop_active = await page.locator(
209
+ 'button[aria-label="Stop generating"]:not([disabled])'
210
+ ).count()
211
+ if stop_active == 0:
212
+ print(f"[DUCK] Response complete (~{elapsed}s) โœ“")
213
  break
214
 
215
+ await asyncio.sleep(1.5)
216
 
217
+ # โ”€โ”€ 8. ุงุณุชุฎุฑุงุฌ ุงู„ู†ุต โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
218
  response_text = await page.evaluate("""
219
  () => {
220
+ // ุทุฑูŠู‚ุฉ 1: article elements
 
 
 
221
  const articles = document.querySelectorAll('article');
222
  if (articles.length > 0) {
223
  return articles[articles.length - 1].innerText.trim();
224
  }
225
 
226
+ // ุทุฑูŠู‚ุฉ 2: divs ุชุญุชูˆูŠ ุนู„ู‰ "message" ููŠ class
227
  const msgDivs = document.querySelectorAll(
228
+ '[class*="message"]:not([class*="user"])' +
229
+ ':not([class*="User"]):not([class*="input"])'
230
  );
231
  if (msgDivs.length > 0) {
 
232
  const textDivs = [...msgDivs].filter(el =>
233
+ el.innerText &&
234
+ el.innerText.trim().length > 20 &&
235
  !el.querySelector('textarea')
236
  );
237
  if (textDivs.length > 0) {
 
239
  }
240
  }
241
 
242
+ // ุทุฑูŠู‚ุฉ 3: ุฃุทูˆู„ div ุจุฏูˆู† textarea
243
  const allDivs = [...document.querySelectorAll('div')].filter(el =>
244
  el.children.length < 10 &&
245
  el.innerText &&
 
248
  !el.querySelector('button[type="submit"]')
249
  );
250
  if (allDivs.length > 0) {
251
+ return allDivs.sort(
252
+ (a, b) => b.innerText.length - a.innerText.length
253
+ )[0].innerText.trim();
254
  }
 
255
  return '';
256
  }
257
  """)
258
 
259
+ # โ”€โ”€ 9. fallback ุฅุฐุง ูƒุงู† ุงู„ู†ุต ูุงุฑุบุงู‹ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
260
  if not response_text or len(response_text.strip()) < 10:
261
  await asyncio.sleep(5)
262
  response_text = await page.evaluate("""
 
265
  if (articles.length > 0) {
266
  return articles[articles.length - 1].innerText.trim();
267
  }
 
268
  return document.body.innerText.slice(0, 5000);
269
  }
270
  """)
 
277
  raise RuntimeError(f"duck.ai error: {e}")
278
  finally:
279
  await page.close()
 
280
 
281
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
282
  def process(self, model_label: str, prompt: str) -> str:
283
+ if not self.ready_event.wait(timeout=120):
284
+ raise RuntimeError("Browser not ready after 120s")
285
  future = asyncio.run_coroutine_threadsafe(
286
  self._chat(model_label, prompt), self.loop
287
  )
 
310
 
311
 
312
  def _build_prompt(messages: list) -> str:
 
 
 
 
313
  parts = []
314
  for msg in messages:
315
  role = msg.get("role", "user")
 
342
  raw = parsed["tool_calls"]
343
  if isinstance(raw, list) and raw:
344
  return [{
345
+ "id": f"call_{uuid.uuid4().hex[:24]}",
346
  "type": "function",
347
  "function": {
348
  "name": call.get("name", ""),
 
420
  model_label = _get_model_label(model)
421
  prompt = _build_prompt(messages)
422
 
423
+ print(f"[API] /v1/chat/completions โ†’ {model} ({model_label})")
424
 
425
  try:
426
  text = await asyncio.get_event_loop().run_in_executor(
 
463
  model_label = _get_model_label(model)
464
  prompt = _build_prompt(messages)
465
 
466
+ print(f"[API] /v1/responses โ†’ {model} ({model_label})")
467
 
468
  try:
469
  text = await asyncio.get_event_loop().run_in_executor(
 
476
 
477
  if tc:
478
  return {
479
+ "id": f"resp-{uuid.uuid4().hex[:29]}",
480
+ "object": "response",
481
+ "created_at": int(start_time),
482
+ "model": model,
483
+ "status": "completed",
484
  "output": [{
485
+ "type": "function_call",
486
+ "id": t["id"],
487
+ "call_id": t["id"],
488
+ "name": t["function"]["name"],
489
  "arguments": t["function"]["arguments"],
490
+ "status": "completed",
491
  } for t in tc],
492
  "usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},
493
  }
494
 
495
  return {
496
+ "id": f"resp-{uuid.uuid4().hex[:29]}",
497
+ "object": "response",
498
+ "created_at": int(start_time),
499
+ "model": model,
500
+ "status": "completed",
501
  "output": [{"type": "message", "role": "assistant",
502
  "content": [{"type": "output_text", "text": text}]}],
503
  "usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},