webscout 8.2.4__py3-none-any.whl → 8.2.6__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 (110) hide show
  1. webscout/AIauto.py +112 -22
  2. webscout/AIutel.py +240 -344
  3. webscout/Extra/autocoder/autocoder.py +66 -5
  4. webscout/Extra/gguf.py +2 -0
  5. webscout/Provider/AISEARCH/scira_search.py +3 -5
  6. webscout/Provider/Aitopia.py +75 -51
  7. webscout/Provider/AllenAI.py +64 -67
  8. webscout/Provider/ChatGPTClone.py +33 -34
  9. webscout/Provider/ChatSandbox.py +342 -0
  10. webscout/Provider/Cloudflare.py +79 -32
  11. webscout/Provider/Deepinfra.py +69 -56
  12. webscout/Provider/ElectronHub.py +48 -39
  13. webscout/Provider/ExaChat.py +36 -20
  14. webscout/Provider/GPTWeb.py +24 -18
  15. webscout/Provider/GithubChat.py +52 -49
  16. webscout/Provider/GizAI.py +285 -0
  17. webscout/Provider/Glider.py +39 -28
  18. webscout/Provider/Groq.py +48 -20
  19. webscout/Provider/HeckAI.py +18 -36
  20. webscout/Provider/Jadve.py +30 -37
  21. webscout/Provider/LambdaChat.py +36 -59
  22. webscout/Provider/MCPCore.py +18 -21
  23. webscout/Provider/Marcus.py +23 -14
  24. webscout/Provider/Nemotron.py +218 -0
  25. webscout/Provider/Netwrck.py +35 -26
  26. webscout/Provider/OPENAI/__init__.py +1 -1
  27. webscout/Provider/OPENAI/exachat.py +4 -0
  28. webscout/Provider/OPENAI/scirachat.py +3 -4
  29. webscout/Provider/OPENAI/textpollinations.py +20 -22
  30. webscout/Provider/OPENAI/toolbaz.py +1 -0
  31. webscout/Provider/PI.py +22 -13
  32. webscout/Provider/StandardInput.py +42 -30
  33. webscout/Provider/TeachAnything.py +24 -12
  34. webscout/Provider/TextPollinationsAI.py +78 -76
  35. webscout/Provider/TwoAI.py +120 -88
  36. webscout/Provider/TypliAI.py +305 -0
  37. webscout/Provider/Venice.py +24 -22
  38. webscout/Provider/VercelAI.py +31 -12
  39. webscout/Provider/WiseCat.py +1 -1
  40. webscout/Provider/WrDoChat.py +370 -0
  41. webscout/Provider/__init__.py +11 -13
  42. webscout/Provider/ai4chat.py +5 -3
  43. webscout/Provider/akashgpt.py +59 -66
  44. webscout/Provider/asksteve.py +53 -44
  45. webscout/Provider/cerebras.py +77 -31
  46. webscout/Provider/chatglm.py +47 -37
  47. webscout/Provider/elmo.py +38 -32
  48. webscout/Provider/freeaichat.py +57 -43
  49. webscout/Provider/granite.py +24 -21
  50. webscout/Provider/hermes.py +27 -20
  51. webscout/Provider/learnfastai.py +25 -20
  52. webscout/Provider/llmchatco.py +48 -78
  53. webscout/Provider/multichat.py +13 -3
  54. webscout/Provider/scira_chat.py +50 -30
  55. webscout/Provider/scnet.py +27 -21
  56. webscout/Provider/searchchat.py +16 -24
  57. webscout/Provider/sonus.py +37 -39
  58. webscout/Provider/toolbaz.py +24 -46
  59. webscout/Provider/turboseek.py +37 -41
  60. webscout/Provider/typefully.py +30 -22
  61. webscout/Provider/typegpt.py +47 -51
  62. webscout/Provider/uncovr.py +46 -40
  63. webscout/__init__.py +0 -1
  64. webscout/cli.py +256 -0
  65. webscout/conversation.py +305 -448
  66. webscout/exceptions.py +3 -0
  67. webscout/swiftcli/__init__.py +80 -794
  68. webscout/swiftcli/core/__init__.py +7 -0
  69. webscout/swiftcli/core/cli.py +297 -0
  70. webscout/swiftcli/core/context.py +104 -0
  71. webscout/swiftcli/core/group.py +241 -0
  72. webscout/swiftcli/decorators/__init__.py +28 -0
  73. webscout/swiftcli/decorators/command.py +221 -0
  74. webscout/swiftcli/decorators/options.py +220 -0
  75. webscout/swiftcli/decorators/output.py +252 -0
  76. webscout/swiftcli/exceptions.py +21 -0
  77. webscout/swiftcli/plugins/__init__.py +9 -0
  78. webscout/swiftcli/plugins/base.py +135 -0
  79. webscout/swiftcli/plugins/manager.py +262 -0
  80. webscout/swiftcli/utils/__init__.py +59 -0
  81. webscout/swiftcli/utils/formatting.py +252 -0
  82. webscout/swiftcli/utils/parsing.py +267 -0
  83. webscout/version.py +1 -1
  84. {webscout-8.2.4.dist-info → webscout-8.2.6.dist-info}/METADATA +166 -45
  85. {webscout-8.2.4.dist-info → webscout-8.2.6.dist-info}/RECORD +89 -89
  86. {webscout-8.2.4.dist-info → webscout-8.2.6.dist-info}/WHEEL +1 -1
  87. webscout-8.2.6.dist-info/entry_points.txt +3 -0
  88. {webscout-8.2.4.dist-info → webscout-8.2.6.dist-info}/top_level.txt +0 -1
  89. inferno/__init__.py +0 -6
  90. inferno/__main__.py +0 -9
  91. inferno/cli.py +0 -6
  92. inferno/lol.py +0 -589
  93. webscout/LLM.py +0 -442
  94. webscout/Local/__init__.py +0 -12
  95. webscout/Local/__main__.py +0 -9
  96. webscout/Local/api.py +0 -576
  97. webscout/Local/cli.py +0 -516
  98. webscout/Local/config.py +0 -75
  99. webscout/Local/llm.py +0 -287
  100. webscout/Local/model_manager.py +0 -253
  101. webscout/Local/server.py +0 -721
  102. webscout/Local/utils.py +0 -93
  103. webscout/Provider/Chatify.py +0 -175
  104. webscout/Provider/PizzaGPT.py +0 -228
  105. webscout/Provider/askmyai.py +0 -158
  106. webscout/Provider/gaurish.py +0 -244
  107. webscout/Provider/promptrefine.py +0 -193
  108. webscout/Provider/tutorai.py +0 -270
  109. webscout-8.2.4.dist-info/entry_points.txt +0 -5
  110. {webscout-8.2.4.dist-info → webscout-8.2.6.dist-info}/licenses/LICENSE.md +0 -0
@@ -7,19 +7,16 @@ import queue
7
7
  import tempfile
8
8
  import threading
9
9
  import subprocess
10
+ from typing import Optional, Generator, List, Tuple, Dict, Any, NamedTuple
10
11
  from rich.panel import Panel
11
12
  from rich.syntax import Syntax
12
- from rich.console import Console, Group
13
+ from rich.console import Console
13
14
  from rich.markdown import Markdown
14
15
  from rich.table import Table
15
16
  from rich.theme import Theme
16
17
  from rich.live import Live
17
- from rich.rule import Rule
18
18
  from rich.box import ROUNDED
19
- from typing import Optional, Generator, List, Tuple, Dict, Any
20
- from webscout.AIutel import run_system_command
21
19
  from .autocoder_utiles import get_intro_prompt
22
-
23
20
  # Initialize LitLogger with custom format and colors
24
21
  default_path = tempfile.mkdtemp(prefix="webscout_autocoder")
25
22
 
@@ -34,6 +31,70 @@ CUSTOM_THEME = Theme({
34
31
  })
35
32
 
36
33
  console = Console(theme=CUSTOM_THEME)
34
+ class CommandResult(NamedTuple):
35
+ """Result of a system command execution."""
36
+ success: bool
37
+ stdout: str
38
+ stderr: str
39
+
40
+ def run_system_command(
41
+ command: str,
42
+ exit_on_error: bool = False,
43
+ stdout_error: bool = False,
44
+ help: Optional[str] = None
45
+ ) -> Tuple[bool, CommandResult]:
46
+ """Execute a system command and return the result.
47
+
48
+ Args:
49
+ command (str): Command to execute
50
+ exit_on_error (bool): Whether to exit on error. Defaults to False.
51
+ stdout_error (bool): Whether to include stdout in error messages. Defaults to False.
52
+ help (str, optional): Help message for errors. Defaults to None.
53
+
54
+ Returns:
55
+ Tuple[bool, CommandResult]: Success status and command result containing stdout/stderr
56
+ """
57
+ try:
58
+ # Execute command and capture output
59
+ process = subprocess.Popen(
60
+ command,
61
+ stdout=subprocess.PIPE,
62
+ stderr=subprocess.PIPE,
63
+ shell=True,
64
+ text=True
65
+ )
66
+
67
+ # Get stdout and stderr
68
+ stdout, stderr = process.communicate()
69
+ success = process.returncode == 0
70
+
71
+ # Create result object
72
+ result = CommandResult(
73
+ success=success,
74
+ stdout=stdout.strip() if stdout else "",
75
+ stderr=stderr.strip() if stderr else ""
76
+ )
77
+
78
+ # Handle errors if needed
79
+ if not success and exit_on_error:
80
+ error_msg = stderr if stderr else stdout if stdout_error else "Command failed"
81
+ if help:
82
+ error_msg += f"\n{help}"
83
+ sys.exit(error_msg)
84
+
85
+ return success, result
86
+
87
+ except Exception as e:
88
+ # Handle execution errors
89
+ error_msg = str(e)
90
+ if help:
91
+ error_msg += f"\n{help}"
92
+
93
+ if exit_on_error:
94
+ sys.exit(error_msg)
95
+
96
+ return False, CommandResult(success=False, stdout="", stderr=error_msg)
97
+
37
98
 
38
99
  class AutoCoder:
39
100
  """Generate and auto-execute Python scripts in the CLI with advanced error handling and retry logic.
webscout/Extra/gguf.py CHANGED
@@ -1,6 +1,8 @@
1
1
  """
2
2
  Convert Hugging Face models to GGUF format with advanced features.
3
3
 
4
+ For detailed documentation, see: webscout/Extra/gguf.md
5
+
4
6
  >>> python -m webscout.Extra.gguf convert -m "OEvortex/HelpingAI-Lite-1.5T" -q "q4_k_m,q5_k_m"
5
7
  >>> # With upload options:
6
8
  >>> python -m webscout.Extra.gguf convert -m "your-model" -u "username" -t "token" -q "q4_k_m"
@@ -69,17 +69,15 @@ class Scira(AISearch):
69
69
  """
70
70
 
71
71
  AVAILABLE_MODELS = {
72
- "scira-default": "Grok3",
73
- "scira-grok-3-mini": "Grok3-mini", # thinking model
72
+ "scira-default": "Grok3-mini", # thinking model
73
+ "scira-grok-3": "Grok3",
74
+ "scira-anthropic": "Sonnet 3.7 thinking",
74
75
  "scira-vision" : "Grok2-Vision", # vision model
75
76
  "scira-4.1-mini": "GPT4.1-mini",
76
77
  "scira-qwq": "QWQ-32B",
77
78
  "scira-o4-mini": "o4-mini",
78
79
  "scira-google": "gemini 2.5 flash"
79
-
80
-
81
80
  }
82
-
83
81
  def __init__(
84
82
  self,
85
83
  timeout: int = 60,
@@ -1,4 +1,5 @@
1
- import requests
1
+ from curl_cffi import CurlError
2
+ from curl_cffi.requests import Session
2
3
  import json
3
4
  import uuid
4
5
  import time
@@ -6,8 +7,8 @@ import hashlib
6
7
  from typing import Any, Dict, Optional, Generator, Union
7
8
 
8
9
  from webscout.AIutel import Optimizers
9
- from webscout.AIutel import Conversation
10
- from webscout.AIutel import AwesomePrompts, sanitize_stream
10
+ from webscout.AIutel import Conversation, sanitize_stream # Import sanitize_stream
11
+ from webscout.AIutel import AwesomePrompts
11
12
  from webscout.AIbase import Provider, AsyncProvider
12
13
  from webscout import exceptions
13
14
  from webscout.litagent import LitAgent
@@ -67,9 +68,9 @@ class Aitopia(Provider):
67
68
  "user-agent": self.fingerprint["user_agent"]
68
69
  }
69
70
 
70
- self.session = requests.Session()
71
+ self.session = Session() # Use curl_cffi Session
71
72
  self.session.headers.update(self.headers)
72
- self.session.proxies.update(proxies)
73
+ self.session.proxies = proxies # Assign proxies directly
73
74
 
74
75
  self.is_conversation = is_conversation
75
76
  self.max_tokens_to_sample = max_tokens
@@ -129,6 +130,20 @@ class Aitopia(Provider):
129
130
  random_str = str(uuid.uuid4()) + str(time.time())
130
131
  return hashlib.md5(random_str.encode()).hexdigest()
131
132
 
133
+ @staticmethod
134
+ def _aitopia_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
135
+ """Extracts content from Aitopia stream JSON objects."""
136
+ if isinstance(chunk, dict):
137
+ # Handle Claude 3 Haiku response format
138
+ if "delta" in chunk and "text" in chunk["delta"]:
139
+ return chunk["delta"]["text"]
140
+ # Handle GPT-4o Mini response format
141
+ elif "choices" in chunk and "0" in chunk["choices"]:
142
+ return chunk["choices"]["0"]["delta"].get("content")
143
+ # Add other potential formats here if needed
144
+ return None
145
+
146
+
132
147
  def ask(
133
148
  self,
134
149
  prompt: str,
@@ -184,63 +199,72 @@ class Aitopia(Provider):
184
199
  }
185
200
 
186
201
  def for_stream():
202
+ streaming_text = "" # Initialize outside try block
187
203
  try:
188
- with requests.post(self.url, headers=self.headers, json=payload, stream=True, timeout=self.timeout) as response:
189
- if response.status_code != 200:
190
- raise exceptions.FailedToGenerateResponseError(
191
- f"Request failed with status code {response.status_code}"
192
- )
193
-
194
- streaming_text = ""
195
- for line in response.iter_lines():
196
- if line:
197
- line = line.decode('utf-8')
198
- if line.startswith('data: '):
199
- data = line[6:]
200
- if data == '[DONE]':
201
- break
202
- try:
203
- json_data = json.loads(data)
204
-
205
- # Handle Claude 3 Haiku response format
206
- if "delta" in json_data and "text" in json_data["delta"]:
207
- content = json_data["delta"]["text"]
208
- if content:
209
- streaming_text += content
210
- resp = dict(text=content)
211
- yield resp if raw else resp
212
- # Handle GPT-4o Mini response format
213
- elif "choices" in json_data and "0" in json_data["choices"]:
214
- content = json_data["choices"]["0"]["delta"].get("content", "")
215
- if content:
216
- streaming_text += content
217
- resp = dict(text=content)
218
- yield resp if raw else resp
219
- except json.JSONDecodeError:
220
- continue
221
-
204
+ response = self.session.post(
205
+ self.url, headers=self.headers, json=payload, stream=True, timeout=self.timeout,
206
+ impersonate="chrome120" # Add impersonate
207
+ )
208
+ response.raise_for_status()
209
+
210
+ # Use sanitize_stream
211
+ processed_stream = sanitize_stream(
212
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
213
+ intro_value="data:",
214
+ to_json=True, # Stream sends JSON
215
+ skip_markers=["[DONE]"],
216
+ content_extractor=self._aitopia_extractor, # Use the specific extractor
217
+ yield_raw_on_error=False # Skip non-JSON lines or lines where extractor fails
218
+ )
219
+
220
+ for content_chunk in processed_stream:
221
+ # content_chunk is the string extracted by _aitopia_extractor
222
+ if content_chunk and isinstance(content_chunk, str):
223
+ streaming_text += content_chunk
224
+ resp = dict(text=content_chunk)
225
+ yield resp if not raw else content_chunk
226
+
227
+ except CurlError as e:
228
+ raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {str(e)}") from e
229
+ except Exception as e:
230
+ raise exceptions.FailedToGenerateResponseError(f"Request failed: {str(e)}")
231
+ finally:
232
+ # Update history after stream finishes or fails
233
+ if streaming_text:
222
234
  self.last_response = {"text": streaming_text}
223
235
  self.conversation.update_chat_history(prompt, streaming_text)
224
-
225
- except requests.RequestException as e:
226
- raise exceptions.FailedToGenerateResponseError(f"Request failed: {str(e)}")
227
236
 
228
237
  def for_non_stream():
229
238
  try:
230
- response = requests.post(self.url, headers=self.headers, json=payload, timeout=self.timeout)
231
- if response.status_code != 200:
232
- raise exceptions.FailedToGenerateResponseError(
233
- f"Request failed with status code {response.status_code}"
234
- )
239
+ response = self.session.post(
240
+ self.url, headers=self.headers, json=payload, timeout=self.timeout,
241
+ impersonate="chrome120" # Add impersonate
242
+ )
243
+ response.raise_for_status()
244
+
245
+ response_text_raw = response.text # Get raw text
246
+
247
+ # Use sanitize_stream to parse the non-streaming JSON response
248
+ # Assuming non-stream uses the GPT format based on original code
249
+ processed_stream = sanitize_stream(
250
+ data=response_text_raw,
251
+ to_json=True, # Parse the whole text as JSON
252
+ intro_value=None,
253
+ content_extractor=lambda chunk: chunk.get("choices", [{}])[0].get("message", {}).get("content") if isinstance(chunk, dict) else None,
254
+ yield_raw_on_error=False
255
+ )
256
+ # Extract the single result
257
+ content = next(processed_stream, None)
258
+ content = content if isinstance(content, str) else "" # Ensure it's a string
235
259
 
236
- response_data = response.json()
237
- if 'choices' in response_data and len(response_data['choices']) > 0:
238
- content = response_data['choices'][0].get('message', {}).get('content', '')
260
+ if content: # Check if content was successfully extracted
239
261
  self.last_response = {"text": content}
240
262
  self.conversation.update_chat_history(prompt, content)
241
263
  return {"text": content}
242
264
  else:
243
- raise exceptions.FailedToGenerateResponseError("No response content found")
265
+ raise exceptions.FailedToGenerateResponseError("No response content found or failed to parse")
266
+ except CurlError as e:
267
+ raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}") from e
244
268
  except Exception as e:
245
269
  raise exceptions.FailedToGenerateResponseError(f"Request failed: {e}")
246
270
 
@@ -156,19 +156,15 @@ class AllenAI(Provider):
156
156
  err_text = getattr(e, 'response', None) and getattr(e.response, 'text', '')
157
157
  return {"client": temp_id, "error": f"{type(e).__name__}: {e} - {err_text}"}
158
158
 
159
-
160
- def parse_stream(self, raw_data):
161
- """Parse the raw streaming data according to the JS implementation"""
162
- result = ""
163
- for line in raw_data.splitlines():
164
- try:
165
- parsed = json.loads(line)
166
- # Check if message starts with msg_ pattern
167
- if parsed.get("message", "").startswith("msg_"):
168
- result += parsed.get("content", "")
169
- except:
170
- continue
171
- return result
159
+ @staticmethod
160
+ def _allenai_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
161
+ """Extracts content from AllenAI stream JSON objects."""
162
+ if isinstance(chunk, dict):
163
+ if chunk.get("message", "").startswith("msg_") and "content" in chunk:
164
+ return chunk.get("content")
165
+ elif "message" in chunk and chunk.get("content"): # Legacy handling
166
+ return chunk.get("content")
167
+ return None
172
168
 
173
169
  def ask(
174
170
  self,
@@ -269,57 +265,49 @@ class AllenAI(Provider):
269
265
  )
270
266
  response.raise_for_status() # Check for HTTP errors
271
267
 
272
- # Iterate over bytes and decode manually
273
- for chunk_bytes in response.iter_content(chunk_size=None): # Process byte chunks
274
- if not chunk_bytes:
275
- continue
276
-
277
- try:
278
- decoded = chunk_bytes.decode('utf-8')
279
- for line in decoded.splitlines(): # Process lines within the chunk
280
- line = line.strip()
281
- if not line:
282
- continue
283
-
284
- data = json.loads(line)
285
-
286
- # Check for message pattern from JS implementation
287
- if isinstance(data, dict):
288
- content_found = False
289
- if data.get("message", "").startswith("msg_") and "content" in data:
290
- content = data.get("content", "")
291
- content_found = True
292
- # Legacy handling for older API
293
- elif "message" in data and data.get("content"):
294
- content = data.get("content")
295
- content_found = True
296
-
297
- if content_found and content:
298
- streaming_text += content
299
- resp = dict(text=content)
300
- yield resp if not raw else content
301
-
302
- # Update parent ID if present
303
- if data.get("id"):
304
- current_parent = data.get("id")
305
- elif data.get("children"):
306
- for child in data["children"]:
307
- if child.get("role") == "assistant":
308
- current_parent = child.get("id")
309
- break
310
-
311
- # Handle completion
312
- if data.get("final") or data.get("finish_reason") == "stop":
313
- if current_parent:
314
- self.parent = current_parent
315
-
316
- # Update conversation history
317
- self.conversation.update_chat_history(prompt, streaming_text)
318
- self.last_response = {"text": streaming_text}
319
- return # End the generator
268
+ # Use sanitize_stream
269
+ processed_stream = sanitize_stream(
270
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
271
+ intro_value=None, # No prefix
272
+ to_json=True, # Stream sends JSON lines
273
+ content_extractor=self._allenai_extractor, # Use the specific extractor
274
+ yield_raw_on_error=False # Skip non-JSON lines or lines where extractor fails
275
+ )
276
+
277
+ for content_chunk in processed_stream:
278
+ # content_chunk is the string extracted by _allenai_extractor
279
+ if content_chunk and isinstance(content_chunk, str):
280
+ streaming_text += content_chunk
281
+ resp = dict(text=content_chunk)
282
+ yield resp if not raw else content_chunk
320
283
 
321
- except (json.JSONDecodeError, UnicodeDecodeError):
322
- continue # Ignore lines/chunks that are not valid JSON or cannot be decoded
284
+ # Try to extract parent ID from the *last* raw line (less reliable than before)
285
+ # This part is tricky as sanitize_stream consumes the raw lines.
286
+ # We might need to re-fetch or adjust if parent ID is critical per stream.
287
+ # For now, we'll rely on the non-stream request to update parent ID more reliably.
288
+ # Example placeholder logic (might not work reliably):
289
+ try:
290
+ last_line_data = json.loads(response.text.splitlines()[-1]) # Get last line if possible
291
+ if last_line_data.get("id"):
292
+ current_parent = last_line_data.get("id")
293
+ elif last_line_data.get("children"):
294
+ for child in last_line_data["children"]: # Use last_line_data here
295
+ if child.get("role") == "assistant":
296
+ current_parent = child.get("id")
297
+ break
298
+
299
+ # Handle completion
300
+ if last_line_data.get("final") or last_line_data.get("finish_reason") == "stop":
301
+ if current_parent:
302
+ self.parent = current_parent
303
+
304
+ # Update conversation history
305
+ self.conversation.update_chat_history(prompt, streaming_text)
306
+ self.last_response = {"text": streaming_text} # Update last response here
307
+ return # End the generator
308
+ except Exception as e:
309
+ # Log the error but continue with the rest of the function
310
+ print(f"Error processing response data: {str(e)}")
323
311
 
324
312
  # If loop finishes without returning (e.g., no final message), update history
325
313
  if current_parent:
@@ -348,10 +336,19 @@ class AllenAI(Provider):
348
336
  )
349
337
  response.raise_for_status() # Check for HTTP errors
350
338
 
351
- # Parse the response as per JS implementation
352
- raw_response = response.text
353
- parsed_response = self.parse_stream(raw_response) # Use existing parser
354
-
339
+ raw_response = response.text # Get raw text
340
+
341
+ # Process the full text using sanitize_stream line by line
342
+ processed_stream = sanitize_stream(
343
+ data=raw_response.splitlines(), # Split into lines
344
+ intro_value=None,
345
+ to_json=True,
346
+ content_extractor=self._allenai_extractor,
347
+ yield_raw_on_error=False
348
+ )
349
+ # Aggregate the results
350
+ parsed_response = "".join(list(processed_stream))
351
+
355
352
  # Update parent ID from the full response if possible (might need adjustment based on actual non-stream response structure)
356
353
  # This part is speculative as the non-stream structure isn't fully clear from the stream logic
357
354
  try:
@@ -10,7 +10,7 @@ from datetime import date
10
10
 
11
11
  from webscout.AIutel import Optimizers
12
12
  from webscout.AIutel import Conversation
13
- from webscout.AIutel import AwesomePrompts
13
+ from webscout.AIutel import AwesomePrompts, sanitize_stream # Import sanitize_stream
14
14
  from webscout.AIbase import Provider
15
15
  from webscout import WEBS, exceptions
16
16
  # from webscout.litagent import LitAgent
@@ -105,6 +105,17 @@ class ChatGPTClone(Provider):
105
105
  self.session.headers.update(self.headers)
106
106
  return self.impersonate
107
107
 
108
+ @staticmethod
109
+ def _chatgptclone_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
110
+ """Extracts content from the ChatGPTClone stream format '0:"..."'."""
111
+ if isinstance(chunk, str):
112
+ match = re.search(r'0:"((?:[^\\"]|\\.)*)"', chunk) # Use the existing regex
113
+ if match:
114
+ content = match.group(1)
115
+ # Decode JSON string escapes and then unicode escapes
116
+ decoded_content = json.loads(f'"{content}"').encode().decode('unicode_escape')
117
+ return decoded_content
118
+ return None
108
119
  def ask(
109
120
  self,
110
121
  prompt: str,
@@ -154,44 +165,31 @@ class ChatGPTClone(Provider):
154
165
 
155
166
  def for_stream():
156
167
  response = _make_request()
157
- streaming_text = ""
158
- buffer = ""
168
+ streaming_text = "" # Initialize outside try block
159
169
  try:
160
- for line in response.iter_content():
161
- if line:
162
- # decode bytes to str before concatenation
163
- if isinstance(line, bytes):
164
- line = line.decode("utf-8", errors="replace")
165
- buffer += line
166
- match = re.search(r'0:"((?:[^\\"]|\\.)*)"', buffer)
167
- if match:
168
- content = match.group(1)
169
- try:
170
- decoded_content = json.loads(f'"{content}"')
171
- except json.JSONDecodeError:
172
- decoded_content = content.encode().decode('unicode_escape')
173
- streaming_text += decoded_content
174
- yield decoded_content if raw else dict(text=decoded_content)
175
- buffer = ""
176
- elif len(buffer) > 1024:
177
- buffer = ""
178
- if buffer:
179
- match = re.search(r'0:"((?:[^\\"]|\\.)*)"', buffer)
180
- if match:
181
- content = match.group(1)
182
- try:
183
- decoded_content = json.loads(f'"{content}"')
184
- except json.JSONDecodeError:
185
- decoded_content = content.encode().decode('unicode_escape')
186
- streaming_text += decoded_content
187
- yield decoded_content if raw else dict(text=decoded_content)
188
- self.last_response.update(dict(text=streaming_text))
189
- self.conversation.update_chat_history(prompt, streaming_text)
170
+ # Use sanitize_stream
171
+ processed_stream = sanitize_stream(
172
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
173
+ intro_value=None, # No simple prefix
174
+ to_json=False, # Content is text after extraction
175
+ content_extractor=self._chatgptclone_extractor, # Use the specific extractor
176
+ yield_raw_on_error=True # Yield even if extractor fails (might get metadata lines)
177
+ )
178
+
179
+ for content_chunk in processed_stream:
180
+ # content_chunk is the string extracted by _chatgptclone_extractor
181
+ if content_chunk and isinstance(content_chunk, str):
182
+ streaming_text += content_chunk
183
+ yield content_chunk if raw else dict(text=content_chunk)
184
+
190
185
  except RequestsError as e:
191
186
  raise exceptions.FailedToGenerateResponseError(f"Stream interrupted by request error: {e}") from e
192
187
  except Exception as e:
193
188
  raise exceptions.FailedToGenerateResponseError(f"Error processing stream: {e}") from e
194
189
  finally:
190
+ # Update history after stream finishes or fails
191
+ self.last_response.update(dict(text=streaming_text))
192
+ self.conversation.update_chat_history(prompt, streaming_text)
195
193
  response.close()
196
194
 
197
195
  def for_non_stream():
@@ -227,7 +225,8 @@ class ChatGPTClone(Provider):
227
225
  assert isinstance(response, dict)
228
226
  if not isinstance(response, dict) or "text" not in response:
229
227
  return str(response)
230
- formatted_text = response["text"]
228
+ # Extractor handles formatting
229
+ formatted_text = response.get("text", "")
231
230
  return formatted_text
232
231
 
233
232
  if __name__ == "__main__":