DDG.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. from __future__ import annotations
  2. import json
  3. import base64
  4. import time
  5. import random
  6. import hashlib
  7. import asyncio
  8. from datetime import datetime
  9. from aiohttp import ClientSession
  10. from ..typing import AsyncResult, Messages
  11. from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
  12. from .helper import format_prompt, get_last_user_message
  13. from ..providers.response import FinishReason, JsonConversation
  14. class Conversation(JsonConversation):
  15. message_history: Messages = []
  16. def __init__(self, model: str):
  17. self.model = model
  18. self.message_history = []
  19. class DDG(AsyncGeneratorProvider, ProviderModelMixin):
  20. label = "DuckDuckGo AI Chat"
  21. url = "https://duckduckgo.com"
  22. api_endpoint = "https://duckduckgo.com/duckchat/v1/chat"
  23. status_url = "https://duckduckgo.com/duckchat/v1/status"
  24. working = True
  25. needs_auth = False
  26. supports_stream = True
  27. supports_system_message = True
  28. supports_message_history = True
  29. default_model = "gpt-4o-mini"
  30. model_aliases = {
  31. "gpt-4": default_model,
  32. "gpt-4o": default_model,
  33. "llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct-Turbo",
  34. "claude-3-haiku": "claude-3-haiku-20240307",
  35. "mistral-small-24b": "mistralai/Mistral-Small-24B-Instruct-2501",
  36. }
  37. models = [default_model, "o3-mini"] + list(model_aliases.keys())
  38. @staticmethod
  39. def generate_user_agent() -> str:
  40. 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"
  41. @staticmethod
  42. def generate_fe_signals() -> str:
  43. current_time = int(time.time() * 1000)
  44. signals_data = {
  45. "start": current_time - 35000,
  46. "events": [
  47. {"name": "onboarding_impression_1", "delta": 383},
  48. {"name": "onboarding_impression_2", "delta": 6004},
  49. {"name": "onboarding_finish", "delta": 9690},
  50. {"name": "startNewChat", "delta": 10082},
  51. {"name": "initSwitchModel", "delta": 16586}
  52. ],
  53. "end": 35163
  54. }
  55. return base64.b64encode(json.dumps(signals_data).encode()).decode()
  56. @staticmethod
  57. def generate_fe_version(page_content: str = "") -> str:
  58. try:
  59. fe_hash = page_content.split('__DDG_FE_CHAT_HASH__="', 1)[1].split('"', 1)[0]
  60. return f"serp_20250510_052906_ET-{fe_hash}"
  61. except Exception:
  62. return "serp_20250510_052906_ET-ed4f51dc2e106020bc4b"
  63. @staticmethod
  64. def generate_x_vqd_hash_1(vqd: str, fe_version: str) -> str:
  65. # Placeholder logic; in reality DuckDuckGo uses dynamic JS challenge
  66. concat = f"{vqd}#{fe_version}"
  67. hash_digest = hashlib.sha256(concat.encode()).digest()
  68. b64 = base64.b64encode(hash_digest).decode()
  69. return base64.b64encode(json.dumps({
  70. "server_hashes": [],
  71. "client_hashes": [b64],
  72. "signals": {},
  73. "meta": {
  74. "v": "1",
  75. "challenge_id": hashlib.md5(concat.encode()).hexdigest(),
  76. "origin": "https://duckduckgo.com",
  77. "stack": "Generated in Python"
  78. }
  79. }).encode()).decode()
  80. @classmethod
  81. async def create_async_generator(
  82. cls,
  83. model: str,
  84. messages: Messages,
  85. proxy: str = None,
  86. conversation: Conversation = None,
  87. return_conversation: bool = True,
  88. retry_count: int = 0,
  89. **kwargs
  90. ) -> AsyncResult:
  91. model = cls.get_model(model)
  92. if conversation is None:
  93. conversation = Conversation(model)
  94. conversation.message_history = messages.copy()
  95. else:
  96. last_message = next((m for m in reversed(messages) if m["role"] == "user"), None)
  97. if last_message and last_message not in conversation.message_history:
  98. conversation.message_history.append(last_message)
  99. base_headers = {
  100. "accept-language": "en-US,en;q=0.9",
  101. "dnt": "1",
  102. "origin": "https://duckduckgo.com",
  103. "referer": "https://duckduckgo.com/",
  104. "sec-ch-ua": '"Chromium";v="135", "Not-A.Brand";v="8"',
  105. "sec-ch-ua-mobile": "?0",
  106. "sec-ch-ua-platform": '"Linux"',
  107. "sec-fetch-dest": "empty",
  108. "sec-fetch-mode": "cors",
  109. "sec-fetch-site": "same-origin",
  110. "user-agent": cls.generate_user_agent(),
  111. }
  112. cookies = {'dcs': '1', 'dcm': '3'}
  113. formatted_prompt = format_prompt(conversation.message_history) if len(conversation.message_history) > 1 else get_last_user_message(messages)
  114. data = {"model": model, "messages": [{"role": "user", "content": formatted_prompt}], "canUseTools": False}
  115. async with ClientSession(cookies=cookies) as session:
  116. try:
  117. # Step 1: Initial page load
  118. async with session.get(f"{cls.url}/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=1",
  119. headers={**base_headers, "accept": "text/html"}, proxy=proxy) as r:
  120. r.raise_for_status()
  121. page = await r.text()
  122. fe_version = cls.generate_fe_version(page)
  123. # Step 2: Get VQD
  124. status_headers = {**base_headers, "accept": "*/*", "cache-control": "no-store", "x-vqd-accept": "1"}
  125. async with session.get(cls.status_url, headers=status_headers, proxy=proxy) as r:
  126. r.raise_for_status()
  127. vqd = r.headers.get("x-vqd-4", "") or f"4-{random.randint(10**29, 10**30 - 1)}"
  128. x_vqd_hash_1 = cls.generate_x_vqd_hash_1(vqd, fe_version)
  129. # Step 3: Actual chat request
  130. chat_headers = {
  131. **base_headers,
  132. "accept": "text/event-stream",
  133. "content-type": "application/json",
  134. "x-fe-signals": cls.generate_fe_signals(),
  135. "x-fe-version": fe_version,
  136. "x-vqd-4": vqd,
  137. "x-vqd-hash-1": x_vqd_hash_1,
  138. }
  139. async with session.post(cls.api_endpoint, json=data, headers=chat_headers, proxy=proxy) as response:
  140. if response.status != 200:
  141. error_text = await response.text()
  142. if "ERR_BN_LIMIT" in error_text:
  143. yield "Blocked by DuckDuckGo: Bot limit exceeded (ERR_BN_LIMIT)."
  144. return
  145. if "ERR_INVALID_VQD" in error_text and retry_count < 3:
  146. await asyncio.sleep(random.uniform(2.5, 5.5))
  147. async for chunk in cls.create_async_generator(
  148. model, messages, proxy, conversation, return_conversation, retry_count + 1, **kwargs
  149. ):
  150. yield chunk
  151. return
  152. yield f"Error: HTTP {response.status} - {error_text}"
  153. return
  154. full_message = ""
  155. async for line in response.content:
  156. line_text = line.decode("utf-8").strip()
  157. if line_text.startswith("data:"):
  158. payload = line_text[5:].strip()
  159. if payload == "[DONE]":
  160. if full_message:
  161. conversation.message_history.append({"role": "assistant", "content": full_message})
  162. if return_conversation:
  163. yield conversation
  164. yield FinishReason("stop")
  165. break
  166. try:
  167. msg = json.loads(payload)
  168. if msg.get("action") == "error":
  169. yield f"Error: {msg.get('type', 'unknown')}"
  170. break
  171. if "message" in msg:
  172. content = msg["message"]
  173. yield content
  174. full_message += content
  175. except json.JSONDecodeError:
  176. continue
  177. except Exception as e:
  178. if retry_count < 3:
  179. await asyncio.sleep(random.uniform(2.5, 5.5))
  180. async for chunk in cls.create_async_generator(
  181. model, messages, proxy, conversation, return_conversation, retry_count + 1, **kwargs
  182. ):
  183. yield chunk
  184. else:
  185. yield f"Error: {str(e)}"