Spaces:
Running
Running
Update main.py
Browse files
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
|
| 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
|
| 40 |
-
self.ready_event
|
| 41 |
-
self.browser
|
| 42 |
-
self.playwright
|
|
|
|
| 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("[
|
| 49 |
self.loop.run_forever()
|
| 50 |
|
| 51 |
async def _start_browser(self):
|
| 52 |
from playwright.async_api import async_playwright
|
| 53 |
-
print("[
|
| 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 |
-
|
| 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
|
| 84 |
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
| 85 |
)
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
try:
|
| 89 |
page.set_default_timeout(120000)
|
| 90 |
|
| 91 |
-
# โโ 1. ูุชุญ
|
| 92 |
-
await page.goto(
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
-
#
|
| 96 |
-
# ุฅุบูุงู ุฅุดุนุงุฑ PDF ุฅู ุธูุฑ
|
| 97 |
try:
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
await asyncio.sleep(1)
|
| 102 |
except Exception:
|
| 103 |
pass
|
| 104 |
|
| 105 |
-
# โโ 3. ุงูุชุธุงุฑ ุตูุฏูู ุงููุชุงุจุฉ
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
| 109 |
|
| 110 |
-
# โโ 4. ุชุบููุฑ ุงููู
ูุฐุฌ ุฅุฐุง
|
| 111 |
-
# ู
ู HTML: <button data-testid="model-select-button"><span>GPT-5</span>
|
| 112 |
try:
|
| 113 |
-
model_btn
|
| 114 |
-
|
| 115 |
-
print(f"[DUCK] Current model: {
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 124 |
if await option.count() > 0:
|
| 125 |
await option.first.click()
|
| 126 |
await asyncio.sleep(1)
|
| 127 |
-
print(f"[DUCK] Model
|
| 128 |
else:
|
| 129 |
-
# ุฃุบูู ุงููุงุฆู
ุฉ
|
| 130 |
await page.keyboard.press("Escape")
|
| 131 |
-
print(f"[DUCK] Model '{model_label}' not found
|
| 132 |
except Exception as e:
|
| 133 |
-
print(f"[DUCK] Model
|
| 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]
|
| 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,
|
| 159 |
|
| 160 |
# โโ 7. ุงูุชุธุงุฑ ุงูุชู
ุงู ุงูุฑุฏ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 161 |
-
# ุงูุฑุฏ ุงูุชู
ู
|
| 162 |
-
max_wait = 120
|
| 163 |
elapsed = 0
|
| 164 |
while elapsed < max_wait:
|
| 165 |
await asyncio.sleep(2)
|
| 166 |
elapsed += 2
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
| 171 |
break
|
| 172 |
|
| 173 |
-
await asyncio.sleep(1.5)
|
| 174 |
|
| 175 |
-
# โโ 8. ุงุณุชุฎุฑุงุฌ ุงู
|
| 176 |
response_text = await page.evaluate("""
|
| 177 |
() => {
|
| 178 |
-
//
|
| 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
|
| 188 |
const msgDivs = document.querySelectorAll(
|
| 189 |
-
'[class*="message"]:not([class*="user"])
|
|
|
|
| 190 |
);
|
| 191 |
if (msgDivs.length > 0) {
|
| 192 |
-
// ููุชุฑ ุงูุนูุงุตุฑ ุงูุชู ุชุญุชูู ุนูู ูุต ุญูููู
|
| 193 |
const textDivs = [...msgDivs].filter(el =>
|
| 194 |
-
el.innerText &&
|
|
|
|
| 195 |
!el.querySelector('textarea')
|
| 196 |
);
|
| 197 |
if (textDivs.length > 0) {
|
|
@@ -199,7 +239,7 @@ class AsyncBrowserThread(threading.Thread):
|
|
| 199 |
}
|
| 200 |
}
|
| 201 |
|
| 202 |
-
// ุทุฑููุฉ 3:
|
| 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 |
-
|
| 212 |
-
|
| 213 |
-
|
| 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=
|
| 246 |
-
raise RuntimeError("Browser not ready after
|
| 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":
|
| 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 โ
|
| 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 โ
|
| 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":
|
| 446 |
-
"
|
|
|
|
|
|
|
|
|
|
| 447 |
"output": [{
|
| 448 |
-
"type":
|
| 449 |
-
"
|
|
|
|
|
|
|
| 450 |
"arguments": t["function"]["arguments"],
|
| 451 |
-
"status":
|
| 452 |
} for t in tc],
|
| 453 |
"usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},
|
| 454 |
}
|
| 455 |
|
| 456 |
return {
|
| 457 |
-
"id":
|
| 458 |
-
"
|
|
|
|
|
|
|
|
|
|
| 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},
|