infinityonline commited on
Commit
9892596
·
verified ·
1 Parent(s): c56debe

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +217 -312
main.py CHANGED
@@ -13,25 +13,21 @@ from fastapi.responses import JSONResponse
13
  # ====================================================================
14
  API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-me-secret")
15
 
 
 
16
  DUCK_MODELS = {
17
- "gpt-4o-mini": "gpt-4o-mini",
18
- "gpt-5-mini": "gpt-5-mini",
19
- "o3-mini": "o3-mini",
20
- "gpt-oss-120b": "gpt-oss-120b",
21
- "claude-haiku-4-5": "claude-haiku-4-5",
22
- "claude-3-haiku": "claude-3-haiku-20240307",
23
- "llama-4-scout": "meta-llama/Llama-4-Scout-17B-16E-Instruct",
24
- "llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct-Turbo",
25
- "mistral-small-4": "mistralai/Mistral-Small-3.1-24B-Instruct-2503",
26
- "mistral-small": "mistralai/Mistral-Small-24B-Instruct-2501",
27
  }
28
 
29
- ZAI_MODELS = [
30
- "GLM-5.1", "GLM-5-Turbo", "GLM-5V-Turbo",
31
- "GLM-5", "GLM-4.7", "GLM-4.6V", "GLM-4.5-Air"
32
- ]
33
-
34
- ALL_MODELS = list(DUCK_MODELS.keys()) + ZAI_MODELS
35
 
36
  # ====================================================================
37
  # Browser Engine
@@ -49,12 +45,12 @@ class AsyncBrowserThread(threading.Thread):
49
  asyncio.set_event_loop(self.loop)
50
  self.loop.run_until_complete(self._start_browser())
51
  self.ready_event.set()
52
- print("[SERVER] Browser is ready!")
53
  self.loop.run_forever()
54
 
55
  async def _start_browser(self):
56
  from playwright.async_api import async_playwright
57
- print("[SERVER] Starting Chrome...")
58
  self.playwright = await async_playwright().start()
59
  self.browser = await self.playwright.chromium.launch(
60
  headless=True,
@@ -69,12 +65,13 @@ class AsyncBrowserThread(threading.Thread):
69
  "--no-zygote",
70
  ],
71
  )
72
- print("[SERVER] Chrome launched!")
73
 
74
- # ────────────────────────────────────────────────────────────────
75
- # Duck.ai
76
- # ────────────────────────────────────────────────────────────────
77
- async def _talk_to_duck(self, model_id: str, messages: list) -> str:
 
78
  context = await self.browser.new_context(
79
  user_agent=(
80
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
@@ -87,207 +84,178 @@ class AsyncBrowserThread(threading.Thread):
87
  "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
88
  )
89
  page = await context.new_page()
 
90
  try:
91
  page.set_default_timeout(120000)
92
- prompt = _build_duck_prompt(messages)
93
 
94
- # ── 1. فتح الصفحة ──────────────────────────────────────
95
  await page.goto("https://duckduckgo.com/aichat", wait_until="domcontentloaded")
96
  await asyncio.sleep(5)
97
 
98
- # ── 2. قبول الشروط إن ظهرت ────────────────────────────
 
99
  try:
100
- btns = ["button:has-text('Got it')",
101
- "button:has-text('Accept')",
102
- "button:has-text('Get started')",
103
- "button:has-text('I agree')",
104
- "[data-testid='accept-terms']"]
105
- for btn_sel in btns:
106
- btn = page.locator(btn_sel)
107
- if await btn.count() > 0:
108
- await btn.first.click()
109
- await asyncio.sleep(2)
110
- break
111
  except Exception:
112
  pass
113
 
114
- # ── 3. الانتظار حتى يظهر صندوق الكتابة ────────────────
115
- # الـ selector المثبت من مصادر حقيقية
116
- await page.wait_for_selector("textarea[name='user-prompt']", timeout=30000)
 
117
 
118
- # ── 4. إرسال الرسالة ───────────────────────────────────
119
- textarea = page.locator("textarea[name='user-prompt']")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  await textarea.click()
121
  await textarea.fill(prompt)
122
  await asyncio.sleep(0.5)
123
 
124
- # إيجاد زر الإرسال والضغط عليه
125
- send_btn = page.locator("button[type='submit'], button[aria-label*='Send'], button[aria-label*='send']")
126
- if await send_btn.count() > 0:
127
- await send_btn.first.click()
128
- else:
129
- await page.keyboard.press("Enter")
130
-
131
- print(f"[DUCK] Sent prompt ({len(prompt)} chars)")
132
 
133
- # ── 5. انتظار بدء الرد ────────────────────────────────
134
  await asyncio.sleep(3)
135
 
136
- # انتظر وقف المؤشر الوامض (يعني انتهى الرد)
137
- # duck.ai يضيف data-copyairesponse على زر النسخ عند اكتمال الرد
138
  try:
139
- await page.wait_for_selector(
140
- "button[data-copyairesponse='true']",
141
- timeout=90000
142
- )
143
- await asyncio.sleep(1)
144
  except Exception:
145
- # fallback: انتظر ثابت
146
- await asyncio.sleep(15)
147
-
148
- # ── 6. استخراج النص ───────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  response_text = await page.evaluate("""
150
  () => {
151
- // duck.ai يضع الردود في عناصر بجانب زر النسخ
152
- // نجيب آخر رد في الصفحة
153
- const copyBtns = document.querySelectorAll("button[data-copyairesponse='true']");
154
- if (copyBtns.length > 0) {
155
- const lastCopyBtn = copyBtns[copyBtns.length - 1];
156
- // الرد في الـ parent container قبل زر النسخ
157
- const msgContainer = lastCopyBtn.closest('[class*="message"], [class*="Message"], [class*="response"], [class*="Response"]');
158
- if (msgContainer) return msgContainer.innerText.trim();
159
  }
160
 
161
- // fallback: كل العناصر التي تبدو كردود AI
162
- const allArticles = document.querySelectorAll('article, [role="article"]');
163
- if (allArticles.length > 0) {
164
- const last = allArticles[allArticles.length - 1];
165
- return last.innerText.trim();
 
 
 
 
 
 
 
 
166
  }
167
 
168
- // آخر حل: أخذ كل النصوص الطويلة
169
- const divs = [...document.querySelectorAll('div')].filter(d =>
170
- d.children.length < 5 &&
171
- d.innerText &&
172
- d.innerText.trim().length > 100
 
 
173
  );
174
- if (divs.length > 0) return divs[divs.length - 1].innerText.trim();
 
 
 
 
 
175
  return '';
176
  }
177
  """)
178
 
179
- # إذا كان النص فارغاً، انتظر أكثر وحاول مرة ثانية
180
- if not response_text or len(response_text.strip()) < 5:
181
- await asyncio.sleep(8)
182
  response_text = await page.evaluate("""
183
  () => {
184
- const copyBtns = document.querySelectorAll("button[data-copyairesponse='true']");
185
- if (copyBtns.length > 0) {
186
- const last = copyBtns[copyBtns.length - 1];
187
- let el = last.parentElement;
188
- for (let i = 0; i < 5; i++) {
189
- if (el && el.innerText && el.innerText.trim().length > 30) {
190
- return el.innerText.trim();
191
- }
192
- el = el ? el.parentElement : null;
193
- }
194
  }
195
- return document.body.innerText.slice(0, 3000);
 
196
  }
197
  """)
198
 
199
- print(f"[DUCK] Response: {len(response_text)} chars")
200
  return response_text.strip()
201
 
202
  except Exception as e:
203
  print(f"[DUCK] Error: {e}")
204
- raise e
205
- finally:
206
- await page.close()
207
- await context.close()
208
-
209
- # ────────────────────────────────────────────────────────────────
210
- # ZAI (محفوظ كما هو بالكامل)
211
- # ────────────────────────────────────────────────────────────────
212
- async def _talk_to_zai(self, prompt: str) -> str:
213
- context = await self.browser.new_context(
214
- user_agent=(
215
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
216
- "AppleWebKit/537.36 (KHTML, like Gecko) "
217
- "Chrome/124.0.0.0 Safari/537.36"
218
- ),
219
- viewport={"width": 1920, "height": 1080},
220
- )
221
- await context.add_init_script(
222
- "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
223
- )
224
- page = await context.new_page()
225
- try:
226
- page.set_default_timeout(120000)
227
- await page.goto("https://chat.z.ai/", wait_until="domcontentloaded")
228
- await asyncio.sleep(3)
229
- await page.wait_for_selector("textarea#chat-input", timeout=60000)
230
- await page.fill("textarea#chat-input", prompt)
231
- await asyncio.sleep(0.5)
232
- await page.press("textarea#chat-input", "Enter")
233
- print(f"[ZAI] Sent ({len(prompt)} chars)")
234
- await asyncio.sleep(2)
235
- await page.wait_for_selector("#response-content-container", timeout=120000)
236
- last_text = ""
237
- unchanged_cnt = 0
238
- while unchanged_cnt < 5:
239
- current_text = await page.evaluate("""
240
- () => {
241
- const prose = document.querySelector(
242
- '#response-content-container .markdown-prose'
243
- );
244
- if (!prose) return '';
245
- const clone = prose.cloneNode(true);
246
- clone.querySelectorAll(':scope > div').forEach(el => el.remove());
247
- return clone.innerText.trim();
248
- }
249
- """)
250
- if current_text == last_text and current_text.strip():
251
- unchanged_cnt += 1
252
- else:
253
- last_text = current_text
254
- unchanged_cnt = 0
255
- await asyncio.sleep(1.0)
256
- print(f"[ZAI] Response: {len(last_text)} chars")
257
- return last_text.strip()
258
- except Exception as e:
259
- print(f"[ZAI] Error: {e}")
260
- raise e
261
  finally:
262
  await page.close()
263
  await context.close()
264
 
265
- # ────────────────────────────────────────────────────────────────
266
- # Public blocking methods
267
- # ────────────────────────────────────────────────────────────────
268
- def _wait_ready(self):
269
- if not self.ready_event.wait(timeout=60):
270
- raise Exception("Browser not ready after 60s")
271
-
272
- def process_duck(self, model_id: str, messages: list) -> str:
273
- self._wait_ready()
274
  future = asyncio.run_coroutine_threadsafe(
275
- self._talk_to_duck(model_id, messages), self.loop
276
  )
277
  return future.result(timeout=180)
278
 
279
- def process_zai(self, prompt: str) -> str:
280
- self._wait_ready()
281
- future = asyncio.run_coroutine_threadsafe(self._talk_to_zai(prompt), self.loop)
282
- return future.result(timeout=120)
283
-
284
 
285
  browser_engine = AsyncBrowserThread()
286
  browser_engine.start()
287
 
288
 
289
  # ====================================================================
290
- # Prompt Helpers
291
  # ====================================================================
292
 
293
  def _extract_content(msg: dict) -> str:
@@ -303,7 +271,11 @@ def _extract_content(msg: dict) -> str:
303
  return str(content) if content else ""
304
 
305
 
306
- def _build_duck_prompt(messages: list) -> str:
 
 
 
 
307
  parts = []
308
  for msg in messages:
309
  role = msg.get("role", "user")
@@ -311,92 +283,16 @@ def _build_duck_prompt(messages: list) -> str:
311
  if not content.strip():
312
  continue
313
  if role == "system":
314
- parts.append(f"[INSTRUCTIONS]:\n{content}")
315
  elif role == "assistant":
316
- parts.append(f"[Previous AI response]:\n{content}")
317
  else:
318
  parts.append(content)
319
- if len(parts) > 1:
320
- return "\n\n---\n\n".join(parts)
321
  return "\n\n".join(parts)
322
 
323
 
324
- def format_prompt(messages, tools=None):
325
- parts = []
326
- system_parts = []
327
- has_tool_results = False
328
- user_question = ""
329
-
330
- for msg in messages:
331
- role = msg.get("role", "")
332
- msg_type = msg.get("type", "")
333
- content = _extract_content(msg)
334
-
335
- if role == "system":
336
- system_parts.append(content)
337
- elif role == "tool":
338
- has_tool_results = True
339
- parts.append(f"[TOOL RESULT from '{msg.get('name','tool')}']:\n{content}")
340
- elif msg_type == "function_call_output":
341
- has_tool_results = True
342
- parts.append(f"[TOOL RESULT (call_id: {msg.get('call_id','')})]:\n{msg.get('output', content)}")
343
- elif msg_type == "function_call":
344
- parts.append(f"[PREVIOUS TOOL CALL: Called '{msg.get('name','?')}' with arguments: {msg.get('arguments','{}')}]")
345
- elif role == "assistant":
346
- ac = content
347
- tc_list = msg.get("tool_calls", [])
348
- if tc_list:
349
- td = [f"Called '{t.get('function',{}).get('name','?')}' with: {t.get('function',{}).get('arguments','{}')}" for t in tc_list]
350
- ac += "\n[Previous tool calls: " + "; ".join(td) + "]"
351
- if ac.strip():
352
- parts.append(f"[Assistant]: {ac}")
353
- elif role == "user" or (msg_type == "message" and role != "system"):
354
- user_question = content
355
- has_tool_results = False
356
- parts.append(content)
357
- elif content:
358
- parts.append(content)
359
-
360
- final = ""
361
- if system_parts:
362
- if tools and not has_tool_results:
363
- final += "=== YOUR ROLE ===\n" + "\n\n".join(system_parts) + "\n=== END OF ROLE ===\n\n"
364
- else:
365
- final += "=== SYSTEM INSTRUCTIONS (FOLLOW STRICTLY) ===\n" + "\n\n".join(system_parts) + "\n=== END OF INSTRUCTIONS ===\n\n"
366
- if tools and not has_tool_results:
367
- final += _format_tools_instruction(tools, user_question)
368
- if has_tool_results:
369
- final += "=== CONTEXT FROM TOOLS ===\nUse ONLY this information to answer.\n\n"
370
- if parts:
371
- final += "\n".join(parts)
372
- if has_tool_results:
373
- final += "\n\n=== INSTRUCTION ===\nAnswer based ONLY on tool results above.\n"
374
- return final
375
-
376
-
377
- def _format_tools_instruction(tools, user_question=""):
378
- i = "\n=== MANDATORY TOOL USAGE ===\n"
379
- i += "You MUST use one of the tools below. Respond with ONLY valid JSON.\n\n"
380
- i += 'FORMAT: {"tool_calls": [{"name": "TOOL_NAME", "arguments": {"param": "value"}}]}\n\n'
381
- i += "Available tools:\n\n"
382
- for tool in tools:
383
- func = tool.get("function", tool)
384
- i += f"Tool: {func.get('name','unknown')}\nDescription: {func.get('description','')}\n"
385
- params = func.get("parameters", {})
386
- if params.get("properties"):
387
- i += "Parameters:\n"
388
- req = params.get("required", [])
389
- for pn, pi in params["properties"].items():
390
- i += f" - {pn} ({pi.get('type','string')}, {'required' if pn in req else 'optional'}): {pi.get('description','')}\n"
391
- i += "\n"
392
- i += "=== END OF TOOLS ===\n\n"
393
- fn = (tools[0] if tools else {}).get("function", tools[0] if tools else {}).get("name", "tool")
394
- i += f'Now respond with JSON to call the tool:\n'
395
- return i
396
-
397
-
398
- def parse_tool_calls(response_text):
399
- cleaned = response_text.strip()
400
  if "```" in cleaned:
401
  m = re.search(r'```(?:json)?\s*\n?(.*?)\n?\s*```', cleaned, re.DOTALL)
402
  if m:
@@ -416,45 +312,50 @@ def parse_tool_calls(response_text):
416
  "type": "function",
417
  "function": {
418
  "name": call.get("name", ""),
419
- "arguments": json.dumps(call.get("arguments", {}), ensure_ascii=False)
420
- if isinstance(call.get("arguments"), dict)
421
- else str(call.get("arguments", "{}"))
422
- }
 
 
423
  } for call in raw]
424
  except (json.JSONDecodeError, TypeError, KeyError):
425
  continue
426
  return None
427
 
428
 
429
- # ====================================================================
430
- # Auth & Shared response builder
431
- # ====================================================================
432
-
433
  def _auth(request: Request) -> bool:
434
- return request.headers.get("authorization", "").replace("Bearer ", "").strip() == API_SECRET_KEY
 
435
 
436
 
437
- def _is_duck(model: str) -> bool:
438
- return model in DUCK_MODELS
439
 
440
 
441
- def _completion_response(start_time, model, text, messages, tools):
442
- p = sum(len(_extract_content(m).split()) for m in messages)
443
- c = len(text.split())
444
- tc = parse_tool_calls(text) if tools else None
445
  if tc:
446
  return {
447
- "id": f"chatcmpl-{uuid.uuid4().hex[:29]}", "object": "chat.completion",
448
- "created": int(start_time), "model": model,
449
- "choices": [{"index": 0, "message": {"role": "assistant", "content": None,
450
- "tool_calls": tc}, "finish_reason": "tool_calls"}],
 
 
 
451
  "usage": {"prompt_tokens": p, "completion_tokens": c, "total_tokens": p + c},
452
  }
453
  return {
454
- "id": f"chatcmpl-{uuid.uuid4().hex[:29]}", "object": "chat.completion",
455
- "created": int(start_time), "model": model,
456
- "choices": [{"index": 0, "message": {"role": "assistant", "content": text},
457
- "finish_reason": "stop"}],
 
 
 
458
  "usage": {"prompt_tokens": p, "completion_tokens": c, "total_tokens": p + c},
459
  }
460
 
@@ -462,7 +363,7 @@ def _completion_response(start_time, model, text, messages, tools):
462
  # ====================================================================
463
  # FastAPI
464
  # ====================================================================
465
- app = FastAPI(title="ZAI + DuckAI API Server")
466
 
467
 
468
  @app.post("/v1/chat/completions")
@@ -471,6 +372,7 @@ async def chat_completions(request: Request):
471
  data = await request.json()
472
  except Exception:
473
  return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON"}})
 
474
  if not _auth(request):
475
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
476
 
@@ -478,26 +380,21 @@ async def chat_completions(request: Request):
478
  if not messages:
479
  return JSONResponse(status_code=400, content={"error": {"message": "messages required"}})
480
 
481
- model = data.get("model", "gpt-4o-mini")
482
- tools = data.get("tools", None)
483
- start_time = time.time()
 
 
 
 
484
 
485
  try:
486
- if _is_duck(model):
487
- model_id = DUCK_MODELS[model]
488
- print(f"[SERVER] duck.ai → {model_id}")
489
- text = await asyncio.get_event_loop().run_in_executor(
490
- None, browser_engine.process_duck, model_id, messages
491
- )
492
- else:
493
- prompt = format_prompt(messages, tools=tools)
494
- print(f"[SERVER] zai → {len(prompt)} chars")
495
- text = await asyncio.get_event_loop().run_in_executor(
496
- None, browser_engine.process_zai, prompt
497
- )
498
- return _completion_response(start_time, model, text, messages, tools)
499
  except Exception as e:
500
- print(f"[SERVER] ERROR: {e}")
501
  return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
502
 
503
 
@@ -507,6 +404,7 @@ async def responses(request: Request):
507
  data = await request.json()
508
  except Exception:
509
  return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON"}})
 
510
  if not _auth(request):
511
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
512
 
@@ -517,39 +415,44 @@ async def responses(request: Request):
517
  messages = input_data
518
  else:
519
  messages = data.get("messages", [])
 
520
  if not messages:
521
  return JSONResponse(status_code=400, content={"error": {"message": "input required"}})
522
 
523
- model = data.get("model", "gpt-4o-mini")
524
  tools = data.get("tools", None)
525
  instructions = data.get("instructions", "")
526
  if instructions:
527
  messages.insert(0, {"role": "system", "content": instructions})
528
 
529
- start_time = time.time()
 
 
 
 
 
530
  try:
531
- if _is_duck(model):
532
- text = await asyncio.get_event_loop().run_in_executor(
533
- None, browser_engine.process_duck, DUCK_MODELS[model], messages
534
- )
535
- else:
536
- text = await asyncio.get_event_loop().run_in_executor(
537
- None, browser_engine.process_zai, format_prompt(messages, tools=tools)
538
- )
539
 
540
  p = sum(len(_extract_content(m).split()) for m in messages)
541
  c = len(text.split())
542
- tc = parse_tool_calls(text) if tools else None
543
 
544
  if tc:
545
  return {
546
  "id": f"resp-{uuid.uuid4().hex[:29]}", "object": "response",
547
  "created_at": int(start_time), "model": model, "status": "completed",
548
- "output": [{"type": "function_call", "id": t["id"], "call_id": t["id"],
549
- "name": t["function"]["name"], "arguments": t["function"]["arguments"],
550
- "status": "completed"} for t in tc],
 
 
 
551
  "usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},
552
  }
 
553
  return {
554
  "id": f"resp-{uuid.uuid4().hex[:29]}", "object": "response",
555
  "created_at": int(start_time), "model": model, "status": "completed",
@@ -557,7 +460,9 @@ async def responses(request: Request):
557
  "content": [{"type": "output_text", "text": text}]}],
558
  "usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},
559
  }
 
560
  except Exception as e:
 
561
  return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
562
 
563
 
@@ -567,9 +472,10 @@ async def list_models(request: Request):
567
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
568
  return {
569
  "object": "list",
570
- "data": [{"id": m, "object": "model",
571
- "owned_by": "duck.ai" if m in DUCK_MODELS else "zai"}
572
- for m in ALL_MODELS],
 
573
  }
574
 
575
 
@@ -577,10 +483,9 @@ async def list_models(request: Request):
577
  @app.get("/")
578
  async def health():
579
  return {
580
- "status": "running",
581
- "message": "ZAI + DuckAI API Server is active!",
582
- "duck_models": list(DUCK_MODELS.keys()),
583
- "zai_models": ZAI_MODELS,
584
  }
585
 
586
 
 
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",
21
+ "gpt-4o-mini": "GPT-4o mini",
22
+ "o3-mini": "o3 mini",
23
+ "gpt-oss-120b": "gpt-oss 120B",
24
+ "claude-haiku-4-5": "Claude Haiku 4.5",
25
+ "llama-4-scout": "Llama 4 Scout",
26
+ "mistral-small-4": "Mistral Small 4",
 
 
27
  }
28
 
29
+ ALL_MODELS = list(DUCK_MODELS.keys())
30
+ DEFAULT_MODEL = "gpt-5-mini"
 
 
 
 
31
 
32
  # ====================================================================
33
  # Browser Engine
 
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
  "--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) "
 
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) {
198
+ return textDivs[textDivs.length - 1].innerText.trim();
199
+ }
200
  }
201
 
202
+ // طريقة 3: البحث عن أطول div بالصفحة لا يحتوي على textarea
203
+ const allDivs = [...document.querySelectorAll('div')].filter(el =>
204
+ el.children.length < 10 &&
205
+ el.innerText &&
206
+ el.innerText.trim().length > 50 &&
207
+ !el.querySelector('textarea') &&
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("""
224
  () => {
225
+ const articles = document.querySelectorAll('article');
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
  """)
233
 
234
+ print(f"[DUCK] Extracted {len(response_text)} chars")
235
  return response_text.strip()
236
 
237
  except Exception as e:
238
  print(f"[DUCK] Error: {e}")
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
  )
250
  return future.result(timeout=180)
251
 
 
 
 
 
 
252
 
253
  browser_engine = AsyncBrowserThread()
254
  browser_engine.start()
255
 
256
 
257
  # ====================================================================
258
+ # Helpers
259
  # ====================================================================
260
 
261
  def _extract_content(msg: dict) -> str:
 
271
  return str(content) if content else ""
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")
 
283
  if not content.strip():
284
  continue
285
  if role == "system":
286
+ parts.append(f"=== SYSTEM INSTRUCTIONS ===\n{content}\n=== END INSTRUCTIONS ===")
287
  elif role == "assistant":
288
+ parts.append(f"[Assistant]: {content}")
289
  else:
290
  parts.append(content)
 
 
291
  return "\n\n".join(parts)
292
 
293
 
294
+ def _parse_tool_calls(text: str):
295
+ cleaned = text.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  if "```" in cleaned:
297
  m = re.search(r'```(?:json)?\s*\n?(.*?)\n?\s*```', cleaned, re.DOTALL)
298
  if m:
 
312
  "type": "function",
313
  "function": {
314
  "name": call.get("name", ""),
315
+ "arguments": (
316
+ json.dumps(call.get("arguments", {}), ensure_ascii=False)
317
+ if isinstance(call.get("arguments"), dict)
318
+ else str(call.get("arguments", "{}"))
319
+ ),
320
+ },
321
  } for call in raw]
322
  except (json.JSONDecodeError, TypeError, KeyError):
323
  continue
324
  return None
325
 
326
 
 
 
 
 
327
  def _auth(request: Request) -> bool:
328
+ token = request.headers.get("authorization", "").replace("Bearer ", "").strip()
329
+ return token == API_SECRET_KEY
330
 
331
 
332
+ def _get_model_label(model: str) -> str:
333
+ return DUCK_MODELS.get(model, DUCK_MODELS[DEFAULT_MODEL])
334
 
335
 
336
+ def _make_completion(start_time, model, text, messages, tools=None):
337
+ p = sum(len(_extract_content(m).split()) for m in messages)
338
+ c = len(text.split())
339
+ tc = _parse_tool_calls(text) if tools else None
340
  if tc:
341
  return {
342
+ "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
343
+ "object": "chat.completion",
344
+ "created": int(start_time),
345
+ "model": model,
346
+ "choices": [{"index": 0, "message": {
347
+ "role": "assistant", "content": None, "tool_calls": tc
348
+ }, "finish_reason": "tool_calls"}],
349
  "usage": {"prompt_tokens": p, "completion_tokens": c, "total_tokens": p + c},
350
  }
351
  return {
352
+ "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
353
+ "object": "chat.completion",
354
+ "created": int(start_time),
355
+ "model": model,
356
+ "choices": [{"index": 0, "message": {
357
+ "role": "assistant", "content": text
358
+ }, "finish_reason": "stop"}],
359
  "usage": {"prompt_tokens": p, "completion_tokens": c, "total_tokens": p + c},
360
  }
361
 
 
363
  # ====================================================================
364
  # FastAPI
365
  # ====================================================================
366
+ app = FastAPI(title="Duck.ai API Server")
367
 
368
 
369
  @app.post("/v1/chat/completions")
 
372
  data = await request.json()
373
  except Exception:
374
  return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON"}})
375
+
376
  if not _auth(request):
377
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
378
 
 
380
  if not messages:
381
  return JSONResponse(status_code=400, content={"error": {"message": "messages required"}})
382
 
383
+ model = data.get("model", DEFAULT_MODEL)
384
+ tools = data.get("tools", None)
385
+ start_time = time.time()
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(
393
+ None, browser_engine.process, model_label, prompt
394
+ )
395
+ return _make_completion(start_time, model, text, messages, tools)
 
 
 
 
 
 
 
 
 
396
  except Exception as e:
397
+ print(f"[API] ERROR: {e}")
398
  return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
399
 
400
 
 
404
  data = await request.json()
405
  except Exception:
406
  return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON"}})
407
+
408
  if not _auth(request):
409
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
410
 
 
415
  messages = input_data
416
  else:
417
  messages = data.get("messages", [])
418
+
419
  if not messages:
420
  return JSONResponse(status_code=400, content={"error": {"message": "input required"}})
421
 
422
+ model = data.get("model", DEFAULT_MODEL)
423
  tools = data.get("tools", None)
424
  instructions = data.get("instructions", "")
425
  if instructions:
426
  messages.insert(0, {"role": "system", "content": instructions})
427
 
428
+ start_time = time.time()
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(
436
+ None, browser_engine.process, model_label, prompt
437
+ )
 
 
 
 
 
438
 
439
  p = sum(len(_extract_content(m).split()) for m in messages)
440
  c = len(text.split())
441
+ tc = _parse_tool_calls(text) if tools else None
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",
 
460
  "content": [{"type": "output_text", "text": text}]}],
461
  "usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},
462
  }
463
+
464
  except Exception as e:
465
+ print(f"[API] ERROR: {e}")
466
  return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
467
 
468
 
 
472
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
473
  return {
474
  "object": "list",
475
+ "data": [
476
+ {"id": m, "object": "model", "owned_by": "duck.ai"}
477
+ for m in ALL_MODELS
478
+ ],
479
  }
480
 
481
 
 
483
  @app.get("/")
484
  async def health():
485
  return {
486
+ "status": "running",
487
+ "message": "Duck.ai API Server is active!",
488
+ "models": ALL_MODELS,
 
489
  }
490
 
491