webscout 7.7__py3-none-any.whl → 7.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of webscout might be problematic. Click here for more details.

Files changed (134) hide show
  1. webscout/AIutel.py +2 -1
  2. webscout/Bard.py +12 -29
  3. webscout/DWEBS.py +477 -461
  4. webscout/Extra/__init__.py +2 -0
  5. webscout/Extra/autocoder/__init__.py +9 -9
  6. webscout/Extra/autocoder/{rawdog.py → autocoder.py} +849 -790
  7. webscout/Extra/autocoder/autocoder_utiles.py +332 -194
  8. webscout/Extra/gguf.py +682 -682
  9. webscout/Extra/tempmail/__init__.py +26 -0
  10. webscout/Extra/tempmail/async_utils.py +141 -0
  11. webscout/Extra/tempmail/base.py +156 -0
  12. webscout/Extra/tempmail/cli.py +187 -0
  13. webscout/Extra/tempmail/mail_tm.py +361 -0
  14. webscout/Extra/tempmail/temp_mail_io.py +292 -0
  15. webscout/Provider/AI21.py +1 -1
  16. webscout/Provider/AISEARCH/DeepFind.py +2 -2
  17. webscout/Provider/AISEARCH/ISou.py +2 -2
  18. webscout/Provider/AISEARCH/felo_search.py +6 -6
  19. webscout/Provider/AISEARCH/genspark_search.py +1 -1
  20. webscout/Provider/Aitopia.py +292 -0
  21. webscout/Provider/AllenAI.py +1 -1
  22. webscout/Provider/Andi.py +3 -3
  23. webscout/Provider/C4ai.py +1 -1
  24. webscout/Provider/ChatGPTES.py +3 -5
  25. webscout/Provider/ChatGPTGratis.py +4 -4
  26. webscout/Provider/Chatify.py +2 -2
  27. webscout/Provider/Cloudflare.py +3 -2
  28. webscout/Provider/DeepSeek.py +2 -2
  29. webscout/Provider/Deepinfra.py +288 -286
  30. webscout/Provider/ElectronHub.py +709 -634
  31. webscout/Provider/ExaChat.py +325 -0
  32. webscout/Provider/Free2GPT.py +2 -2
  33. webscout/Provider/Gemini.py +167 -179
  34. webscout/Provider/GithubChat.py +1 -1
  35. webscout/Provider/Glider.py +4 -4
  36. webscout/Provider/Groq.py +41 -27
  37. webscout/Provider/HF_space/qwen_qwen2.py +1 -1
  38. webscout/Provider/HeckAI.py +1 -1
  39. webscout/Provider/HuggingFaceChat.py +1 -1
  40. webscout/Provider/Hunyuan.py +1 -1
  41. webscout/Provider/Jadve.py +3 -3
  42. webscout/Provider/Koboldai.py +3 -3
  43. webscout/Provider/LambdaChat.py +3 -2
  44. webscout/Provider/Llama.py +3 -5
  45. webscout/Provider/Llama3.py +4 -12
  46. webscout/Provider/Marcus.py +3 -3
  47. webscout/Provider/OLLAMA.py +8 -8
  48. webscout/Provider/Openai.py +7 -3
  49. webscout/Provider/PI.py +1 -1
  50. webscout/Provider/Perplexitylabs.py +1 -1
  51. webscout/Provider/Phind.py +1 -1
  52. webscout/Provider/PizzaGPT.py +1 -1
  53. webscout/Provider/QwenLM.py +4 -7
  54. webscout/Provider/TTI/FreeAIPlayground/async_freeaiplayground.py +3 -1
  55. webscout/Provider/TTI/FreeAIPlayground/sync_freeaiplayground.py +3 -3
  56. webscout/Provider/TTI/ImgSys/__init__.py +23 -0
  57. webscout/Provider/TTI/ImgSys/async_imgsys.py +202 -0
  58. webscout/Provider/TTI/ImgSys/sync_imgsys.py +195 -0
  59. webscout/Provider/TTI/__init__.py +3 -1
  60. webscout/Provider/TTI/artbit/async_artbit.py +1 -1
  61. webscout/Provider/TTI/artbit/sync_artbit.py +1 -1
  62. webscout/Provider/TTI/huggingface/async_huggingface.py +1 -1
  63. webscout/Provider/TTI/huggingface/sync_huggingface.py +1 -1
  64. webscout/Provider/TTI/piclumen/__init__.py +22 -22
  65. webscout/Provider/TTI/piclumen/sync_piclumen.py +232 -232
  66. webscout/Provider/TTI/pixelmuse/__init__.py +4 -0
  67. webscout/Provider/TTI/pixelmuse/async_pixelmuse.py +249 -0
  68. webscout/Provider/TTI/pixelmuse/sync_pixelmuse.py +182 -0
  69. webscout/Provider/TTI/talkai/sync_talkai.py +1 -1
  70. webscout/Provider/TTS/utils.py +1 -1
  71. webscout/Provider/TeachAnything.py +1 -1
  72. webscout/Provider/TextPollinationsAI.py +232 -230
  73. webscout/Provider/TwoAI.py +1 -2
  74. webscout/Provider/Venice.py +4 -2
  75. webscout/Provider/VercelAI.py +234 -0
  76. webscout/Provider/WebSim.py +3 -2
  77. webscout/Provider/WiseCat.py +10 -12
  78. webscout/Provider/Youchat.py +1 -1
  79. webscout/Provider/__init__.py +10 -4
  80. webscout/Provider/ai4chat.py +1 -1
  81. webscout/Provider/aimathgpt.py +2 -6
  82. webscout/Provider/akashgpt.py +1 -1
  83. webscout/Provider/askmyai.py +4 -4
  84. webscout/Provider/{DARKAI.py → asksteve.py} +56 -77
  85. webscout/Provider/bagoodex.py +2 -2
  86. webscout/Provider/cerebras.py +1 -1
  87. webscout/Provider/chatglm.py +4 -4
  88. webscout/Provider/cleeai.py +1 -0
  89. webscout/Provider/copilot.py +21 -9
  90. webscout/Provider/elmo.py +1 -1
  91. webscout/Provider/flowith.py +1 -1
  92. webscout/Provider/freeaichat.py +64 -31
  93. webscout/Provider/gaurish.py +3 -5
  94. webscout/Provider/geminiprorealtime.py +1 -1
  95. webscout/Provider/granite.py +4 -4
  96. webscout/Provider/hermes.py +5 -5
  97. webscout/Provider/julius.py +1 -1
  98. webscout/Provider/koala.py +1 -1
  99. webscout/Provider/lepton.py +1 -1
  100. webscout/Provider/llama3mitril.py +4 -4
  101. webscout/Provider/llamatutor.py +1 -1
  102. webscout/Provider/llmchat.py +3 -3
  103. webscout/Provider/meta.py +1 -1
  104. webscout/Provider/multichat.py +10 -10
  105. webscout/Provider/promptrefine.py +1 -1
  106. webscout/Provider/searchchat.py +293 -0
  107. webscout/Provider/sonus.py +2 -2
  108. webscout/Provider/talkai.py +2 -2
  109. webscout/Provider/turboseek.py +1 -1
  110. webscout/Provider/tutorai.py +1 -1
  111. webscout/Provider/typegpt.py +5 -42
  112. webscout/Provider/uncovr.py +312 -297
  113. webscout/Provider/x0gpt.py +1 -1
  114. webscout/Provider/yep.py +64 -12
  115. webscout/__init__.py +3 -1
  116. webscout/cli.py +59 -98
  117. webscout/conversation.py +350 -17
  118. webscout/litprinter/__init__.py +59 -667
  119. webscout/optimizers.py +419 -419
  120. webscout/tempid.py +11 -11
  121. webscout/update_checker.py +14 -12
  122. webscout/utils.py +2 -2
  123. webscout/version.py +1 -1
  124. webscout/webscout_search.py +146 -87
  125. webscout/webscout_search_async.py +148 -27
  126. {webscout-7.7.dist-info → webscout-7.9.dist-info}/METADATA +92 -66
  127. webscout-7.9.dist-info/RECORD +248 -0
  128. webscout/Provider/EDITEE.py +0 -192
  129. webscout/litprinter/colors.py +0 -54
  130. webscout-7.7.dist-info/RECORD +0 -234
  131. {webscout-7.7.dist-info → webscout-7.9.dist-info}/LICENSE.md +0 -0
  132. {webscout-7.7.dist-info → webscout-7.9.dist-info}/WHEEL +0 -0
  133. {webscout-7.7.dist-info → webscout-7.9.dist-info}/entry_points.txt +0 -0
  134. {webscout-7.7.dist-info → webscout-7.9.dist-info}/top_level.txt +0 -0
webscout/tempid.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import aiohttp
2
2
  from dataclasses import dataclass
3
- from typing import NoReturn, List, Dict, Any
3
+ from typing import NoReturn, List, Dict, Any, Optional, Union
4
4
  import requests
5
5
 
6
6
  @dataclass
@@ -19,15 +19,15 @@ class CreateEmailResponseModel:
19
19
 
20
20
  @dataclass
21
21
  class MessageResponseModel:
22
- attachments: list | None
23
- body_html: str | None
24
- body_text: str | None
25
- cc: str | None
22
+ attachments: Optional[List[Any]]
23
+ body_html: Optional[str]
24
+ body_text: Optional[str]
25
+ cc: Optional[str]
26
26
  created_at: str
27
- email_from: str | None
27
+ email_from: Optional[str]
28
28
  id: str
29
- subject: str | None
30
- email_to: str | None
29
+ subject: Optional[str]
30
+ email_to: Optional[str]
31
31
 
32
32
 
33
33
  class TempMail:
@@ -52,12 +52,12 @@ class TempMail:
52
52
  await self.close()
53
53
  return None
54
54
 
55
- async def get_domains(self) -> list[DomainModel]:
55
+ async def get_domains(self) -> List[DomainModel]:
56
56
  async with self._session.get("/api/v3/domains") as response:
57
57
  response_json = await response.json()
58
58
  return [DomainModel(domain['name'], domain['type'], domain['forward_available'], domain['forward_max_seconds']) for domain in response_json['domains']]
59
59
 
60
- async def create_email(self, alias: str | None = None, domain: str | None = None) -> CreateEmailResponseModel:
60
+ async def create_email(self, alias: Optional[str] = None, domain: Optional[str] = None) -> CreateEmailResponseModel:
61
61
  async with self._session.post("/api/v3/email/new", data={'name': alias, 'domain': domain}) as response:
62
62
  response_json = await response.json()
63
63
  return CreateEmailResponseModel(response_json['email'], response_json['token'])
@@ -69,7 +69,7 @@ class TempMail:
69
69
  else:
70
70
  return False
71
71
 
72
- async def get_messages(self, email: str) -> list[MessageResponseModel] | None:
72
+ async def get_messages(self, email: str) -> Optional[List[MessageResponseModel]]:
73
73
  async with self._session.get(f"/api/v3/email/{email}/messages") as response:
74
74
  response_json = await response.json()
75
75
  if len(response_json) == 0:
@@ -1,3 +1,6 @@
1
+ from pkg_resources import get_distribution, DistributionNotFound as PackageNotFoundError
2
+ def get_package_version(package_name: str) -> str:
3
+ return get_distribution(package_name).version
1
4
  """
2
5
  Webscout Update Checker
3
6
  >>> from webscout import check_for_updates
@@ -10,8 +13,12 @@ from typing import Optional, Dict, Any, Literal
10
13
 
11
14
  import requests
12
15
  from packaging import version
13
- from importlib.metadata import version as get_package_version
14
- from importlib.metadata import PackageNotFoundError
16
+
17
+ # Constants
18
+ PYPI_URL = "https://pypi.org/pypi/webscout/json"
19
+
20
+ # Create a session for HTTP requests
21
+ session = requests.Session()
15
22
 
16
23
  # Version comparison result type
17
24
  VersionCompareResult = Literal[-1, 0, 1]
@@ -31,8 +38,6 @@ def get_installed_version() -> Optional[str]:
31
38
  return get_package_version('webscout')
32
39
  except PackageNotFoundError:
33
40
  return None
34
- except Exception:
35
- return None
36
41
 
37
42
  def get_pypi_version() -> Optional[str]:
38
43
  """Get the latest version available on PyPI.
@@ -46,14 +51,11 @@ def get_pypi_version() -> Optional[str]:
46
51
  '2.0.0'
47
52
  """
48
53
  try:
49
- response = requests.get(
50
- "https://pypi.org/pypi/webscout/json",
51
- timeout=10
52
- )
54
+ response = session.get(PYPI_URL, timeout=10)
53
55
  response.raise_for_status()
54
56
  data: Dict[str, Any] = response.json()
55
57
  return data['info']['version']
56
- except (requests.RequestException, KeyError, ValueError, Exception):
58
+ except (requests.RequestException, KeyError, ValueError):
57
59
  return None
58
60
 
59
61
  def version_compare(v1: str, v2: str) -> VersionCompareResult:
@@ -78,7 +80,7 @@ def version_compare(v1: str, v2: str) -> VersionCompareResult:
78
80
  if version1 > version2:
79
81
  return 1
80
82
  return 0
81
- except (version.InvalidVersion, Exception):
83
+ except version.InvalidVersion:
82
84
  return 0
83
85
 
84
86
  def get_update_message(installed: str, latest: str) -> Optional[str]:
@@ -116,11 +118,11 @@ def check_for_updates() -> Optional[str]:
116
118
  installed_version = get_installed_version()
117
119
  if not installed_version:
118
120
  return None
119
-
121
+
120
122
  latest_version = get_pypi_version()
121
123
  if not latest_version:
122
124
  return None
123
-
125
+
124
126
  return get_update_message(installed_version, latest_version)
125
127
 
126
128
  if __name__ == "__main__":
webscout/utils.py CHANGED
@@ -2,7 +2,7 @@ import re
2
2
  from decimal import Decimal
3
3
  from html import unescape
4
4
  from math import atan2, cos, radians, sin, sqrt
5
- from typing import Any, Dict, List, Union
5
+ from typing import Any, Dict, List, Optional, Union
6
6
  from urllib.parse import unquote
7
7
 
8
8
  from .exceptions import WebscoutE
@@ -16,7 +16,7 @@ except ImportError:
16
16
 
17
17
  REGEX_STRIP_TAGS = re.compile("<.*?>")
18
18
 
19
- def _expand_proxy_tb_alias(proxy: str | None) -> str | None:
19
+ def _expand_proxy_tb_alias(proxy: Optional[str]) -> Optional[str]:
20
20
  """Expand "tb" to a full proxy URL if applicable."""
21
21
  return "socks5://127.0.0.1:9150" if proxy == "tb" else proxy
22
22
 
webscout/version.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "7.7"
1
+ __version__ = "7.9"
2
2
  __prog__ = "webscout"
@@ -14,6 +14,8 @@ from threading import Event
14
14
  from time import sleep, time
15
15
  from types import TracebackType
16
16
  from typing import Any, cast
17
+ import os
18
+ from typing import Literal, Iterator
17
19
 
18
20
  import primp # type: ignore
19
21
 
@@ -45,15 +47,26 @@ class WEBS:
45
47
 
46
48
  _executor: ThreadPoolExecutor = ThreadPoolExecutor()
47
49
  _impersonates = (
48
- "chrome_100", "chrome_101", "chrome_104", "chrome_105", "chrome_106", "chrome_107", "chrome_108",
49
- "chrome_109", "chrome_114", "chrome_116", "chrome_117", "chrome_118", "chrome_119", "chrome_120",
50
- #"chrome_123", "chrome_124", "chrome_126",
51
- "chrome_127", "chrome_128", "chrome_129",
52
- "safari_ios_16.5", "safari_ios_17.2", "safari_ios_17.4.1", "safari_15.3", "safari_15.5", "safari_15.6.1",
53
- "safari_16", "safari_16.5", "safari_17.0", "safari_17.2.1", "safari_17.4.1", "safari_17.5", "safari_18",
50
+ "chrome_100", "chrome_101", "chrome_104", "chrome_105", "chrome_106", "chrome_107",
51
+ "chrome_108", "chrome_109", "chrome_114", "chrome_116", "chrome_117", "chrome_118",
52
+ "chrome_119", "chrome_120", "chrome_123", "chrome_124", "chrome_126", "chrome_127",
53
+ "chrome_128", "chrome_129", "chrome_130", "chrome_131", "chrome_133",
54
+ "safari_ios_16.5", "safari_ios_17.2", "safari_ios_17.4.1", "safari_ios_18.1.1",
55
+ "safari_15.3", "safari_15.5", "safari_15.6.1", "safari_16", "safari_16.5",
56
+ "safari_17.0", "safari_17.2.1", "safari_17.4.1", "safari_17.5",
57
+ "safari_18", "safari_18.2",
54
58
  "safari_ipad_18",
55
- "edge_101", "edge_122", "edge_127",
59
+ "edge_101", "edge_122", "edge_127", "edge_131",
60
+ "firefox_109", "firefox_117", "firefox_128", "firefox_133", "firefox_135",
56
61
  ) # fmt: skip
62
+ _impersonates_os = ("android", "ios", "linux", "macos", "windows")
63
+ _chat_models = {
64
+ "gpt-4o-mini": "gpt-4o-mini",
65
+ "llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct-Turbo",
66
+ "claude-3-haiku": "claude-3-haiku-20240307",
67
+ "o3-mini": "o3-mini",
68
+ "mistral-small-3": "mistralai/Mistral-Small-24B-Instruct-2501",
69
+ }
57
70
 
58
71
  def __init__(
59
72
  self,
@@ -61,6 +74,7 @@ class WEBS:
61
74
  proxy: str | None = None,
62
75
  proxies: dict[str, str] | str | None = None, # deprecated
63
76
  timeout: int | None = 10,
77
+ verify: bool = True,
64
78
  ) -> None:
65
79
  """Initialize the WEBS object.
66
80
 
@@ -69,8 +83,10 @@ class WEBS:
69
83
  proxy (str, optional): proxy for the HTTP client, supports http/https/socks5 protocols.
70
84
  example: "http://user:pass@example.com:3128". Defaults to None.
71
85
  timeout (int, optional): Timeout value for the HTTP client. Defaults to 10.
86
+ verify (bool): SSL verification when making the request. Defaults to True.
72
87
  """
73
- self.proxy: str | None = _expand_proxy_tb_alias(proxy)
88
+ ddgs_proxy: str | None = os.environ.get("DDGS_PROXY")
89
+ self.proxy: str | None = ddgs_proxy if ddgs_proxy else _expand_proxy_tb_alias(proxy)
74
90
  assert self.proxy is None or isinstance(self.proxy, str), "proxy must be a str"
75
91
  if not proxy and proxies:
76
92
  warnings.warn("'proxies' is deprecated, use 'proxy' instead.", stacklevel=1)
@@ -100,15 +116,19 @@ class WEBS:
100
116
  cookie_store=True,
101
117
  referer=True,
102
118
  impersonate=choice(self._impersonates),
103
- follow_redirects=True,
104
- verify=False,
119
+ impersonate_os=choice(self._impersonates_os),
120
+ follow_redirects=False,
121
+ verify=verify,
105
122
  )
123
+ self.timeout = timeout
106
124
  self.sleep_timestamp = 0.0
107
125
 
108
126
  self._exception_event = Event()
109
127
  self._chat_messages: list[dict[str, str]] = []
110
128
  self._chat_tokens_count = 0
111
129
  self._chat_vqd: str = ""
130
+ self._chat_vqd_hash: str = ""
131
+ self._chat_xfe: str = ""
112
132
 
113
133
  def __enter__(self) -> WEBS:
114
134
  return self
@@ -126,114 +146,153 @@ class WEBS:
126
146
  """Get HTML parser."""
127
147
  return LHTMLParser(remove_blank_text=True, remove_comments=True, remove_pis=True, collect_ids=False)
128
148
 
129
- def _sleep(self, sleeptime: float = 2.0) -> None:
149
+ def _sleep(self, sleeptime: float = 0.75) -> None:
130
150
  """Sleep between API requests."""
131
- delay = sleeptime if not self.sleep_timestamp else sleeptime if time() - self.sleep_timestamp >= 30 else sleeptime * 2
151
+ delay = 0.0 if not self.sleep_timestamp else 0.0 if time() - self.sleep_timestamp >= 20 else sleeptime
132
152
  self.sleep_timestamp = time()
133
153
  sleep(delay)
134
154
 
135
155
  def _get_url(
136
156
  self,
137
- method: str,
157
+ method: Literal["GET", "HEAD", "OPTIONS", "DELETE", "POST", "PUT", "PATCH"],
138
158
  url: str,
139
159
  params: dict[str, str] | None = None,
140
160
  content: bytes | None = None,
141
161
  data: dict[str, str] | None = None,
142
- ) -> bytes:
143
- """Make HTTP request with proper rate limiting."""
162
+ headers: dict[str, str] | None = None,
163
+ cookies: dict[str, str] | None = None,
164
+ json: Any = None,
165
+ timeout: float | None = None,
166
+ ) -> Any:
144
167
  self._sleep()
145
168
  try:
146
- resp = self.client.request(method, url, params=params, content=content, data=data)
147
-
148
- # Add additional delay if we get a 429 or similar status
149
- if resp.status_code in (429, 403, 503):
150
- sleep(5.0) # Additional delay for rate limit responses
151
- resp = self.client.request(method, url, params=params, content=content, data=data)
152
-
169
+ resp = self.client.request(
170
+ method,
171
+ url,
172
+ params=params,
173
+ content=content,
174
+ data=data,
175
+ headers=headers,
176
+ cookies=cookies,
177
+ json=json,
178
+ timeout=timeout or self.timeout,
179
+ )
153
180
  except Exception as ex:
154
181
  if "time" in str(ex).lower():
155
182
  raise TimeoutE(f"{url} {type(ex).__name__}: {ex}") from ex
156
183
  raise WebscoutE(f"{url} {type(ex).__name__}: {ex}") from ex
157
-
158
184
  if resp.status_code == 200:
159
- return resp.content
160
- elif resp.status_code in (202, 301, 403, 429, 503):
161
- raise RatelimitE(f"{url} {resp.status_code} Ratelimit - Please wait a few minutes before retrying")
162
- raise WebscoutE(f"{url} return None. {params=} {content=} {data=}")
185
+ return resp
186
+ elif resp.status_code in (202, 301, 403, 400, 429, 418):
187
+ raise RatelimitE(f"{resp.url} {resp.status_code} Ratelimit")
188
+ raise WebscoutE(f"{resp.url} return None. {params=} {content=} {data=}")
163
189
 
164
190
  def _get_vqd(self, keywords: str) -> str:
165
191
  """Get vqd value for a search query."""
166
- resp_content = self._get_url("GET", "https://duckduckgo.com", params={"q": keywords})
192
+ resp_content = self._get_url("GET", "https://duckduckgo.com", params={"q": keywords}).content
167
193
  return _extract_vqd(resp_content, keywords)
168
194
 
169
- def chat(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30) -> str:
195
+ def chat_yield(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30) -> Iterator[str]:
170
196
  """Initiates a chat session with webscout AI.
171
197
 
172
198
  Args:
173
199
  keywords (str): The initial message or question to send to the AI.
174
- model (str): The model to use: "gpt-4o-mini", "claude-3-haiku", "llama-3.1-70b", "mixtral-8x7b".
175
- Defaults to "gpt-4o-mini".
200
+ model (str): The model to use: "gpt-4o-mini", "llama-3.3-70b", "claude-3-haiku",
201
+ "o3-mini", "mistral-small-3". Defaults to "gpt-4o-mini".
176
202
  timeout (int): Timeout value for the HTTP client. Defaults to 20.
177
203
 
178
- Returns:
179
- str: The response from the AI.
204
+ Yields:
205
+ str: Chunks of the response from the AI.
180
206
  """
181
- models_deprecated = {
182
- "gpt-3.5": "gpt-4o-mini",
183
- "llama-3.1-70b": "llama-3.3-70b",
184
- "mixtral-8x7b": "mistral-24B"
185
- }
186
- if model in models_deprecated:
187
- # logger.info(f"{model=} is deprecated, using {models_deprecated[model]}")
188
- model = models_deprecated[model]
189
- models = {
190
- "claude-3-haiku": "claude-3-haiku-20240307",
191
- "gpt-4o-mini": "gpt-4o-mini",
192
- "llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct-Turbo",
193
- "o3-mini":"o3-mini",
194
- "mistral-24B": "mistralai/Mistral-Small-24B-Instruct-2501"
195
- }
207
+ # x-fe-version
208
+ if not self._chat_xfe:
209
+ resp_content = self._get_url(
210
+ method="GET",
211
+ url="https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=1",
212
+ ).content
213
+ try:
214
+ xfe1 = resp_content.split(b'__DDG_BE_VERSION__="', maxsplit=1)[1].split(b'"', maxsplit=1)[0].decode()
215
+ xfe2 = resp_content.split(b'__DDG_FE_CHAT_HASH__="', maxsplit=1)[1].split(b'"', maxsplit=1)[0].decode()
216
+ self._chat_xfe = f"{xfe1}-{xfe2}"
217
+ except Exception as ex:
218
+ raise WebscoutE(
219
+ f"chat_yield() Error to get _chat_xfe: {type(ex).__name__}: {ex}"
220
+ ) from ex
196
221
  # vqd
197
222
  if not self._chat_vqd:
198
- resp = self.client.get("https://duckduckgo.com/duckchat/v1/status", headers={"x-vqd-accept": "1"})
223
+ resp = self._get_url(
224
+ method="GET", url="https://duckduckgo.com/duckchat/v1/status", headers={"x-vqd-accept": "1"}
225
+ )
199
226
  self._chat_vqd = resp.headers.get("x-vqd-4", "")
227
+ self._chat_vqd_hash = resp.headers.get("x-vqd-hash-1", "")
200
228
 
201
229
  self._chat_messages.append({"role": "user", "content": keywords})
202
- self._chat_tokens_count += len(keywords) // 4 if len(keywords) >= 4 else 1 # approximate number of tokens
203
-
230
+ self._chat_tokens_count += max(len(keywords) // 4, 1) # approximate number of tokens
231
+ if model not in self._chat_models:
232
+ warnings.warn(f"{model=} is unavailable. Using 'gpt-4o-mini'", stacklevel=1)
233
+ model = "gpt-4o-mini"
204
234
  json_data = {
205
- "model": models[model],
235
+ "model": self._chat_models[model],
206
236
  "messages": self._chat_messages,
207
237
  }
208
- resp = self.client.post(
209
- "https://duckduckgo.com/duckchat/v1/chat",
210
- headers={"x-vqd-4": self._chat_vqd},
238
+ resp = self._get_url(
239
+ method="POST",
240
+ url="https://duckduckgo.com/duckchat/v1/chat",
241
+ headers={
242
+ "x-fe-version": self._chat_xfe,
243
+ "x-vqd-4": self._chat_vqd,
244
+ "x-vqd-hash-1": "",
245
+ },
211
246
  json=json_data,
212
247
  timeout=timeout,
213
248
  )
214
249
  self._chat_vqd = resp.headers.get("x-vqd-4", "")
250
+ self._chat_vqd_hash = resp.headers.get("x-vqd-hash-1", "")
251
+ chunks = []
252
+ try:
253
+ for chunk in resp.stream():
254
+ lines = chunk.split(b"data:")
255
+ for line in lines:
256
+ if line := line.strip():
257
+ if line == b"[DONE]":
258
+ break
259
+ if line == b"[DONE][LIMIT_CONVERSATION]":
260
+ raise ConversationLimitException("ERR_CONVERSATION_LIMIT")
261
+ x = json_loads(line)
262
+ if isinstance(x, dict):
263
+ if x.get("action") == "error":
264
+ err_message = x.get("type", "")
265
+ if x.get("status") == 429:
266
+ raise (
267
+ ConversationLimitException(err_message)
268
+ if err_message == "ERR_CONVERSATION_LIMIT"
269
+ else RatelimitE(err_message)
270
+ )
271
+ raise WebscoutE(err_message)
272
+ elif message := x.get("message"):
273
+ chunks.append(message)
274
+ yield message
275
+ except Exception as ex:
276
+ raise WebscoutE(f"chat_yield() {type(ex).__name__}: {ex}") from ex
215
277
 
216
- data = ",".join(line.strip() for line in resp.text.rstrip("[DONE]LIMT_CVRSA\n").split("data:") if line.strip())
217
- data = json_loads("[" + data + "]")
278
+ result = "".join(chunks)
279
+ self._chat_messages.append({"role": "assistant", "content": result})
280
+ self._chat_tokens_count += len(result)
218
281
 
219
- results = []
220
- for x in data:
221
- if x.get("action") == "error":
222
- err_message = x.get("type", "")
223
- if x.get("status") == 429:
224
- raise (
225
- ConversationLimitException(err_message)
226
- if err_message == "ERR_CONVERSATION_LIMIT"
227
- else RatelimitE(err_message)
228
- )
229
- raise WebscoutE(err_message)
230
- elif message := x.get("message"):
231
- results.append(message)
232
- result = "".join(results)
282
+ def chat(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30) -> str:
283
+ """Initiates a chat session with webscout AI.
233
284
 
234
- self._chat_messages.append({"role": "assistant", "content": result})
235
- self._chat_tokens_count += len(results)
236
- return result
285
+ Args:
286
+ keywords (str): The initial message or question to send to the AI.
287
+ model (str): The model to use: "gpt-4o-mini", "llama-3.3-70b", "claude-3-haiku",
288
+ "o3-mini", "mistral-small-3". Defaults to "gpt-4o-mini".
289
+ timeout (int): Timeout value for the HTTP client. Defaults to 30.
290
+
291
+ Returns:
292
+ str: The response from the AI.
293
+ """
294
+ answer_generator = self.chat_yield(keywords, model, timeout)
295
+ return "".join(answer_generator)
237
296
 
238
297
  def text(
239
298
  self,
@@ -339,7 +398,7 @@ class WEBS:
339
398
 
340
399
  def _text_api_page(s: int) -> list[dict[str, str]]:
341
400
  payload["s"] = f"{s}"
342
- resp_content = self._get_url("GET", "https://links.duckduckgo.com/d.js", params=payload)
401
+ resp_content = self._get_url("GET", "https://links.duckduckgo.com/d.js", params=payload).content
343
402
  page_data = _text_extract_json(resp_content, keywords)
344
403
  page_results = []
345
404
  for row in page_data:
@@ -413,7 +472,7 @@ class WEBS:
413
472
 
414
473
  def _text_html_page(s: int) -> list[dict[str, str]]:
415
474
  payload["s"] = f"{s}"
416
- resp_content = self._get_url("POST", "https://html.duckduckgo.com/html", data=payload)
475
+ resp_content = self._get_url("POST", "https://html.duckduckgo.com/html", data=payload).content
417
476
  if b"No results." in resp_content:
418
477
  return []
419
478
 
@@ -500,7 +559,7 @@ class WEBS:
500
559
 
501
560
  def _text_lite_page(s: int) -> list[dict[str, str]]:
502
561
  payload["s"] = f"{s}"
503
- resp_content = self._get_url("POST", "https://lite.duckduckgo.com/lite/", data=payload)
562
+ resp_content = self._get_url("POST", "https://lite.duckduckgo.com/lite/", data=payload).content
504
563
  if b"No more results." in resp_content:
505
564
  return []
506
565
 
@@ -621,7 +680,7 @@ class WEBS:
621
680
 
622
681
  def _images_page(s: int) -> list[dict[str, str]]:
623
682
  payload["s"] = f"{s}"
624
- resp_content = self._get_url("GET", "https://duckduckgo.com/i.js", params=payload)
683
+ resp_content = self._get_url("GET", "https://duckduckgo.com/i.js", params=payload).content
625
684
  resp_json = json_loads(resp_content)
626
685
 
627
686
  page_data = resp_json.get("results", [])
@@ -708,7 +767,7 @@ class WEBS:
708
767
 
709
768
  def _videos_page(s: int) -> list[dict[str, str]]:
710
769
  payload["s"] = f"{s}"
711
- resp_content = self._get_url("GET", "https://duckduckgo.com/v.js", params=payload)
770
+ resp_content = self._get_url("GET", "https://duckduckgo.com/v.js", params=payload).content
712
771
  resp_json = json_loads(resp_content)
713
772
 
714
773
  page_data = resp_json.get("results", [])
@@ -777,7 +836,7 @@ class WEBS:
777
836
 
778
837
  def _news_page(s: int) -> list[dict[str, str]]:
779
838
  payload["s"] = f"{s}"
780
- resp_content = self._get_url("GET", "https://duckduckgo.com/news.js", params=payload)
839
+ resp_content = self._get_url("GET", "https://duckduckgo.com/news.js", params=payload).content
781
840
  resp_json = json_loads(resp_content)
782
841
  page_data = resp_json.get("results", [])
783
842
  page_results = []
@@ -828,7 +887,7 @@ class WEBS:
828
887
  "q": f"what is {keywords}",
829
888
  "format": "json",
830
889
  }
831
- resp_content = self._get_url("GET", "https://api.duckduckgo.com/", params=payload)
890
+ resp_content = self._get_url("GET", "https://api.duckduckgo.com/", params=payload).content
832
891
  page_data = json_loads(resp_content)
833
892
 
834
893
  results = []
@@ -849,7 +908,7 @@ class WEBS:
849
908
  "q": f"{keywords}",
850
909
  "format": "json",
851
910
  }
852
- resp_content = self._get_url("GET", "https://api.duckduckgo.com/", params=payload)
911
+ resp_content = self._get_url("GET", "https://api.duckduckgo.com/", params=payload).content
853
912
  resp_json = json_loads(resp_content)
854
913
  page_data = resp_json.get("RelatedTopics", [])
855
914
 
@@ -900,7 +959,7 @@ class WEBS:
900
959
  "q": keywords,
901
960
  "kl": region,
902
961
  }
903
- resp_content = self._get_url("GET", "https://duckduckgo.com/ac/", params=payload)
962
+ resp_content = self._get_url("GET", "https://duckduckgo.com/ac/", params=payload).content
904
963
  page_data = json_loads(resp_content)
905
964
  return [r for r in page_data]
906
965
 
@@ -986,7 +1045,7 @@ class WEBS:
986
1045
  "GET",
987
1046
  "https://nominatim.openstreetmap.org/search.php",
988
1047
  params=params,
989
- )
1048
+ ).content
990
1049
  if resp_content == b"[]":
991
1050
  raise WebscoutE("maps() Coordinates are not found, check function parameters.")
992
1051
  resp_json = json_loads(resp_content)
@@ -1022,7 +1081,7 @@ class WEBS:
1022
1081
  "bbox_br": f"{lat_b},{lon_r}",
1023
1082
  "strict_bbox": "1",
1024
1083
  }
1025
- resp_content = self._get_url("GET", "https://duckduckgo.com/local.js", params=params)
1084
+ resp_content = self._get_url("GET", "https://duckduckgo.com/local.js", params=params).content
1026
1085
  resp_json = json_loads(resp_content)
1027
1086
  page_data = resp_json.get("results", [])
1028
1087
 
@@ -1127,7 +1186,7 @@ class WEBS:
1127
1186
  "https://duckduckgo.com/translation.js",
1128
1187
  params=payload,
1129
1188
  content=keyword.encode(),
1130
- )
1189
+ ).content
1131
1190
  page_data: dict[str, str] = json_loads(resp_content)
1132
1191
  page_data["original"] = keyword
1133
1192
  return page_data
@@ -1167,7 +1226,7 @@ class WEBS:
1167
1226
  lang = language.split('-')[0]
1168
1227
  url = f"https://duckduckgo.com/js/spice/forecast/{quote(location)}/{lang}"
1169
1228
 
1170
- resp = self._get_url("GET", url)
1229
+ resp = self._get_url("GET", url).content
1171
1230
  resp_text = resp.decode('utf-8')
1172
1231
 
1173
1232
  if "ddg_spice_forecast(" not in resp_text: