webscout 7.9__py3-none-any.whl → 8.0__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 (38) hide show
  1. webscout/Extra/GitToolkit/__init__.py +10 -0
  2. webscout/Extra/GitToolkit/gitapi/__init__.py +12 -0
  3. webscout/Extra/GitToolkit/gitapi/repository.py +195 -0
  4. webscout/Extra/GitToolkit/gitapi/user.py +96 -0
  5. webscout/Extra/GitToolkit/gitapi/utils.py +62 -0
  6. webscout/Extra/YTToolkit/ytapi/video.py +232 -103
  7. webscout/Provider/AISEARCH/__init__.py +5 -1
  8. webscout/Provider/AISEARCH/hika_search.py +194 -0
  9. webscout/Provider/AISEARCH/monica_search.py +246 -0
  10. webscout/Provider/AISEARCH/scira_search.py +320 -0
  11. webscout/Provider/AISEARCH/webpilotai_search.py +281 -0
  12. webscout/Provider/AllenAI.py +255 -122
  13. webscout/Provider/DeepSeek.py +1 -2
  14. webscout/Provider/Deepinfra.py +17 -9
  15. webscout/Provider/ExaAI.py +261 -0
  16. webscout/Provider/ExaChat.py +8 -1
  17. webscout/Provider/GithubChat.py +2 -1
  18. webscout/Provider/Netwrck.py +3 -2
  19. webscout/Provider/OpenGPT.py +199 -0
  20. webscout/Provider/PI.py +39 -24
  21. webscout/Provider/Youchat.py +326 -296
  22. webscout/Provider/__init__.py +10 -0
  23. webscout/Provider/ai4chat.py +58 -56
  24. webscout/Provider/akashgpt.py +34 -22
  25. webscout/Provider/freeaichat.py +1 -1
  26. webscout/Provider/labyrinth.py +121 -20
  27. webscout/Provider/llmchatco.py +306 -0
  28. webscout/Provider/scira_chat.py +271 -0
  29. webscout/Provider/typefully.py +280 -0
  30. webscout/version.py +1 -1
  31. webscout/webscout_search.py +118 -54
  32. webscout/webscout_search_async.py +109 -45
  33. {webscout-7.9.dist-info → webscout-8.0.dist-info}/METADATA +2 -2
  34. {webscout-7.9.dist-info → webscout-8.0.dist-info}/RECORD +38 -24
  35. {webscout-7.9.dist-info → webscout-8.0.dist-info}/LICENSE.md +0 -0
  36. {webscout-7.9.dist-info → webscout-8.0.dist-info}/WHEEL +0 -0
  37. {webscout-7.9.dist-info → webscout-8.0.dist-info}/entry_points.txt +0 -0
  38. {webscout-7.9.dist-info → webscout-8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,280 @@
1
+ from typing import Union, Any, Dict
2
+ import requests
3
+ import re
4
+ from uuid import uuid4
5
+
6
+ from webscout.AIutel import Optimizers
7
+ from webscout.AIutel import Conversation
8
+ from webscout.AIutel import AwesomePrompts
9
+ from webscout.AIbase import Provider
10
+ from webscout import exceptions
11
+ from webscout.litagent import LitAgent
12
+
13
+ class TypefullyAI(Provider):
14
+ """
15
+ A class to interact with the Typefully AI API.
16
+
17
+ Attributes:
18
+ system_prompt (str): The system prompt to define the assistant's role.
19
+ model (str): The model identifier to use for completions.
20
+ output_length (int): Maximum length of the generated output.
21
+
22
+ Examples:
23
+ >>> from webscout.Provider.typefully import TypefullyAI
24
+ >>> ai = TypefullyAI()
25
+ >>> response = ai.chat("What's the weather today?")
26
+ >>> print(response)
27
+ 'The weather today is sunny with a high of 75°F.'
28
+ """
29
+ AVAILABLE_MODELS = ["openai:gpt-4o-mini", "openai:gpt-4o", "anthropic:claude-3-5-haiku-20241022", "groq:llama-3.3-70b-versatile"]
30
+
31
+ def __init__(
32
+ self,
33
+ is_conversation: bool = True,
34
+ max_tokens: int = 600,
35
+ timeout: int = 30,
36
+ intro: str = None,
37
+ filepath: str = None,
38
+ update_file: bool = True,
39
+ proxies: dict = {},
40
+ history_offset: int = 10250,
41
+ act: str = None,
42
+ system_prompt: str = "You're a helpful assistant.",
43
+ model: str = "openai:gpt-4o-mini",
44
+ ):
45
+ """
46
+ Initializes the TypefullyAI API with given parameters.
47
+
48
+ Args:
49
+ is_conversation (bool): Whether the provider is in conversation mode.
50
+ max_tokens (int): Maximum number of tokens to sample.
51
+ timeout (int): Timeout for API requests.
52
+ intro (str): Introduction message for the conversation.
53
+ filepath (str): Filepath for storing conversation history.
54
+ update_file (bool): Whether to update the conversation history file.
55
+ proxies (dict): Proxies for the API requests.
56
+ history_offset (int): Offset for conversation history.
57
+ act (str): Act for the conversation.
58
+ system_prompt (str): The system prompt to define the assistant's role.
59
+ model (str): The model identifier to use.
60
+
61
+ Examples:
62
+ >>> ai = TypefullyAI(system_prompt="You are a friendly assistant.")
63
+ >>> print(ai.system_prompt)
64
+ 'You are a friendly assistant.'
65
+ """
66
+ self.session = requests.Session()
67
+ self.is_conversation = is_conversation
68
+ self.max_tokens_to_sample = max_tokens
69
+ self.api_endpoint = "https://typefully.com/tools/ai/api/completion"
70
+ self.timeout = timeout
71
+ self.last_response = {}
72
+ self.system_prompt = system_prompt
73
+ self.model = model
74
+ self.output_length = max_tokens
75
+
76
+ # Initialize LitAgent for user agent generation
77
+ self.agent = LitAgent()
78
+
79
+ self.headers = {
80
+ "authority": "typefully.com",
81
+ "accept": "*/*",
82
+ "accept-encoding": "gzip, deflate, br, zstd",
83
+ "accept-language": "en-US,en;q=0.9",
84
+ "content-type": "application/json",
85
+ "dnt": "1",
86
+ "origin": "https://typefully.com",
87
+ "referer": "https://typefully.com/tools/ai/chat-gpt-alternative",
88
+ "sec-ch-ua": '"Microsoft Edge";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
89
+ "sec-ch-ua-mobile": "?0",
90
+ "sec-ch-ua-platform": '"Windows"',
91
+ "user-agent": self.agent.random() # Use LitAgent to generate a random user agent
92
+ }
93
+
94
+ self.__available_optimizers = (
95
+ method
96
+ for method in dir(Optimizers)
97
+ if callable(getattr(Optimizers, method)) and not method.startswith("__")
98
+ )
99
+ self.session.headers.update(self.headers)
100
+ Conversation.intro = (
101
+ AwesomePrompts().get_act(
102
+ act, raise_not_found=True, default=None, case_insensitive=True
103
+ )
104
+ if act
105
+ else intro or Conversation.intro
106
+ )
107
+ self.conversation = Conversation(
108
+ is_conversation, self.max_tokens_to_sample, filepath, update_file
109
+ )
110
+ self.conversation.history_offset = history_offset
111
+ self.session.proxies = proxies
112
+
113
+ def ask(
114
+ self,
115
+ prompt: str,
116
+ stream: bool = False,
117
+ raw: bool = False,
118
+ optimizer: str = None,
119
+ conversationally: bool = False,
120
+ ) -> Dict[str, Any]:
121
+ """
122
+ Sends a prompt to the Typefully AI API and returns the response.
123
+
124
+ Args:
125
+ prompt (str): The prompt to send to the API.
126
+ stream (bool): Whether to stream the response.
127
+ raw (bool): Whether to return the raw response.
128
+ optimizer (str): Optimizer to use for the prompt.
129
+ conversationally (bool): Whether to generate the prompt conversationally.
130
+
131
+ Returns:
132
+ Dict[str, Any]: The API response.
133
+
134
+ Examples:
135
+ >>> ai = TypefullyAI()
136
+ >>> response = ai.ask("Tell me a joke!")
137
+ >>> print(response)
138
+ {'text': 'Why did the scarecrow win an award? Because he was outstanding in his field!'}
139
+ """
140
+ conversation_prompt = self.conversation.gen_complete_prompt(prompt)
141
+ if optimizer:
142
+ if optimizer in self.__available_optimizers:
143
+ conversation_prompt = getattr(Optimizers, optimizer)(
144
+ conversation_prompt if conversationally else prompt
145
+ )
146
+ else:
147
+ raise Exception(
148
+ f"Optimizer is not one of {self.__available_optimizers}"
149
+ )
150
+
151
+ payload = {
152
+ "prompt": conversation_prompt,
153
+ "systemPrompt": self.system_prompt,
154
+ "modelIdentifier": self.model,
155
+ "outputLength": self.output_length
156
+ }
157
+
158
+ def for_stream():
159
+ response = self.session.post(self.api_endpoint, headers=self.headers, json=payload, stream=True, timeout=self.timeout)
160
+ if not response.ok:
161
+ raise exceptions.FailedToGenerateResponseError(
162
+ f"Failed to generate response - ({response.status_code}, {response.reason}) - {response.text}"
163
+ )
164
+ streaming_response = ""
165
+ for line in response.iter_lines(decode_unicode=True):
166
+ if line:
167
+ match = re.search(r'0:"(.*?)"', line)
168
+ if match:
169
+ content = match.group(1)
170
+ streaming_response += content
171
+ yield content if raw else dict(text=content)
172
+ elif line.startswith('e:') or line.startswith('d:'):
173
+ # End of response
174
+ break
175
+ self.last_response.update(dict(text=streaming_response))
176
+ self.conversation.update_chat_history(
177
+ prompt, self.get_message(self.last_response)
178
+ )
179
+
180
+ def for_non_stream():
181
+ for _ in for_stream():
182
+ pass
183
+ return self.last_response
184
+
185
+ return for_stream() if stream else for_non_stream()
186
+
187
+ def chat(
188
+ self,
189
+ prompt: str,
190
+ stream: bool = False,
191
+ optimizer: str = None,
192
+ conversationally: bool = False,
193
+ ) -> str:
194
+ """
195
+ Generates a response from the Typefully AI API.
196
+
197
+ Args:
198
+ prompt (str): The prompt to send to the API.
199
+ stream (bool): Whether to stream the response.
200
+ optimizer (str): Optimizer to use for the prompt.
201
+ conversationally (bool): Whether to generate the prompt conversationally.
202
+
203
+ Returns:
204
+ str: The API response.
205
+
206
+ Examples:
207
+ >>> ai = TypefullyAI()
208
+ >>> response = ai.chat("What's the weather today?")
209
+ >>> print(response)
210
+ 'The weather today is sunny with a high of 75°F.'
211
+ """
212
+
213
+ def for_stream():
214
+ for response in self.ask(
215
+ prompt, True, optimizer=optimizer, conversationally=conversationally
216
+ ):
217
+ yield self.get_message(response)
218
+
219
+ def for_non_stream():
220
+ return self.get_message(
221
+ self.ask(
222
+ prompt,
223
+ False,
224
+ optimizer=optimizer,
225
+ conversationally=conversationally,
226
+ )
227
+ )
228
+
229
+ return for_stream() if stream else for_non_stream()
230
+
231
+ def get_message(self, response: dict) -> str:
232
+ """
233
+ Extracts the message from the API response.
234
+
235
+ Args:
236
+ response (dict): The API response.
237
+
238
+ Returns:
239
+ str: The message content.
240
+
241
+ Examples:
242
+ >>> ai = TypefullyAI()
243
+ >>> response = ai.ask("Tell me a joke!")
244
+ >>> message = ai.get_message(response)
245
+ >>> print(message)
246
+ 'Why did the scarecrow win an award? Because he was outstanding in his field!'
247
+ """
248
+ assert isinstance(response, dict), "Response should be of dict data-type only"
249
+ formatted_text = response["text"].replace('\\n', '\n').replace('\\n\\n', '\n\n')
250
+ return formatted_text
251
+
252
+ if __name__ == "__main__":
253
+ print("-" * 80)
254
+ print(f"{'Model':<50} {'Status':<10} {'Response'}")
255
+ print("-" * 80)
256
+
257
+ # Test all available models
258
+ working = 0
259
+ total = len(TypefullyAI.AVAILABLE_MODELS)
260
+
261
+ for model in TypefullyAI.AVAILABLE_MODELS:
262
+ try:
263
+ test_ai = TypefullyAI(model=model, timeout=60)
264
+ response = test_ai.chat("Say 'Hello' in one word", stream=True)
265
+ response_text = ""
266
+ for chunk in response:
267
+ response_text += chunk
268
+ print(f"\r{model:<50} {'Testing...':<10}", end="", flush=True)
269
+
270
+ if response_text and len(response_text.strip()) > 0:
271
+ status = "✓"
272
+ # Truncate response if too long
273
+ display_text = response_text.strip()[:50] + "..." if len(response_text.strip()) > 50 else response_text.strip()
274
+ else:
275
+ status = "✗"
276
+ display_text = "Empty or invalid response"
277
+ print(f"\r{model:<50} {status:<10} {display_text}")
278
+ except Exception as e:
279
+ print(f"\r{model:<50} {'✗':<10} {str(e)}")
280
+
webscout/version.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "7.9"
1
+ __version__ = "8.0"
2
2
  __prog__ = "webscout"
@@ -91,7 +91,7 @@ class WEBS:
91
91
  if not proxy and proxies:
92
92
  warnings.warn("'proxies' is deprecated, use 'proxy' instead.", stacklevel=1)
93
93
  self.proxy = proxies.get("http") or proxies.get("https") if isinstance(proxies, dict) else proxies
94
-
94
+
95
95
  default_headers = {
96
96
  "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
97
97
  "Accept-Language": "en-US,en;q=0.5",
@@ -105,10 +105,10 @@ class WEBS:
105
105
  "Sec-Fetch-User": "?1",
106
106
  "Referer": "https://duckduckgo.com/",
107
107
  }
108
-
108
+
109
109
  self.headers = headers if headers else {}
110
110
  self.headers.update(default_headers)
111
-
111
+
112
112
  self.client = primp.Client(
113
113
  headers=self.headers,
114
114
  proxy=self.proxy,
@@ -192,18 +192,36 @@ class WEBS:
192
192
  resp_content = self._get_url("GET", "https://duckduckgo.com", params={"q": keywords}).content
193
193
  return _extract_vqd(resp_content, keywords)
194
194
 
195
- def chat_yield(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30) -> Iterator[str]:
195
+ def chat_yield(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30, max_retries: int = 3) -> Iterator[str]:
196
196
  """Initiates a chat session with webscout AI.
197
197
 
198
198
  Args:
199
199
  keywords (str): The initial message or question to send to the AI.
200
200
  model (str): The model to use: "gpt-4o-mini", "llama-3.3-70b", "claude-3-haiku",
201
201
  "o3-mini", "mistral-small-3". Defaults to "gpt-4o-mini".
202
- timeout (int): Timeout value for the HTTP client. Defaults to 20.
202
+ timeout (int): Timeout value for the HTTP client. Defaults to 30.
203
+ max_retries (int): Maximum number of retry attempts for rate limited requests. Defaults to 3.
203
204
 
204
205
  Yields:
205
206
  str: Chunks of the response from the AI.
206
207
  """
208
+ # Get Cloudflare Turnstile token
209
+ def get_turnstile_token():
210
+ try:
211
+ # Visit the DuckDuckGo chat page to get the Turnstile token
212
+ resp_content = self._get_url(
213
+ method="GET",
214
+ url="https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=1",
215
+ ).content
216
+
217
+ # Extract the Turnstile token if available
218
+ if b'cf-turnstile-response' in resp_content:
219
+ token = resp_content.split(b'cf-turnstile-response="', maxsplit=1)[1].split(b'"', maxsplit=1)[0].decode()
220
+ return token
221
+ return ""
222
+ except Exception:
223
+ return ""
224
+
207
225
  # x-fe-version
208
226
  if not self._chat_xfe:
209
227
  resp_content = self._get_url(
@@ -231,55 +249,100 @@ class WEBS:
231
249
  if model not in self._chat_models:
232
250
  warnings.warn(f"{model=} is unavailable. Using 'gpt-4o-mini'", stacklevel=1)
233
251
  model = "gpt-4o-mini"
252
+
253
+ # Get Cloudflare Turnstile token
254
+ turnstile_token = get_turnstile_token()
255
+
234
256
  json_data = {
235
257
  "model": self._chat_models[model],
236
258
  "messages": self._chat_messages,
237
259
  }
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
- },
246
- json=json_data,
247
- timeout=timeout,
248
- )
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
277
260
 
278
- result = "".join(chunks)
279
- self._chat_messages.append({"role": "assistant", "content": result})
280
- self._chat_tokens_count += len(result)
261
+ # Add Turnstile token if available
262
+ if turnstile_token:
263
+ json_data["cf-turnstile-response"] = turnstile_token
264
+
265
+ # Enhanced headers to better mimic a real browser
266
+ chat_headers = {
267
+ "x-fe-version": self._chat_xfe,
268
+ "x-vqd-4": self._chat_vqd,
269
+ "x-vqd-hash-1": "",
270
+ "Accept": "text/event-stream",
271
+ "Accept-Language": "en-US,en;q=0.9",
272
+ "Cache-Control": "no-cache",
273
+ "Content-Type": "application/json",
274
+ "DNT": "1",
275
+ "Origin": "https://duckduckgo.com",
276
+ "Referer": "https://duckduckgo.com/",
277
+ "Sec-Fetch-Dest": "empty",
278
+ "Sec-Fetch-Mode": "cors",
279
+ "Sec-Fetch-Site": "same-origin",
280
+ "User-Agent": self.client.headers.get("User-Agent", "")
281
+ }
281
282
 
282
- def chat(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30) -> str:
283
+ # Retry logic for rate limited requests
284
+ retry_count = 0
285
+ while retry_count <= max_retries:
286
+ try:
287
+ resp = self._get_url(
288
+ method="POST",
289
+ url="https://duckduckgo.com/duckchat/v1/chat",
290
+ headers=chat_headers,
291
+ json=json_data,
292
+ timeout=timeout,
293
+ )
294
+
295
+ self._chat_vqd = resp.headers.get("x-vqd-4", "")
296
+ self._chat_vqd_hash = resp.headers.get("x-vqd-hash-1", "")
297
+ chunks = []
298
+
299
+ for chunk in resp.stream():
300
+ lines = chunk.split(b"data:")
301
+ for line in lines:
302
+ if line := line.strip():
303
+ if line == b"[DONE]":
304
+ break
305
+ if line == b"[DONE][LIMIT_CONVERSATION]":
306
+ raise ConversationLimitException("ERR_CONVERSATION_LIMIT")
307
+ x = json_loads(line)
308
+ if isinstance(x, dict):
309
+ if x.get("action") == "error":
310
+ err_message = x.get("type", "")
311
+ if x.get("status") == 429:
312
+ raise (
313
+ ConversationLimitException(err_message)
314
+ if err_message == "ERR_CONVERSATION_LIMIT"
315
+ else RatelimitE(err_message)
316
+ )
317
+ raise WebscoutE(err_message)
318
+ elif message := x.get("message"):
319
+ chunks.append(message)
320
+ yield message
321
+
322
+ # If we get here, the request was successful
323
+ result = "".join(chunks)
324
+ self._chat_messages.append({"role": "assistant", "content": result})
325
+ self._chat_tokens_count += len(result)
326
+ return
327
+
328
+ except RatelimitE as ex:
329
+ retry_count += 1
330
+ if retry_count > max_retries:
331
+ raise WebscoutE(f"chat_yield() Rate limit exceeded after {max_retries} retries: {ex}") from ex
332
+
333
+ # Get a fresh Turnstile token for the retry
334
+ turnstile_token = get_turnstile_token()
335
+ if turnstile_token:
336
+ json_data["cf-turnstile-response"] = turnstile_token
337
+
338
+ # Exponential backoff
339
+ sleep_time = 2 ** retry_count
340
+ sleep(sleep_time)
341
+
342
+ except Exception as ex:
343
+ raise WebscoutE(f"chat_yield() {type(ex).__name__}: {ex}") from ex
344
+
345
+ def chat(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30, max_retries: int = 3) -> str:
283
346
  """Initiates a chat session with webscout AI.
284
347
 
285
348
  Args:
@@ -287,11 +350,12 @@ class WEBS:
287
350
  model (str): The model to use: "gpt-4o-mini", "llama-3.3-70b", "claude-3-haiku",
288
351
  "o3-mini", "mistral-small-3". Defaults to "gpt-4o-mini".
289
352
  timeout (int): Timeout value for the HTTP client. Defaults to 30.
353
+ max_retries (int): Maximum number of retry attempts for rate limited requests. Defaults to 3.
290
354
 
291
355
  Returns:
292
356
  str: The response from the AI.
293
357
  """
294
- answer_generator = self.chat_yield(keywords, model, timeout)
358
+ answer_generator = self.chat_yield(keywords, model, timeout, max_retries)
295
359
  return "".join(answer_generator)
296
360
 
297
361
  def text(
@@ -1225,22 +1289,22 @@ class WEBS:
1225
1289
  assert location, "location is mandatory"
1226
1290
  lang = language.split('-')[0]
1227
1291
  url = f"https://duckduckgo.com/js/spice/forecast/{quote(location)}/{lang}"
1228
-
1292
+
1229
1293
  resp = self._get_url("GET", url).content
1230
1294
  resp_text = resp.decode('utf-8')
1231
-
1295
+
1232
1296
  if "ddg_spice_forecast(" not in resp_text:
1233
1297
  raise WebscoutE(f"No weather data found for {location}")
1234
-
1298
+
1235
1299
  json_text = resp_text[resp_text.find('(') + 1:resp_text.rfind(')')]
1236
1300
  try:
1237
1301
  result = json.loads(json_text)
1238
1302
  except Exception as e:
1239
1303
  raise WebscoutE(f"Error parsing weather JSON: {e}")
1240
-
1304
+
1241
1305
  if not result or 'currentWeather' not in result or 'forecastDaily' not in result:
1242
1306
  raise WebscoutE(f"Invalid weather data format for {location}")
1243
-
1307
+
1244
1308
  formatted_data = {
1245
1309
  "location": result["currentWeather"]["metadata"].get("ddg-location", "Unknown"),
1246
1310
  "current": {