webscout 8.2.4__py3-none-any.whl → 8.2.5__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 (80) hide show
  1. webscout/Extra/gguf.py +2 -0
  2. webscout/Provider/AISEARCH/scira_search.py +2 -5
  3. webscout/Provider/Aitopia.py +75 -51
  4. webscout/Provider/AllenAI.py +64 -67
  5. webscout/Provider/ChatGPTClone.py +33 -34
  6. webscout/Provider/ChatSandbox.py +342 -0
  7. webscout/Provider/Cloudflare.py +79 -32
  8. webscout/Provider/Deepinfra.py +69 -56
  9. webscout/Provider/ElectronHub.py +48 -39
  10. webscout/Provider/ExaChat.py +36 -20
  11. webscout/Provider/GPTWeb.py +24 -18
  12. webscout/Provider/GithubChat.py +52 -49
  13. webscout/Provider/GizAI.py +283 -0
  14. webscout/Provider/Glider.py +39 -28
  15. webscout/Provider/Groq.py +48 -20
  16. webscout/Provider/HeckAI.py +18 -36
  17. webscout/Provider/Jadve.py +30 -37
  18. webscout/Provider/LambdaChat.py +36 -59
  19. webscout/Provider/MCPCore.py +18 -21
  20. webscout/Provider/Marcus.py +23 -14
  21. webscout/Provider/Netwrck.py +35 -26
  22. webscout/Provider/OPENAI/__init__.py +1 -1
  23. webscout/Provider/OPENAI/exachat.py +4 -0
  24. webscout/Provider/OPENAI/scirachat.py +2 -4
  25. webscout/Provider/OPENAI/textpollinations.py +20 -22
  26. webscout/Provider/OPENAI/toolbaz.py +1 -0
  27. webscout/Provider/PI.py +22 -13
  28. webscout/Provider/StandardInput.py +42 -30
  29. webscout/Provider/TeachAnything.py +16 -7
  30. webscout/Provider/TextPollinationsAI.py +78 -76
  31. webscout/Provider/TwoAI.py +120 -88
  32. webscout/Provider/TypliAI.py +305 -0
  33. webscout/Provider/Venice.py +24 -22
  34. webscout/Provider/VercelAI.py +31 -12
  35. webscout/Provider/__init__.py +7 -7
  36. webscout/Provider/asksteve.py +53 -44
  37. webscout/Provider/cerebras.py +77 -31
  38. webscout/Provider/chatglm.py +47 -37
  39. webscout/Provider/elmo.py +38 -32
  40. webscout/Provider/granite.py +24 -21
  41. webscout/Provider/hermes.py +27 -20
  42. webscout/Provider/learnfastai.py +25 -20
  43. webscout/Provider/llmchatco.py +48 -78
  44. webscout/Provider/multichat.py +13 -3
  45. webscout/Provider/scira_chat.py +49 -30
  46. webscout/Provider/scnet.py +23 -20
  47. webscout/Provider/searchchat.py +16 -24
  48. webscout/Provider/sonus.py +37 -39
  49. webscout/Provider/toolbaz.py +24 -46
  50. webscout/Provider/turboseek.py +37 -41
  51. webscout/Provider/typefully.py +30 -22
  52. webscout/Provider/typegpt.py +47 -51
  53. webscout/Provider/uncovr.py +46 -40
  54. webscout/cli.py +256 -0
  55. webscout/conversation.py +0 -2
  56. webscout/exceptions.py +3 -0
  57. webscout/version.py +1 -1
  58. {webscout-8.2.4.dist-info → webscout-8.2.5.dist-info}/METADATA +166 -45
  59. {webscout-8.2.4.dist-info → webscout-8.2.5.dist-info}/RECORD +63 -76
  60. {webscout-8.2.4.dist-info → webscout-8.2.5.dist-info}/WHEEL +1 -1
  61. webscout-8.2.5.dist-info/entry_points.txt +3 -0
  62. {webscout-8.2.4.dist-info → webscout-8.2.5.dist-info}/top_level.txt +0 -1
  63. inferno/__init__.py +0 -6
  64. inferno/__main__.py +0 -9
  65. inferno/cli.py +0 -6
  66. inferno/lol.py +0 -589
  67. webscout/Local/__init__.py +0 -12
  68. webscout/Local/__main__.py +0 -9
  69. webscout/Local/api.py +0 -576
  70. webscout/Local/cli.py +0 -516
  71. webscout/Local/config.py +0 -75
  72. webscout/Local/llm.py +0 -287
  73. webscout/Local/model_manager.py +0 -253
  74. webscout/Local/server.py +0 -721
  75. webscout/Local/utils.py +0 -93
  76. webscout/Provider/Chatify.py +0 -175
  77. webscout/Provider/askmyai.py +0 -158
  78. webscout/Provider/gaurish.py +0 -244
  79. webscout-8.2.4.dist-info/entry_points.txt +0 -5
  80. {webscout-8.2.4.dist-info → webscout-8.2.5.dist-info}/licenses/LICENSE.md +0 -0
@@ -2,11 +2,11 @@ from curl_cffi.requests import Session
2
2
  from curl_cffi import CurlError
3
3
  import json
4
4
  import os
5
- from typing import Any, Dict, Optional, Generator, Union
5
+ from typing import Any, Dict, Optional, Generator, Union, List
6
6
 
7
7
  from webscout.AIutel import Optimizers
8
8
  from webscout.AIutel import Conversation
9
- from webscout.AIutel import AwesomePrompts, sanitize_stream
9
+ from webscout.AIutel import AwesomePrompts, sanitize_stream # Import sanitize_stream
10
10
  from webscout.AIbase import Provider, AsyncProvider
11
11
  from webscout import exceptions
12
12
  from webscout.litagent import LitAgent
@@ -73,6 +73,13 @@ class DeepInfra(Provider):
73
73
  # "Sao10K/L3.3-70B-Euryale-v2.3", # >>>> NOT WORKING
74
74
  ]
75
75
 
76
+ @staticmethod
77
+ def _deepinfra_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
78
+ """Extracts content from DeepInfra stream JSON objects."""
79
+ if isinstance(chunk, dict):
80
+ return chunk.get("choices", [{}])[0].get("delta", {}).get("content")
81
+ return None
82
+
76
83
  def __init__(
77
84
  self,
78
85
  is_conversation: bool = True,
@@ -91,14 +98,14 @@ class DeepInfra(Provider):
91
98
  """Initializes the DeepInfra API client."""
92
99
  if model not in self.AVAILABLE_MODELS:
93
100
  raise ValueError(f"Invalid model: {model}. Choose from: {self.AVAILABLE_MODELS}")
94
-
101
+
95
102
  self.url = "https://api.deepinfra.com/v1/openai/chat/completions"
96
-
103
+
97
104
  # Initialize LitAgent (keep if needed for other headers or logic)
98
105
  self.agent = LitAgent()
99
106
  # Fingerprint generation might be less relevant with impersonate
100
107
  self.fingerprint = self.agent.generate_fingerprint(browser)
101
-
108
+
102
109
  # Use the fingerprint for headers (keep relevant ones)
103
110
  self.headers = {
104
111
  "Accept": self.fingerprint["accept"], # Keep Accept
@@ -113,7 +120,7 @@ class DeepInfra(Provider):
113
120
  "Sec-Fetch-Site": "same-site",
114
121
  "X-Deepinfra-Source": "web-embed", # Keep custom headers
115
122
  }
116
-
123
+
117
124
  # Initialize curl_cffi Session
118
125
  self.session = Session()
119
126
  # Update curl_cffi session headers and proxies
@@ -147,22 +154,22 @@ class DeepInfra(Provider):
147
154
  def refresh_identity(self, browser: str = None):
148
155
  """
149
156
  Refreshes the browser identity fingerprint.
150
-
157
+
151
158
  Args:
152
159
  browser: Specific browser to use for the new fingerprint
153
160
  """
154
161
  browser = browser or self.fingerprint.get("browser_type", "chrome")
155
162
  self.fingerprint = self.agent.generate_fingerprint(browser)
156
-
163
+
157
164
  # Update headers with new fingerprint (only relevant ones)
158
165
  self.headers.update({
159
166
  "Accept": self.fingerprint["accept"],
160
167
  "Accept-Language": self.fingerprint["accept_language"],
161
168
  })
162
-
169
+
163
170
  # Update session headers
164
171
  self.session.headers.update(self.headers) # Update only relevant headers
165
-
172
+
166
173
  return self.fingerprint
167
174
 
168
175
  def ask(
@@ -197,68 +204,74 @@ class DeepInfra(Provider):
197
204
  try:
198
205
  # Use curl_cffi session post with impersonate
199
206
  response = self.session.post(
200
- self.url,
207
+ self.url,
201
208
  # headers are set on the session
202
- data=json.dumps(payload),
203
- stream=True,
209
+ data=json.dumps(payload),
210
+ stream=True,
204
211
  timeout=self.timeout,
205
212
  impersonate="chrome110" # Use a common impersonation profile
206
213
  )
207
214
  response.raise_for_status() # Check for HTTP errors
208
-
209
- # Iterate over bytes and decode manually
210
- for line_bytes in response.iter_lines():
211
- if line_bytes:
212
- try:
213
- line = line_bytes.decode('utf-8').strip()
214
- if line.startswith("data: "):
215
- json_str = line[6:]
216
- if json_str == "[DONE]":
217
- break
218
- json_data = json.loads(json_str)
219
- if 'choices' in json_data:
220
- choice = json_data['choices'][0]
221
- if 'delta' in choice and 'content' in choice['delta']:
222
- content = choice['delta']['content']
223
- if content: # Ensure content is not None or empty
224
- streaming_text += content
225
- resp = dict(text=content)
226
- # Yield dict or raw string chunk
227
- yield resp if not raw else content
228
- except (json.JSONDecodeError, UnicodeDecodeError):
229
- continue # Ignore lines that are not valid JSON or cannot be decoded
230
-
231
- # Update history after stream finishes
232
- self.last_response = {"text": streaming_text}
233
- self.conversation.update_chat_history(prompt, streaming_text)
234
-
235
- except CurlError as e: # Catch CurlError
215
+
216
+ # Use sanitize_stream
217
+ processed_stream = sanitize_stream(
218
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
219
+ intro_value="data:",
220
+ to_json=True, # Stream sends JSON
221
+ skip_markers=["[DONE]"],
222
+ content_extractor=self._deepinfra_extractor, # Use the specific extractor
223
+ yield_raw_on_error=False # Skip non-JSON lines or lines where extractor fails
224
+ )
225
+
226
+ for content_chunk in processed_stream:
227
+ # content_chunk is the string extracted by _deepinfra_extractor
228
+ if content_chunk and isinstance(content_chunk, str):
229
+ streaming_text += content_chunk
230
+ resp = dict(text=content_chunk)
231
+ yield resp if not raw else content_chunk
232
+
233
+ except CurlError as e:
236
234
  raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {str(e)}") from e
237
- except Exception as e: # Catch other potential exceptions (like HTTPError)
238
- err_text = getattr(e, 'response', None) and getattr(e.response, 'text', '')
239
- raise exceptions.FailedToGenerateResponseError(f"Request failed ({type(e).__name__}): {str(e)} - {err_text}") from e
235
+ except Exception as e:
236
+ raise exceptions.FailedToGenerateResponseError(f"Request failed ({type(e).__name__}): {str(e)}") from e
237
+ finally:
238
+ # Update history after stream finishes or fails
239
+ if streaming_text:
240
+ self.last_response = {"text": streaming_text}
241
+ self.conversation.update_chat_history(prompt, streaming_text)
240
242
 
241
243
 
242
244
  def for_non_stream():
243
245
  try:
244
246
  # Use curl_cffi session post with impersonate for non-streaming
245
247
  response = self.session.post(
246
- self.url,
248
+ self.url,
247
249
  # headers are set on the session
248
- data=json.dumps(payload),
250
+ data=json.dumps(payload),
249
251
  timeout=self.timeout,
250
252
  impersonate="chrome110" # Use a common impersonation profile
251
253
  )
252
254
  response.raise_for_status() # Check for HTTP errors
253
255
 
254
- response_data = response.json()
255
- if 'choices' in response_data and len(response_data['choices']) > 0:
256
- content = response_data['choices'][0].get('message', {}).get('content', '')
257
- self.last_response = {"text": content}
258
- self.conversation.update_chat_history(prompt, content)
259
- return self.last_response if not raw else content # Return dict or raw string
260
- else:
261
- raise exceptions.FailedToGenerateResponseError("No response content found")
256
+ response_text = response.text # Get raw text
257
+
258
+ # Use sanitize_stream to parse the non-streaming JSON response
259
+ processed_stream = sanitize_stream(
260
+ data=response_text,
261
+ to_json=True, # Parse the whole text as JSON
262
+ intro_value=None,
263
+ # Extractor for non-stream structure
264
+ content_extractor=lambda chunk: chunk.get("choices", [{}])[0].get("message", {}).get("content") if isinstance(chunk, dict) else None,
265
+ yield_raw_on_error=False
266
+ )
267
+ # Extract the single result
268
+ content = next(processed_stream, None)
269
+ content = content if isinstance(content, str) else "" # Ensure it's a string
270
+
271
+ self.last_response = {"text": content}
272
+ self.conversation.update_chat_history(prompt, content)
273
+ return self.last_response if not raw else content # Return dict or raw string
274
+
262
275
  except CurlError as e: # Catch CurlError
263
276
  raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}") from e
264
277
  except Exception as e: # Catch other potential exceptions (like HTTPError, JSONDecodeError)
@@ -311,7 +324,7 @@ if __name__ == "__main__":
311
324
  response_text = ""
312
325
  for chunk in response:
313
326
  response_text += chunk
314
-
327
+
315
328
  if response_text and len(response_text.strip()) > 0:
316
329
  status = "✓"
317
330
  # Clean and truncate response
@@ -322,4 +335,4 @@ if __name__ == "__main__":
322
335
  display_text = "Empty or invalid response"
323
336
  print(f"\r{model:<50} {status:<10} {display_text}")
324
337
  except Exception as e:
325
- print(f"\r{model:<50} {'✗':<10} {str(e)}")
338
+ print(f"\r{model:<50} {'✗':<10} {str(e)}")
@@ -1,10 +1,13 @@
1
- import requests
1
+ from curl_cffi import CurlError
2
+ from curl_cffi.requests import Session
2
3
  import json
3
4
  import os
4
- from typing import Any, Dict, Optional, Generator, Union
5
+ from typing import Any, Dict, Optional, Generator, Union, List
6
+
7
+ import requests
5
8
 
6
9
  from webscout.AIutel import Optimizers
7
- from webscout.AIutel import Conversation
10
+ from webscout.AIutel import Conversation, sanitize_stream # Import sanitize_stream
8
11
  from webscout.AIutel import AwesomePrompts, sanitize_stream
9
12
  from webscout.AIbase import Provider, AsyncProvider
10
13
  from webscout import exceptions
@@ -550,6 +553,13 @@ class ElectronHub(Provider):
550
553
  # Fallback to default models list if fetching fails
551
554
  pass
552
555
 
556
+ @staticmethod
557
+ def _electronhub_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
558
+ """Extracts content from ElectronHub stream JSON objects."""
559
+ if isinstance(chunk, dict):
560
+ return chunk.get("choices", [{}])[0].get("delta", {}).get("content")
561
+ return None
562
+
553
563
  def __init__(
554
564
  self,
555
565
  is_conversation: bool = True,
@@ -593,9 +603,9 @@ class ElectronHub(Provider):
593
603
  if api_key:
594
604
  self.headers['Authorization'] = f'Bearer {api_key}'
595
605
  self.system_prompt = system_prompt
596
- self.session = requests.Session()
606
+ self.session = Session() # Use curl_cffi Session
597
607
  self.session.headers.update(self.headers)
598
- self.session.proxies.update(proxies)
608
+ self.session.proxies = proxies # Assign proxies directly
599
609
 
600
610
  self.is_conversation = is_conversation
601
611
  self.max_tokens = max_tokens
@@ -663,41 +673,38 @@ class ElectronHub(Provider):
663
673
 
664
674
  def for_stream():
665
675
  try:
666
- with requests.post(self.url, headers=self.headers, data=json.dumps(payload), stream=True, timeout=self.timeout) as response:
667
- if response.status_code != 200:
668
- raise exceptions.FailedToGenerateResponseError(
669
- f"Request failed with status code {response.status_code}"
670
- )
671
-
672
- streaming_text = ""
673
- for line in response.iter_lines(decode_unicode=True):
674
- if line:
675
- line = line.strip()
676
- if line.startswith("data: "):
677
- json_str = line[6:]
678
- if json_str == "[DONE]":
679
- break
680
- try:
681
- json_data = json.loads(json_str)
682
- if 'choices' in json_data:
683
- choice = json_data['choices'][0]
684
- if 'delta' in choice and 'content' in choice['delta']:
685
- content = choice['delta']['content']
686
- # Fix: Check if content is not None before concatenating
687
- if content is not None:
688
- streaming_text += content
689
- resp = dict(text=content)
690
- yield resp if raw else resp
691
- except json.JSONDecodeError:
692
- continue
693
- except Exception as e:
694
- print(f"Error processing chunk: {e}")
695
- continue
696
-
697
- self.conversation.update_chat_history(prompt, streaming_text)
698
-
699
- except requests.RequestException as e:
676
+ response = self.session.post(
677
+ self.url, headers=self.headers, data=json.dumps(payload), stream=True, timeout=self.timeout,
678
+ impersonate="chrome120" # Add impersonate
679
+ )
680
+ response.raise_for_status()
681
+
682
+ streaming_text = ""
683
+ # Use sanitize_stream
684
+ processed_stream = sanitize_stream(
685
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
686
+ intro_value="data:",
687
+ to_json=True, # Stream sends JSON
688
+ skip_markers=["[DONE]"],
689
+ content_extractor=self._electronhub_extractor, # Use the specific extractor
690
+ yield_raw_on_error=False # Skip non-JSON lines or lines where extractor fails
691
+ )
692
+
693
+ for content_chunk in processed_stream:
694
+ # content_chunk is the string extracted by _electronhub_extractor
695
+ if content_chunk and isinstance(content_chunk, str):
696
+ streaming_text += content_chunk
697
+ resp = dict(text=content_chunk)
698
+ yield resp if not raw else content_chunk
699
+
700
+ except CurlError as e:
701
+ raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {str(e)}") from e
702
+ except Exception as e:
700
703
  raise exceptions.FailedToGenerateResponseError(f"Request failed: {str(e)}")
704
+ finally:
705
+ # Update history after stream finishes or fails
706
+ if streaming_text:
707
+ self.conversation.update_chat_history(prompt, streaming_text)
701
708
 
702
709
  def for_non_stream():
703
710
  collected_response = ""
@@ -710,7 +717,9 @@ class ElectronHub(Provider):
710
717
  except Exception as e:
711
718
  raise exceptions.FailedToGenerateResponseError(f"Error during non-stream processing: {str(e)}")
712
719
 
720
+ # Update history and last_response after aggregation
713
721
  self.last_response = {"text": collected_response}
722
+ self.conversation.update_chat_history(prompt, collected_response)
714
723
  return self.last_response
715
724
 
716
725
  return for_stream() if stream else for_non_stream()
@@ -1,10 +1,11 @@
1
- import requests
1
+ from curl_cffi import CurlError
2
+ from curl_cffi.requests import Session, Response # Import Response
2
3
  import json
3
4
  import uuid
4
- from typing import Any, Dict, Union, Optional
5
+ from typing import Any, Dict, Union, Optional, List
5
6
  from datetime import datetime
6
- from webscout.AIutel import Optimizers, Conversation, AwesomePrompts
7
- from webscout.AIbase import Provider
7
+ from webscout.AIutel import Optimizers, Conversation, AwesomePrompts, sanitize_stream # Import sanitize_stream
8
+ from webscout.AIbase import Provider
8
9
  from webscout import exceptions
9
10
  from webscout.litagent import LitAgent
10
11
 
@@ -22,6 +23,8 @@ MODEL_CONFIGS = {
22
23
  "gemini-2.0-flash-thinking-exp-01-21",
23
24
  "gemini-2.5-pro-exp-03-25",
24
25
  "gemini-2.0-pro-exp-02-05",
26
+ "gemini-2.5-flash-preview-04-17",
27
+
25
28
 
26
29
  ],
27
30
  },
@@ -87,6 +90,7 @@ class ExaChat(Provider):
87
90
  "gemini-2.0-flash-thinking-exp-01-21",
88
91
  "gemini-2.5-pro-exp-03-25",
89
92
  "gemini-2.0-pro-exp-02-05",
93
+ "gemini-2.5-flash-preview-04-17",
90
94
 
91
95
  # OpenRouter Models
92
96
  "mistralai/mistral-small-3.1-24b-instruct:free",
@@ -141,7 +145,7 @@ class ExaChat(Provider):
141
145
  if model not in self.AVAILABLE_MODELS:
142
146
  raise ValueError(f"Invalid model: {model}. Choose from: {self.AVAILABLE_MODELS}")
143
147
 
144
- self.session = requests.Session()
148
+ self.session = Session() # Use curl_cffi Session
145
149
  self.is_conversation = is_conversation
146
150
  self.max_tokens_to_sample = max_tokens
147
151
  self.timeout = timeout
@@ -166,7 +170,7 @@ class ExaChat(Provider):
166
170
  }
167
171
 
168
172
  self.session.headers.update(self.headers)
169
- self.session.proxies = proxies
173
+ self.session.proxies = proxies # Assign proxies directly
170
174
  self.session.cookies.update({"session": uuid.uuid4().hex})
171
175
 
172
176
  self.__available_optimizers = (
@@ -208,18 +212,27 @@ class ExaChat(Provider):
208
212
  error_msg = f"Invalid model: {model}\nAvailable models: {', '.join(available_models)}"
209
213
  raise ValueError(error_msg)
210
214
 
211
- def _make_request(self, payload: Dict[str, Any]) -> requests.Response:
215
+ @staticmethod
216
+ def _exachat_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
217
+ """Extracts content from ExaChat stream JSON objects."""
218
+ if isinstance(chunk, dict):
219
+ return chunk.get("choices", [{}])[0].get("delta", {}).get("content")
220
+ return None
221
+
222
+ def _make_request(self, payload: Dict[str, Any]) -> Response: # Change type hint to Response
212
223
  """Make the API request with proper error handling."""
213
224
  try:
214
225
  response = self.session.post(
215
226
  self._get_endpoint(),
216
227
  headers=self.headers,
217
228
  json=payload,
218
- timeout=self.timeout,
229
+ timeout=self.timeout, # type: ignore
230
+ stream=True, # Enable streaming for the request
231
+ impersonate="chrome120" # Add impersonate
219
232
  )
220
233
  response.raise_for_status()
221
234
  return response
222
- except requests.exceptions.RequestException as e:
235
+ except (CurlError, exceptions.FailedToGenerateResponseError, Exception) as e: # Catch CurlError and others
223
236
  raise exceptions.FailedToGenerateResponseError(f"API request failed: {e}") from e
224
237
 
225
238
  def _build_payload(self, conversation_prompt: str) -> Dict[str, Any]:
@@ -271,20 +284,23 @@ class ExaChat(Provider):
271
284
 
272
285
  try:
273
286
  full_response = ""
274
- for line in response.iter_lines():
275
- if line:
276
- try:
277
- data = json.loads(line.decode('utf-8'))
278
- if 'choices' in data and len(data['choices']) > 0:
279
- content = data['choices'][0].get('delta', {}).get('content', '')
280
- if content:
281
- full_response += content
282
- except json.JSONDecodeError:
283
- continue
287
+ # Use sanitize_stream to process the response
288
+ processed_stream = sanitize_stream(
289
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
290
+ intro_value=None, # API doesn't seem to use 'data:' prefix
291
+ to_json=True, # Stream sends JSON lines
292
+ content_extractor=self._exachat_extractor, # Use the specific extractor
293
+ yield_raw_on_error=False # Skip non-JSON lines or lines where extractor fails
294
+ )
295
+
296
+ for content_chunk in processed_stream:
297
+ # content_chunk is the string extracted by _exachat_extractor
298
+ if content_chunk and isinstance(content_chunk, str):
299
+ full_response += content_chunk
284
300
 
285
301
  self.last_response = {"text": full_response}
286
302
  self.conversation.update_chat_history(prompt, full_response)
287
- return self.last_response
303
+ return self.last_response if not raw else full_response # Return dict or raw string
288
304
 
289
305
  except json.JSONDecodeError as e:
290
306
  raise exceptions.FailedToGenerateResponseError(f"Invalid JSON response: {e}") from e
@@ -1,11 +1,11 @@
1
- from typing import Generator, Union
1
+ from typing import Any, Dict, Generator, Optional, Union
2
2
  from curl_cffi.requests import Session
3
3
  from curl_cffi import CurlError
4
4
  import json
5
5
 
6
6
  from webscout import exceptions
7
7
  from webscout.AIutel import Optimizers
8
- from webscout.AIutel import Conversation
8
+ from webscout.AIutel import Conversation, sanitize_stream # Import sanitize_stream
9
9
  from webscout.AIutel import AwesomePrompts
10
10
  from webscout.AIbase import Provider
11
11
 
@@ -75,6 +75,13 @@ class GPTWeb(Provider):
75
75
  )
76
76
  self.conversation.history_offset = history_offset
77
77
 
78
+ @staticmethod
79
+ def _gptweb_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
80
+ """Extracts content from GPTWeb stream JSON objects."""
81
+ if isinstance(chunk, dict):
82
+ return chunk.get("gpt")
83
+ return None
84
+
78
85
  def ask(
79
86
  self,
80
87
  prompt: str,
@@ -129,22 +136,21 @@ class GPTWeb(Provider):
129
136
  )
130
137
  response.raise_for_status() # Check for HTTP errors
131
138
 
132
- # Iterate over bytes and decode manually
133
- for line_bytes in response.iter_lines():
134
- if line_bytes:
135
- try:
136
- line = line_bytes.decode('utf-8').lstrip('_') # Remove "_"
137
- # Attempt to parse the entire line as JSON
138
- json_data = json.loads(line)
139
- content = json_data.get("gpt", "")
140
- if content: # Ensure content is not None or empty
141
- full_response = content # API seems to send the full response each time
142
- resp = dict(text=full_response)
143
- # Yield dict or raw string chunk (yielding full response each time)
144
- yield resp if not raw else full_response
145
- except (json.JSONDecodeError, UnicodeDecodeError):
146
- # print(f"Skipping invalid JSON line: {line}") # Optional: for debugging
147
- continue # Ignore lines that are not valid JSON or cannot be decoded
139
+ # Use sanitize_stream
140
+ processed_stream = sanitize_stream(
141
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
142
+ intro_value=None, # No standard prefix, potential '_' handled by json.loads
143
+ to_json=True, # Stream sends JSON lines
144
+ content_extractor=self._gptweb_extractor, # Use the specific extractor
145
+ yield_raw_on_error=False # Skip non-JSON lines or lines where extractor fails
146
+ )
147
+
148
+ for content_chunk in processed_stream:
149
+ # content_chunk is the full text extracted by _gptweb_extractor
150
+ if content_chunk and isinstance(content_chunk, str):
151
+ full_response = content_chunk # API sends full response each time
152
+ resp = dict(text=full_response)
153
+ yield resp if not raw else full_response
148
154
 
149
155
  # Update history after stream finishes (using the final full response)
150
156
  self.last_response = dict(text=full_response)