webscout 8.3.1__py3-none-any.whl → 8.3.3__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 (114) hide show
  1. webscout/AIutel.py +180 -78
  2. webscout/Bing_search.py +417 -0
  3. webscout/Extra/gguf.py +706 -177
  4. webscout/Provider/AISEARCH/__init__.py +1 -0
  5. webscout/Provider/AISEARCH/genspark_search.py +7 -7
  6. webscout/Provider/AISEARCH/stellar_search.py +132 -0
  7. webscout/Provider/ExaChat.py +84 -58
  8. webscout/Provider/GeminiProxy.py +140 -0
  9. webscout/Provider/HeckAI.py +85 -80
  10. webscout/Provider/Jadve.py +56 -50
  11. webscout/Provider/MCPCore.py +78 -75
  12. webscout/Provider/MiniMax.py +207 -0
  13. webscout/Provider/Nemotron.py +41 -13
  14. webscout/Provider/Netwrck.py +34 -51
  15. webscout/Provider/OPENAI/BLACKBOXAI.py +0 -4
  16. webscout/Provider/OPENAI/GeminiProxy.py +328 -0
  17. webscout/Provider/OPENAI/MiniMax.py +298 -0
  18. webscout/Provider/OPENAI/README.md +32 -29
  19. webscout/Provider/OPENAI/README_AUTOPROXY.md +238 -0
  20. webscout/Provider/OPENAI/TogetherAI.py +4 -17
  21. webscout/Provider/OPENAI/__init__.py +17 -1
  22. webscout/Provider/OPENAI/autoproxy.py +1067 -39
  23. webscout/Provider/OPENAI/base.py +17 -76
  24. webscout/Provider/OPENAI/deepinfra.py +42 -108
  25. webscout/Provider/OPENAI/e2b.py +0 -1
  26. webscout/Provider/OPENAI/flowith.py +179 -166
  27. webscout/Provider/OPENAI/friendli.py +233 -0
  28. webscout/Provider/OPENAI/mcpcore.py +109 -70
  29. webscout/Provider/OPENAI/monochat.py +329 -0
  30. webscout/Provider/OPENAI/pydantic_imports.py +1 -172
  31. webscout/Provider/OPENAI/scirachat.py +59 -51
  32. webscout/Provider/OPENAI/toolbaz.py +3 -9
  33. webscout/Provider/OPENAI/typegpt.py +1 -1
  34. webscout/Provider/OPENAI/utils.py +19 -42
  35. webscout/Provider/OPENAI/x0gpt.py +14 -2
  36. webscout/Provider/OPENAI/xenai.py +514 -0
  37. webscout/Provider/OPENAI/yep.py +8 -2
  38. webscout/Provider/OpenGPT.py +54 -32
  39. webscout/Provider/PI.py +58 -84
  40. webscout/Provider/StandardInput.py +32 -13
  41. webscout/Provider/TTI/README.md +9 -9
  42. webscout/Provider/TTI/__init__.py +3 -1
  43. webscout/Provider/TTI/aiarta.py +92 -78
  44. webscout/Provider/TTI/bing.py +231 -0
  45. webscout/Provider/TTI/infip.py +212 -0
  46. webscout/Provider/TTI/monochat.py +220 -0
  47. webscout/Provider/TTS/speechma.py +45 -39
  48. webscout/Provider/TeachAnything.py +11 -3
  49. webscout/Provider/TextPollinationsAI.py +78 -70
  50. webscout/Provider/TogetherAI.py +350 -0
  51. webscout/Provider/Venice.py +37 -46
  52. webscout/Provider/VercelAI.py +27 -24
  53. webscout/Provider/WiseCat.py +35 -35
  54. webscout/Provider/WrDoChat.py +22 -26
  55. webscout/Provider/WritingMate.py +26 -22
  56. webscout/Provider/XenAI.py +324 -0
  57. webscout/Provider/__init__.py +10 -5
  58. webscout/Provider/deepseek_assistant.py +378 -0
  59. webscout/Provider/granite.py +48 -57
  60. webscout/Provider/koala.py +51 -39
  61. webscout/Provider/learnfastai.py +49 -64
  62. webscout/Provider/llmchat.py +79 -93
  63. webscout/Provider/llmchatco.py +63 -78
  64. webscout/Provider/multichat.py +51 -40
  65. webscout/Provider/oivscode.py +1 -1
  66. webscout/Provider/scira_chat.py +159 -96
  67. webscout/Provider/scnet.py +13 -13
  68. webscout/Provider/searchchat.py +13 -13
  69. webscout/Provider/sonus.py +12 -11
  70. webscout/Provider/toolbaz.py +25 -8
  71. webscout/Provider/turboseek.py +41 -42
  72. webscout/Provider/typefully.py +27 -12
  73. webscout/Provider/typegpt.py +41 -46
  74. webscout/Provider/uncovr.py +55 -90
  75. webscout/Provider/x0gpt.py +33 -17
  76. webscout/Provider/yep.py +79 -96
  77. webscout/auth/__init__.py +55 -0
  78. webscout/auth/api_key_manager.py +189 -0
  79. webscout/auth/auth_system.py +100 -0
  80. webscout/auth/config.py +76 -0
  81. webscout/auth/database.py +400 -0
  82. webscout/auth/exceptions.py +67 -0
  83. webscout/auth/middleware.py +248 -0
  84. webscout/auth/models.py +130 -0
  85. webscout/auth/providers.py +279 -0
  86. webscout/auth/rate_limiter.py +254 -0
  87. webscout/auth/request_models.py +127 -0
  88. webscout/auth/request_processing.py +226 -0
  89. webscout/auth/routes.py +550 -0
  90. webscout/auth/schemas.py +103 -0
  91. webscout/auth/server.py +367 -0
  92. webscout/client.py +121 -70
  93. webscout/litagent/Readme.md +68 -55
  94. webscout/litagent/agent.py +99 -9
  95. webscout/scout/core/scout.py +104 -26
  96. webscout/scout/element.py +139 -18
  97. webscout/swiftcli/core/cli.py +14 -3
  98. webscout/swiftcli/decorators/output.py +59 -9
  99. webscout/update_checker.py +31 -49
  100. webscout/version.py +1 -1
  101. webscout/webscout_search.py +4 -12
  102. webscout/webscout_search_async.py +3 -10
  103. webscout/yep_search.py +2 -11
  104. {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/METADATA +141 -99
  105. {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/RECORD +109 -83
  106. {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/entry_points.txt +1 -1
  107. webscout/Provider/HF_space/__init__.py +0 -0
  108. webscout/Provider/HF_space/qwen_qwen2.py +0 -206
  109. webscout/Provider/OPENAI/api.py +0 -1320
  110. webscout/Provider/TTI/fastflux.py +0 -233
  111. webscout/Provider/Writecream.py +0 -246
  112. {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/WHEEL +0 -0
  113. {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/licenses/LICENSE.md +0 -0
  114. {webscout-8.3.1.dist-info → webscout-8.3.3.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,12 @@
1
1
  ##################################################################################
2
2
  ## Modified version of code written by t.me/infip1217 ##
3
3
  ##################################################################################
4
- import time
5
4
  import requests
6
5
  import pathlib
7
6
  import tempfile
8
- from io import BytesIO
9
7
  from webscout import exceptions
10
8
  from webscout.litagent import LitAgent
11
- from concurrent.futures import ThreadPoolExecutor, as_completed
12
- from webscout.Provider.TTS import utils
9
+ from webscout.Litlogger import Logger, LogLevel
13
10
  from webscout.Provider.TTS.base import BaseTTSProvider
14
11
 
15
12
  class SpeechMaTTS(BaseTTSProvider):
@@ -18,12 +15,11 @@ class SpeechMaTTS(BaseTTSProvider):
18
15
  """
19
16
  # Request headers
20
17
  headers = {
21
- "accept": "*/*",
22
- "accept-language": "en-IN,en-GB;q=0.9,en-US;q=0.8,en;q=0.7,en-AU;q=0.6",
23
- "content-type": "application/json",
18
+ "authority": "speechma.com",
24
19
  "origin": "https://speechma.com",
25
- "priority": "u=1, i",
26
- "User-Agent": LitAgent().random()
20
+ "referer": "https://speechma.com/",
21
+ "content-type": "application/json",
22
+ **LitAgent().generate_fingerprint()
27
23
  }
28
24
 
29
25
  # Available voices with their IDs
@@ -496,8 +492,9 @@ class SpeechMaTTS(BaseTTSProvider):
496
492
  if proxies:
497
493
  self.session.proxies.update(proxies)
498
494
  self.timeout = timeout
495
+ self.logger = Logger(name="SpeechMaTTS", level=LogLevel.INFO)
499
496
 
500
- def tts(self, text: str, voice: str = "Emma", pitch: int = 0, rate: int = 0) -> str:
497
+ def tts(self, text: str, voice: str = "Emma", pitch: int = 0, rate: int = 0, verbose: bool = False) -> str:
501
498
  """
502
499
  Converts text to speech using the SpeechMa API and saves it to a file.
503
500
 
@@ -506,6 +503,7 @@ class SpeechMaTTS(BaseTTSProvider):
506
503
  voice (str): The voice to use for TTS (default: "Emma")
507
504
  pitch (int): Voice pitch adjustment (-10 to 10, default: 0)
508
505
  rate (int): Voice rate/speed adjustment (-10 to 10, default: 0)
506
+ verbose (bool): Whether to print debug information (default: False)
509
507
 
510
508
  Returns:
511
509
  str: Path to the generated audio file
@@ -517,10 +515,13 @@ class SpeechMaTTS(BaseTTSProvider):
517
515
  voice in self.all_voices
518
516
  ), f"Voice '{voice}' not one of [{', '.join(self.all_voices.keys())}]"
519
517
 
518
+ if not text or text.strip() == '':
519
+ raise exceptions.FailedToGenerateResponseError("Text is empty")
520
+
520
521
  filename = pathlib.Path(tempfile.mktemp(suffix=".mp3", dir=self.temp_dir))
521
522
  voice_id = self.all_voices[voice]
522
523
 
523
- # Prepare payload for the job-based API
524
+ # Prepare payload for the API
524
525
  payload = {
525
526
  "text": text,
526
527
  "voice": voice_id,
@@ -530,44 +531,49 @@ class SpeechMaTTS(BaseTTSProvider):
530
531
  }
531
532
 
532
533
  try:
534
+ # Set logger level based on verbose flag
535
+ if verbose:
536
+ self.logger.level = LogLevel.DEBUG
537
+ self.logger.debug(f"Generating audio for voice: {voice} ({voice_id})")
538
+ self.logger.debug(f"Text length: {len(text)} characters")
539
+ else:
540
+ self.logger.level = LogLevel.INFO
541
+
542
+ # Make the request to the SpeechMa API
533
543
  response = self.session.post(
534
544
  self.api_url,
535
545
  headers=self.headers,
536
546
  json=payload,
537
547
  timeout=self.timeout
538
548
  )
539
- response.raise_for_status()
540
- resp_json = response.json()
541
- if not resp_json.get("success") or "data" not in resp_json or "job_id" not in resp_json["data"]:
542
- raise exceptions.FailedToGenerateResponseError(f"SpeechMa API error: {resp_json}")
543
- job_id = resp_json["data"]["job_id"]
544
549
 
545
- # Poll for job completion
546
- status_url = f"https://speechma.com/com.api/tts-api.php/status/{job_id}"
547
- for _ in range(30): # up to ~30 seconds
548
- status_resp = self.session.get(
549
- status_url,
550
- headers=self.headers,
551
- timeout=self.timeout
552
- )
553
- status_resp.raise_for_status()
554
- status_json = status_resp.json()
555
- if status_json.get("success") and status_json.get("data", {}).get("status") == "completed":
556
- break
557
- time.sleep(1)
558
- else:
559
- raise exceptions.FailedToGenerateResponseError("TTS job did not complete in time.")
550
+ if response.status_code != 200:
551
+ if verbose:
552
+ self.logger.error(f"API error: Status {response.status_code}")
553
+ raise exceptions.FailedToGenerateResponseError(f"API returned status {response.status_code}: {response.text[:500]}")
560
554
 
561
- # Download the audio file (API provides a URL in the status response)
562
- data = status_json["data"]
563
- audio_url = f"https://speechma.com/com.api/tts-api.php/audio/{job_id}"
564
- audio_resp = self.session.get(audio_url, timeout=self.timeout)
565
- audio_resp.raise_for_status()
566
- with open(filename, 'wb') as f:
567
- f.write(audio_resp.content)
568
- return filename.as_posix()
555
+ # Check if response is audio data (content-type should be audio/mpeg)
556
+ content_type = response.headers.get('content-type', '').lower()
557
+ if verbose:
558
+ self.logger.debug(f"Response content type: {content_type}")
559
+ self.logger.debug(f"Response size: {len(response.content)} bytes")
560
+
561
+ if 'audio' in content_type or response.content.startswith(b'\xff\xfb') or response.content.startswith(b'ID3') or b'LAME' in response.content[:100]:
562
+ # This is audio data, save it directly
563
+ with open(filename, 'wb') as f:
564
+ f.write(response.content)
565
+ if verbose:
566
+ self.logger.debug(f"Audio saved to: {filename}")
567
+ return filename.as_posix()
568
+ else:
569
+ # Unexpected response format
570
+ if verbose:
571
+ self.logger.error(f"Unexpected response format: {content_type}")
572
+ raise exceptions.FailedToGenerateResponseError(f"Unexpected response format. Content-Type: {content_type}, Content: {response.text[:200]}")
569
573
 
570
574
  except requests.exceptions.RequestException as e:
575
+ if verbose:
576
+ self.logger.error(f"Request failed: {e}")
571
577
  raise exceptions.FailedToGenerateResponseError(
572
578
  f"Failed to perform the operation: {e}"
573
579
  )
@@ -163,6 +163,7 @@ class TeachAnything(Provider):
163
163
  stream: bool = False, # Keep stream param for interface consistency
164
164
  optimizer: str = None,
165
165
  conversationally: bool = False,
166
+ raw: bool = False, # Added raw parameter
166
167
  ) -> Union[str, Any]:
167
168
  """Generate response `str` or yield for streaming compatibility
168
169
  Args:
@@ -170,22 +171,29 @@ class TeachAnything(Provider):
170
171
  stream (bool, optional): Flag for streaming response. Defaults to False.
171
172
  optimizer (str, optional): Prompt optimizer name - `[code, shell_command]`. Defaults to None.
172
173
  conversationally (bool, optional): Chat conversationally when using optimizer. Defaults to False.
174
+ raw (bool, optional): If True, return raw string output.
173
175
  Returns:
174
176
  str or generator: Response generated
175
177
  """
176
178
  response_data = self.ask(
177
179
  prompt,
178
180
  stream=False, # Call ask in non-stream mode internally
179
- raw=False, # Ensure ask returns dict
181
+ raw=raw, # Pass raw flag
180
182
  optimizer=optimizer,
181
183
  conversationally=conversationally
182
184
  )
183
185
  if stream:
184
186
  def stream_wrapper():
185
- yield self.get_message(response_data)
187
+ if raw:
188
+ yield response_data if isinstance(response_data, str) else self.get_message(response_data)
189
+ else:
190
+ yield self.get_message(response_data)
186
191
  return stream_wrapper()
187
192
  else:
188
- return self.get_message(response_data)
193
+ if raw:
194
+ return response_data if isinstance(response_data, str) else self.get_message(response_data)
195
+ else:
196
+ return self.get_message(response_data)
189
197
 
190
198
  def get_message(self, response: Union[dict, str]) -> str:
191
199
  """Retrieves message only from response
@@ -135,95 +135,98 @@ class TextPollinationsAI(Provider):
135
135
  payload["tool_choice"] = tool_choice
136
136
 
137
137
  def for_stream():
138
- try: # Add try block for CurlError
139
- # Use curl_cffi session post with impersonate
138
+ try:
140
139
  response = self.session.post(
141
140
  self.api_endpoint,
142
- # headers are set on the session
143
141
  json=payload,
144
142
  stream=True,
145
143
  timeout=self.timeout,
146
- impersonate="chrome120" # Add impersonate
144
+ impersonate="chrome120"
147
145
  )
148
-
149
146
  if not response.ok:
150
147
  raise exceptions.FailedToGenerateResponseError(
151
148
  f"Failed to generate response - ({response.status_code}, {response.reason}) - {response.text}"
152
149
  )
153
-
154
150
  streaming_text = ""
155
- # Use sanitize_stream
156
151
  processed_stream = sanitize_stream(
157
- data=response.iter_content(chunk_size=None), # Pass byte iterator
152
+ data=response.iter_content(chunk_size=None),
158
153
  intro_value="data:",
159
- to_json=True, # Stream sends JSON
154
+ to_json=True,
160
155
  skip_markers=["[DONE]"],
161
- # Extractor handles both content and tool_calls
162
156
  content_extractor=lambda chunk: chunk.get('choices', [{}])[0].get('delta') if isinstance(chunk, dict) else None,
163
- yield_raw_on_error=False # Skip non-JSON or lines where extractor fails
157
+ yield_raw_on_error=False,
158
+ raw=raw
164
159
  )
165
-
166
160
  for delta in processed_stream:
167
- # delta is the extracted 'delta' object or None
168
- if delta and isinstance(delta, dict):
169
- if 'content' in delta and delta['content'] is not None:
170
- content = delta['content']
171
- streaming_text += content
172
- yield content if raw else dict(text=content)
173
- elif 'tool_calls' in delta:
174
- tool_calls = delta['tool_calls']
175
- yield tool_calls if raw else dict(tool_calls=tool_calls)
176
-
177
- # Update history and last response after stream finishes
178
- self.last_response.update(dict(text=streaming_text)) # Store aggregated text
179
- if streaming_text: # Only update history if text was received
161
+ if isinstance(delta, bytes):
162
+ delta = delta.decode('utf-8', errors='ignore')
163
+ if delta is None:
164
+ continue
165
+ if raw:
166
+ # Only yield content or tool_calls as string
167
+ if isinstance(delta, dict):
168
+ if 'content' in delta and delta['content'] is not None:
169
+ content = delta['content']
170
+ streaming_text += content
171
+ yield content
172
+ elif 'tool_calls' in delta:
173
+ tool_calls = delta['tool_calls']
174
+ yield json.dumps(tool_calls)
175
+ elif isinstance(delta, str):
176
+ streaming_text += delta
177
+ yield delta
178
+ else:
179
+ if isinstance(delta, dict):
180
+ if 'content' in delta and delta['content'] is not None:
181
+ content = delta['content']
182
+ streaming_text += content
183
+ yield dict(text=content)
184
+ elif 'tool_calls' in delta:
185
+ tool_calls = delta['tool_calls']
186
+ yield dict(tool_calls=tool_calls)
187
+ self.last_response.update(dict(text=streaming_text))
188
+ if streaming_text:
180
189
  self.conversation.update_chat_history(
181
- prompt, streaming_text # Use the fully aggregated text
190
+ prompt, streaming_text
182
191
  )
183
- except CurlError as e: # Catch CurlError
192
+ except CurlError as e:
184
193
  raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}") from e
185
- except Exception as e: # Catch other potential exceptions
194
+ except Exception as e:
186
195
  raise exceptions.FailedToGenerateResponseError(f"An unexpected error occurred ({type(e).__name__}): {e}") from e
187
-
188
-
189
196
  def for_non_stream():
190
- # Aggregate the stream using the updated for_stream logic
191
197
  final_content = ""
192
- tool_calls_aggregated = None # To store potential tool calls
193
- try: # Add try block for potential errors during aggregation
198
+ tool_calls_aggregated = None
199
+ try:
194
200
  for chunk_data in for_stream():
195
- if isinstance(chunk_data, dict):
196
- if "text" in chunk_data:
197
- final_content += chunk_data["text"]
198
- elif "tool_calls" in chunk_data:
199
- # Aggregate tool calls (simple aggregation, might need refinement)
201
+ if raw:
202
+ if isinstance(chunk_data, str):
203
+ final_content += chunk_data
204
+ elif isinstance(chunk_data, bytes):
205
+ final_content += chunk_data.decode('utf-8', errors='ignore')
206
+ elif isinstance(chunk_data, list):
200
207
  if tool_calls_aggregated is None:
201
208
  tool_calls_aggregated = []
202
- tool_calls_aggregated.extend(chunk_data["tool_calls"])
203
- elif isinstance(chunk_data, str): # Handle raw stream case
204
- final_content += chunk_data
205
- # Handle raw tool calls list if raw=True
206
- elif isinstance(chunk_data, list) and raw:
207
- if tool_calls_aggregated is None:
208
- tool_calls_aggregated = []
209
- tool_calls_aggregated.extend(chunk_data)
209
+ tool_calls_aggregated.extend(chunk_data)
210
+ else:
211
+ if isinstance(chunk_data, dict):
212
+ if "text" in chunk_data:
213
+ final_content += chunk_data["text"]
214
+ elif "tool_calls" in chunk_data:
215
+ if tool_calls_aggregated is None:
216
+ tool_calls_aggregated = []
217
+ tool_calls_aggregated.extend(chunk_data["tool_calls"])
218
+ elif isinstance(chunk_data, str):
219
+ final_content += chunk_data
210
220
  except Exception as e:
211
- # If aggregation fails but some text was received, use it. Otherwise, re-raise.
212
- if not final_content and not tool_calls_aggregated:
213
- raise exceptions.FailedToGenerateResponseError(f"Failed to get non-stream response: {str(e)}") from e
214
-
215
-
216
- # last_response and history are updated within for_stream (for text)
217
- # Return a dict containing text and/or tool_calls
221
+ if not final_content and not tool_calls_aggregated:
222
+ raise exceptions.FailedToGenerateResponseError(f"Failed to get non-stream response: {str(e)}") from e
218
223
  result = {}
219
224
  if final_content:
220
225
  result["text"] = final_content
221
226
  if tool_calls_aggregated:
222
227
  result["tool_calls"] = tool_calls_aggregated
223
- self.last_response = result # Update last_response with aggregated result
224
- return self.last_response
225
-
226
-
228
+ self.last_response = result
229
+ return self.last_response if not raw else (final_content if final_content else json.dumps(tool_calls_aggregated) if tool_calls_aggregated else "")
227
230
  return for_stream() if stream else for_non_stream()
228
231
 
229
232
  def chat(
@@ -234,27 +237,32 @@ class TextPollinationsAI(Provider):
234
237
  conversationally: bool = False,
235
238
  tools: Optional[List[Dict[str, Any]]] = None,
236
239
  tool_choice: Optional[Dict[str, Any]] = None,
240
+ raw: bool = False, # Added raw parameter
237
241
  ) -> Union[str, Generator[str, None, None]]:
238
242
  """Generate response as a string"""
239
243
  def for_stream():
240
244
  for response in self.ask(
241
- prompt, True, optimizer=optimizer, conversationally=conversationally,
245
+ prompt, True, raw=raw, optimizer=optimizer, conversationally=conversationally,
242
246
  tools=tools, tool_choice=tool_choice
243
247
  ):
244
- yield self.get_message(response)
245
-
248
+ if raw:
249
+ yield response
250
+ else:
251
+ yield self.get_message(response)
246
252
  def for_non_stream():
247
- return self.get_message(
248
- self.ask(
249
- prompt,
250
- False,
251
- optimizer=optimizer,
252
- conversationally=conversationally,
253
- tools=tools,
254
- tool_choice=tool_choice,
255
- )
253
+ result = self.ask(
254
+ prompt,
255
+ False,
256
+ raw=raw,
257
+ optimizer=optimizer,
258
+ conversationally=conversationally,
259
+ tools=tools,
260
+ tool_choice=tool_choice,
256
261
  )
257
-
262
+ if raw:
263
+ return result if isinstance(result, str) else (result.get("text", "") if isinstance(result, dict) else str(result))
264
+ else:
265
+ return self.get_message(result)
258
266
  return for_stream() if stream else for_non_stream()
259
267
 
260
268
  def get_message(self, response: dict) -> str: