|
|
from __future__ import annotations |
|
|
|
|
|
import json |
|
|
import base64 |
|
|
import time |
|
|
import random |
|
|
import hashlib |
|
|
import asyncio |
|
|
from aiohttp import ClientSession |
|
|
|
|
|
from ...typing import AsyncResult, Messages |
|
|
from ...errors import ResponseError |
|
|
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin |
|
|
from ..helper import format_prompt, get_last_user_message |
|
|
from ...providers.response import FinishReason, JsonConversation |
|
|
|
|
|
class Conversation(JsonConversation): |
|
|
message_history: Messages = [] |
|
|
|
|
|
def __init__(self, model: str): |
|
|
self.model = model |
|
|
self.message_history = [] |
|
|
|
|
|
class DDG(AsyncGeneratorProvider, ProviderModelMixin): |
|
|
label = "DuckDuckGo AI Chat" |
|
|
url = "https://duckduckgo.com" |
|
|
api_endpoint = "https://duckduckgo.com/duckchat/v1/chat" |
|
|
status_url = "https://duckduckgo.com/duckchat/v1/status" |
|
|
working = False |
|
|
needs_auth = False |
|
|
supports_stream = True |
|
|
supports_system_message = True |
|
|
supports_message_history = True |
|
|
|
|
|
default_model = "gpt-4o-mini" |
|
|
model_aliases = { |
|
|
"gpt-4": default_model, |
|
|
"gpt-4o": default_model, |
|
|
"llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct-Turbo", |
|
|
"claude-3-haiku": "claude-3-haiku-20240307", |
|
|
"mistral-small": "mistralai/Mistral-Small-24B-Instruct-2501", |
|
|
"mistral-small-24b": "mistralai/Mistral-Small-24B-Instruct-2501", |
|
|
} |
|
|
models = [default_model, "o3-mini"] + list(model_aliases.keys()) |
|
|
|
|
|
@staticmethod |
|
|
def generate_user_agent() -> str: |
|
|
return f"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.{random.randint(1000,9999)}.0 Safari/537.36" |
|
|
|
|
|
@staticmethod |
|
|
def generate_fe_signals() -> str: |
|
|
current_time = int(time.time() * 1000) |
|
|
signals_data = { |
|
|
"start": current_time - 35000, |
|
|
"events": [ |
|
|
{"name": "onboarding_impression_1", "delta": 383}, |
|
|
{"name": "onboarding_impression_2", "delta": 6004}, |
|
|
{"name": "onboarding_finish", "delta": 9690}, |
|
|
{"name": "startNewChat", "delta": 10082}, |
|
|
{"name": "initSwitchModel", "delta": 16586} |
|
|
], |
|
|
"end": 35163 |
|
|
} |
|
|
return base64.b64encode(json.dumps(signals_data).encode()).decode() |
|
|
|
|
|
@staticmethod |
|
|
def generate_fe_version(page_content: str = "") -> str: |
|
|
try: |
|
|
fe_hash = page_content.split('__DDG_FE_CHAT_HASH__="', 1)[1].split('"', 1)[0] |
|
|
return f"serp_20250510_052906_ET-{fe_hash}" |
|
|
except Exception: |
|
|
return "serp_20250510_052906_ET-ed4f51dc2e106020bc4b" |
|
|
|
|
|
@staticmethod |
|
|
def generate_x_vqd_hash_1(vqd: str, fe_version: str) -> str: |
|
|
|
|
|
concat = f"{vqd}#{fe_version}" |
|
|
hash_digest = hashlib.sha256(concat.encode()).digest() |
|
|
b64 = base64.b64encode(hash_digest).decode() |
|
|
return base64.b64encode(json.dumps({ |
|
|
"server_hashes": [], |
|
|
"client_hashes": [b64], |
|
|
"signals": {}, |
|
|
"meta": { |
|
|
"v": "1", |
|
|
"challenge_id": hashlib.md5(concat.encode()).hexdigest(), |
|
|
"origin": "https://duckduckgo.com", |
|
|
"stack": "Generated in Python" |
|
|
} |
|
|
}).encode()).decode() |
|
|
|
|
|
@classmethod |
|
|
async def create_async_generator( |
|
|
cls, |
|
|
model: str, |
|
|
messages: Messages, |
|
|
proxy: str = None, |
|
|
conversation: Conversation = None, |
|
|
return_conversation: bool = True, |
|
|
retry_count: int = 0, |
|
|
**kwargs |
|
|
) -> AsyncResult: |
|
|
model = cls.get_model(model) |
|
|
|
|
|
if conversation is None: |
|
|
conversation = Conversation(model) |
|
|
conversation.message_history = messages.copy() |
|
|
else: |
|
|
last_message = next((m for m in reversed(messages) if m["role"] == "user"), None) |
|
|
if last_message and last_message not in conversation.message_history: |
|
|
conversation.message_history.append(last_message) |
|
|
|
|
|
base_headers = { |
|
|
"accept-language": "en-US,en;q=0.9", |
|
|
"dnt": "1", |
|
|
"origin": "https://duckduckgo.com", |
|
|
"referer": "https://duckduckgo.com/", |
|
|
"sec-ch-ua": '"Chromium";v="135", "Not-A.Brand";v="8"', |
|
|
"sec-ch-ua-mobile": "?0", |
|
|
"sec-ch-ua-platform": '"Linux"', |
|
|
"sec-fetch-dest": "empty", |
|
|
"sec-fetch-mode": "cors", |
|
|
"sec-fetch-site": "same-origin", |
|
|
"user-agent": cls.generate_user_agent(), |
|
|
} |
|
|
cookies = {'dcs': '1', 'dcm': '3'} |
|
|
|
|
|
formatted_prompt = format_prompt(conversation.message_history) if len(conversation.message_history) > 1 else get_last_user_message(messages) |
|
|
data = {"model": model, "messages": [{"role": "user", "content": formatted_prompt}], "canUseTools": False} |
|
|
|
|
|
async with ClientSession(cookies=cookies) as session: |
|
|
try: |
|
|
|
|
|
async with session.get(f"{cls.url}/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=1", |
|
|
headers={**base_headers, "accept": "text/html"}, proxy=proxy) as r: |
|
|
r.raise_for_status() |
|
|
page = await r.text() |
|
|
fe_version = cls.generate_fe_version(page) |
|
|
|
|
|
|
|
|
status_headers = {**base_headers, "accept": "*/*", "cache-control": "no-store", "x-vqd-accept": "1"} |
|
|
async with session.get(cls.status_url, headers=status_headers, proxy=proxy) as r: |
|
|
r.raise_for_status() |
|
|
vqd = r.headers.get("x-vqd-4", "") or f"4-{random.randint(10**29, 10**30 - 1)}" |
|
|
|
|
|
x_vqd_hash_1 = cls.generate_x_vqd_hash_1(vqd, fe_version) |
|
|
|
|
|
|
|
|
chat_headers = { |
|
|
**base_headers, |
|
|
"accept": "text/event-stream", |
|
|
"content-type": "application/json", |
|
|
"x-fe-signals": cls.generate_fe_signals(), |
|
|
"x-fe-version": fe_version, |
|
|
"x-vqd-4": vqd, |
|
|
"x-vqd-hash-1": x_vqd_hash_1, |
|
|
} |
|
|
|
|
|
async with session.post(cls.api_endpoint, json=data, headers=chat_headers, proxy=proxy) as response: |
|
|
if response.status != 200: |
|
|
error_text = await response.text() |
|
|
if "ERR_BN_LIMIT" in error_text: |
|
|
raise ResponseError("Blocked by DuckDuckGo: Bot limit exceeded (ERR_BN_LIMIT).") |
|
|
if "ERR_INVALID_VQD" in error_text and retry_count < 3: |
|
|
await asyncio.sleep(random.uniform(2.5, 5.5)) |
|
|
async for chunk in cls.create_async_generator( |
|
|
model, messages, proxy, conversation, return_conversation, retry_count + 1, **kwargs |
|
|
): |
|
|
yield chunk |
|
|
return |
|
|
raise ResponseError(f"HTTP {response.status} - {error_text}") |
|
|
full_message = "" |
|
|
async for line in response.content: |
|
|
line_text = line.decode("utf-8").strip() |
|
|
if line_text.startswith("data:"): |
|
|
payload = line_text[5:].strip() |
|
|
if payload == "[DONE]": |
|
|
if full_message: |
|
|
conversation.message_history.append({"role": "assistant", "content": full_message}) |
|
|
if return_conversation: |
|
|
yield conversation |
|
|
yield FinishReason("stop") |
|
|
break |
|
|
try: |
|
|
msg = json.loads(payload) |
|
|
if msg.get("action") == "error": |
|
|
raise ResponseError(f"Error: {msg.get('type', 'unknown')}") |
|
|
if "message" in msg: |
|
|
content = msg["message"] |
|
|
yield content |
|
|
full_message += content |
|
|
except json.JSONDecodeError: |
|
|
continue |
|
|
except Exception as e: |
|
|
if retry_count < 3: |
|
|
await asyncio.sleep(random.uniform(2.5, 5.5)) |
|
|
async for chunk in cls.create_async_generator( |
|
|
model, messages, proxy, conversation, return_conversation, retry_count + 1, **kwargs |
|
|
): |
|
|
yield chunk |
|
|
else: |
|
|
raise ResponseError(f"Error: {str(e)}") |
|
|
|