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
@@ -1,11 +1,12 @@
1
1
  from os import system
2
- import requests
2
+ from curl_cffi import CurlError
3
+ from curl_cffi.requests import Session
3
4
  import json
4
5
  import uuid
5
6
  import re
6
- from typing import Any, Dict, Optional, Union
7
+ from typing import Any, Dict, Optional, Union, List
7
8
  from webscout.AIutel import Optimizers
8
- from webscout.AIutel import Conversation
9
+ from webscout.AIutel import Conversation, sanitize_stream # Import sanitize_stream
9
10
  from webscout.AIutel import AwesomePrompts
10
11
  from webscout.AIbase import Provider
11
12
  from webscout import exceptions
@@ -17,15 +18,14 @@ class SciraAI(Provider):
17
18
  """
18
19
 
19
20
  AVAILABLE_MODELS = {
20
- "scira-default": "Grok3",
21
- "scira-grok-3-mini": "Grok3-mini", # thinking model
21
+ "scira-default": "Grok3-mini", # thinking model
22
+ "scira-grok-3": "Grok3",
23
+ "scira-anthropic": "Sonnet 3.7 thinking",
22
24
  "scira-vision" : "Grok2-Vision", # vision model
23
25
  "scira-4.1-mini": "GPT4.1-mini",
24
26
  "scira-qwq": "QWQ-32B",
25
27
  "scira-o4-mini": "o4-mini",
26
28
  "scira-google": "gemini 2.5 flash"
27
-
28
-
29
29
  }
30
30
 
31
31
  def __init__(
@@ -92,9 +92,9 @@ class SciraAI(Provider):
92
92
  "Sec-Fetch-Site": "same-origin"
93
93
  }
94
94
 
95
- self.session = requests.Session()
95
+ self.session = Session() # Use curl_cffi Session
96
96
  self.session.headers.update(self.headers)
97
- self.session.proxies.update(proxies)
97
+ self.session.proxies = proxies # Assign proxies directly
98
98
 
99
99
  self.is_conversation = is_conversation
100
100
  self.max_tokens_to_sample = max_tokens
@@ -150,12 +150,23 @@ class SciraAI(Provider):
150
150
 
151
151
  return self.fingerprint
152
152
 
153
+ @staticmethod
154
+ def _scira_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
155
+ """Extracts content from the Scira stream format '0:"..."'."""
156
+ if isinstance(chunk, str):
157
+ match = re.search(r'0:"(.*?)"(?=,|$)', chunk) # Look for 0:"...", possibly followed by comma or end of string
158
+ if match:
159
+ # Decode potential unicode escapes like \u00e9 and handle escaped quotes/backslashes
160
+ content = match.group(1).encode().decode('unicode_escape')
161
+ return content.replace('\\\\', '\\').replace('\\"', '"')
162
+ return None
163
+
153
164
  def ask(
154
165
  self,
155
166
  prompt: str,
156
167
  optimizer: str = None,
157
168
  conversationally: bool = False,
158
- ) -> Dict[str, Any]:
169
+ ) -> Dict[str, Any]: # Note: Stream parameter removed as API doesn't seem to support it
159
170
  conversation_prompt = self.conversation.gen_complete_prompt(prompt)
160
171
  if optimizer:
161
172
  if optimizer in self.__available_optimizers:
@@ -181,10 +192,16 @@ class SciraAI(Provider):
181
192
  }
182
193
 
183
194
  try:
184
- response = self.session.post(self.url, json=payload, timeout=self.timeout)
195
+ # Use curl_cffi post with impersonate
196
+ response = self.session.post(
197
+ self.url,
198
+ json=payload,
199
+ timeout=self.timeout,
200
+ impersonate="chrome120" # Add impersonate
201
+ )
185
202
  if response.status_code != 200:
186
203
  # Try to get response content for better error messages
187
- try:
204
+ try: # Use try-except for reading response content
188
205
  error_content = response.text
189
206
  except:
190
207
  error_content = "<could not read response content>"
@@ -192,7 +209,10 @@ class SciraAI(Provider):
192
209
  if response.status_code in [403, 429]:
193
210
  print(f"Received status code {response.status_code}, refreshing identity...")
194
211
  self.refresh_identity()
195
- response = self.session.post(self.url, json=payload, timeout=self.timeout)
212
+ response = self.session.post(
213
+ self.url, json=payload, timeout=self.timeout,
214
+ impersonate="chrome120" # Add impersonate to retry
215
+ )
196
216
  if not response.ok:
197
217
  raise exceptions.FailedToGenerateResponseError(
198
218
  f"Failed to generate response after identity refresh - ({response.status_code}, {response.reason}) - {error_content}"
@@ -203,28 +223,27 @@ class SciraAI(Provider):
203
223
  f"Request failed with status code {response.status_code}. Response: {error_content}"
204
224
  )
205
225
 
206
- full_response = ""
207
- debug_lines = []
208
-
209
- # Collect the first few lines for debugging
210
- for i, line in enumerate(response.iter_lines()):
211
- if line:
212
- try:
213
- line_str = line.decode('utf-8')
214
- debug_lines.append(line_str)
226
+ response_text_raw = response.text # Get raw response text
215
227
 
216
- # Format 2: 0:"content" (quoted format)
217
- match = re.search(r'0:"(.*?)"', line_str)
218
- if match:
219
- content = match.group(1)
220
- full_response += content
221
- continue
228
+ # Process the text using sanitize_stream line by line
229
+ processed_stream = sanitize_stream(
230
+ data=response_text_raw.splitlines(), # Split into lines
231
+ intro_value=None, # No simple prefix
232
+ to_json=False, # Content is not JSON
233
+ content_extractor=self._scira_extractor # Use the specific extractor
234
+ )
222
235
 
236
+ # Aggregate the results from the generator
237
+ full_response = ""
238
+ for content in processed_stream:
239
+ if content and isinstance(content, str):
240
+ full_response += content
223
241
 
224
- except: pass
225
242
  self.last_response = {"text": full_response}
226
243
  self.conversation.update_chat_history(prompt, full_response)
227
244
  return {"text": full_response}
245
+ except CurlError as e: # Catch CurlError
246
+ raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}") from e
228
247
  except Exception as e:
229
248
  raise exceptions.FailedToGenerateResponseError(f"Request failed: {e}")
230
249
 
@@ -242,7 +261,8 @@ class SciraAI(Provider):
242
261
 
243
262
  def get_message(self, response: dict) -> str:
244
263
  assert isinstance(response, dict), "Response should be of dict data-type only"
245
- return response["text"].replace('\\n', '\n').replace('\\n\\n', '\n\n')
264
+ # Extractor handles formatting
265
+ return response.get("text", "").replace('\\n', '\n').replace('\\n\\n', '\n\n')
246
266
 
247
267
  if __name__ == "__main__":
248
268
  print("-" * 100)
@@ -4,7 +4,7 @@ import json
4
4
  import secrets
5
5
  from typing import Any, Dict, Optional, Generator, Union
6
6
 
7
- from webscout.AIutel import Optimizers, Conversation, AwesomePrompts
7
+ from webscout.AIutel import Optimizers, Conversation, AwesomePrompts, sanitize_stream
8
8
  from webscout.AIbase import Provider
9
9
  from webscout import exceptions
10
10
 
@@ -29,7 +29,10 @@ class SCNet(Provider):
29
29
  is_conversation: bool = True,
30
30
  max_tokens: int = 2048, # Note: max_tokens is not used by this API
31
31
  timeout: int = 30,
32
- intro: Optional[str] = None,
32
+ intro: Optional[str] = ("You are a helpful, advanced LLM assistant. "
33
+ "You must always answer in English, regardless of the user's language. "
34
+ "If the user asks in another language, politely respond in English only. "
35
+ "Be clear, concise, and helpful."),
33
36
  filepath: Optional[str] = None,
34
37
  update_file: bool = True,
35
38
  proxies: Optional[dict] = None,
@@ -86,6 +89,13 @@ class SCNet(Provider):
86
89
  self.conversation = Conversation(is_conversation, max_tokens, filepath, update_file)
87
90
  self.conversation.history_offset = history_offset
88
91
 
92
+ @staticmethod
93
+ def _scnet_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
94
+ """Extracts content from SCNet stream JSON objects."""
95
+ if isinstance(chunk, dict):
96
+ return chunk.get("content")
97
+ return None
98
+
89
99
  def ask(
90
100
  self,
91
101
  prompt: str,
@@ -126,25 +136,21 @@ class SCNet(Provider):
126
136
  response.raise_for_status() # Check for HTTP errors
127
137
 
128
138
  streaming_text = ""
129
- # Iterate over bytes and decode manually
130
- for line_bytes in response.iter_lines():
131
- if line_bytes:
132
- line = line_bytes.decode('utf-8') # Decode bytes
133
- if line.startswith("data:"):
134
- data = line[5:].strip()
135
- if data and data != "[done]":
136
- try:
137
- obj = json.loads(data)
138
- content = obj.get("content", "")
139
- streaming_text += content
140
- resp = {"text": content}
141
- # Yield dict or raw string
142
- yield resp if not raw else content
143
- except (json.JSONDecodeError, UnicodeDecodeError):
144
- continue
145
- elif data == "[done]":
146
- break
147
-
139
+ # Use sanitize_stream
140
+ processed_stream = sanitize_stream(
141
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
142
+ intro_value="data:",
143
+ to_json=True, # Stream sends JSON
144
+ skip_markers=["[done]"],
145
+ content_extractor=self._scnet_extractor, # Use the specific extractor
146
+ yield_raw_on_error=False # Skip non-JSON lines or lines where extractor fails
147
+ )
148
+
149
+ for content_chunk in processed_stream:
150
+ # content_chunk is the string extracted by _scnet_extractor
151
+ if content_chunk and isinstance(content_chunk, str):
152
+ streaming_text += content_chunk
153
+ yield {"text": content_chunk} if not raw else content_chunk
148
154
  # Update history and last response after stream finishes
149
155
  self.last_response = {"text": streaming_text}
150
156
  self.conversation.update_chat_history(prompt, streaming_text)
@@ -6,7 +6,7 @@ from typing import Any, Dict, Optional, Generator, Union
6
6
 
7
7
  from webscout.AIutel import Optimizers
8
8
  from webscout.AIutel import Conversation
9
- from webscout.AIutel import AwesomePrompts
9
+ from webscout.AIutel import AwesomePrompts, sanitize_stream # Import sanitize_stream
10
10
  from webscout.AIbase import Provider
11
11
  from webscout import exceptions
12
12
  from webscout.litagent import LitAgent
@@ -183,33 +183,25 @@ class SearchChatAI(Provider):
183
183
  )
184
184
 
185
185
  streaming_text = ""
186
- # Iterate over bytes and decode manually
187
- for line_bytes in response.iter_lines():
188
- if line_bytes:
189
- line = line_bytes.decode('utf-8')
190
- if line.startswith('data: '):
191
- data_str = line[6:] # Remove 'data: ' prefix
192
-
193
- if data_str == '[DONE]':
194
- break
195
-
196
- try:
197
- data = json.loads(data_str)
198
- if "choices" in data and len(data["choices"]) > 0:
199
- delta = data["choices"][0].get("delta", {})
200
- if "content" in delta and delta["content"] is not None:
201
- content = delta["content"]
202
- streaming_text += content
203
- resp = dict(text=content)
204
- # Yield dict or raw string
205
- yield resp if not raw else content
206
- except (json.JSONDecodeError, UnicodeDecodeError):
207
- continue
186
+ # Use sanitize_stream
187
+ processed_stream = sanitize_stream(
188
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
189
+ intro_value="data:",
190
+ to_json=True, # Stream sends JSON
191
+ skip_markers=["[DONE]"],
192
+ content_extractor=lambda chunk: chunk.get('choices', [{}])[0].get('delta', {}).get('content') if isinstance(chunk, dict) else None,
193
+ yield_raw_on_error=False # Skip non-JSON or lines where extractor fails
194
+ )
195
+
196
+ for content_chunk in processed_stream:
197
+ # content_chunk is the string extracted by the content_extractor
198
+ if content_chunk and isinstance(content_chunk, str):
199
+ streaming_text += content_chunk
200
+ yield dict(text=content_chunk) if not raw else content_chunk
208
201
 
209
202
  # Update history and last response after stream finishes
210
203
  self.last_response = {"text": streaming_text}
211
204
  self.conversation.update_chat_history(prompt, streaming_text)
212
-
213
205
  except CurlError as e: # Catch CurlError
214
206
  raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {str(e)}") from e
215
207
  except Exception as e: # Catch other potential exceptions
@@ -4,7 +4,7 @@ import json
4
4
  from typing import Any, Dict, Optional, Generator, Union
5
5
  from webscout.AIutel import Optimizers
6
6
  from webscout.AIutel import Conversation
7
- from webscout.AIutel import AwesomePrompts
7
+ from webscout.AIutel import AwesomePrompts, sanitize_stream # Import sanitize_stream
8
8
  from webscout.AIbase import Provider
9
9
  from webscout import exceptions
10
10
  from webscout.litagent import LitAgent
@@ -78,6 +78,13 @@ class SonusAI(Provider):
78
78
  )
79
79
  self.conversation.history_offset = history_offset
80
80
 
81
+ @staticmethod
82
+ def _sonus_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
83
+ """Extracts content from Sonus stream JSON objects."""
84
+ if isinstance(chunk, dict) and "content" in chunk:
85
+ return chunk.get("content")
86
+ return None
87
+
81
88
  def ask(
82
89
  self,
83
90
  prompt: str,
@@ -124,30 +131,22 @@ class SonusAI(Provider):
124
131
  raise exceptions.FailedToGenerateResponseError(
125
132
  f"Request failed with status code {response.status_code} - {response.text}"
126
133
  )
127
-
134
+
128
135
  streaming_text = ""
129
- # Iterate over bytes and decode manually
130
- for line_bytes in response.iter_lines():
131
- if line_bytes:
132
- try:
133
- # Decode the line and remove 'data: ' prefix if present
134
- line = line_bytes.decode('utf-8')
135
- if line.startswith('data: '):
136
- line = line[6:]
137
-
138
- # Handle potential empty lines after prefix removal
139
- if not line.strip():
140
- continue
136
+ # Use sanitize_stream
137
+ processed_stream = sanitize_stream(
138
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
139
+ intro_value="data:",
140
+ to_json=True, # Stream sends JSON
141
+ content_extractor=self._sonus_extractor, # Use the specific extractor
142
+ yield_raw_on_error=False # Skip non-JSON lines or lines where extractor fails
143
+ )
141
144
 
142
- data = json.loads(line)
143
- if "content" in data:
144
- content = data["content"]
145
- streaming_text += content
146
- resp = dict(text=content)
147
- # Yield dict or raw string
148
- yield resp if raw else resp
149
- except (json.JSONDecodeError, UnicodeDecodeError):
150
- continue
145
+ for content_chunk in processed_stream:
146
+ # content_chunk is the string extracted by _sonus_extractor
147
+ if content_chunk and isinstance(content_chunk, str):
148
+ streaming_text += content_chunk
149
+ yield dict(text=content_chunk) if not raw else content_chunk
151
150
 
152
151
  # Update history and last response after stream finishes
153
152
  self.last_response = {"text": streaming_text}
@@ -173,23 +172,22 @@ class SonusAI(Provider):
173
172
  f"Request failed with status code {response.status_code} - {response.text}"
174
173
  )
175
174
 
175
+ response_text_raw = response.text # Get raw text
176
+
177
+ # Use sanitize_stream to process the non-streaming text
178
+ processed_stream = sanitize_stream(
179
+ data=response_text_raw.splitlines(), # Split into lines
180
+ intro_value="data:",
181
+ to_json=True,
182
+ content_extractor=self._sonus_extractor,
183
+ yield_raw_on_error=False
184
+ )
185
+
186
+ # Aggregate the results
176
187
  full_response = ""
177
- # Process the full response text which might contain multiple JSON objects
178
- # Split by lines and process each potential JSON object
179
- for line in response.text.splitlines():
180
- if line:
181
- try:
182
- if line.startswith('data: '):
183
- line = line[6:]
184
-
185
- if not line.strip():
186
- continue
187
-
188
- data = json.loads(line)
189
- if "content" in data:
190
- full_response += data["content"]
191
- except (json.JSONDecodeError, UnicodeDecodeError):
192
- continue
188
+ for content in processed_stream:
189
+ if content and isinstance(content, str):
190
+ full_response += content
193
191
 
194
192
  self.last_response = {"text": full_response}
195
193
  self.conversation.update_chat_history(prompt, full_response)
@@ -13,7 +13,7 @@ from typing import Any, Dict, Optional, Generator, Union, List
13
13
  from webscout import exceptions
14
14
  from webscout.AIutel import Optimizers
15
15
  from webscout.AIutel import Conversation
16
- from webscout.AIutel import AwesomePrompts
16
+ from webscout.AIutel import AwesomePrompts, sanitize_stream # Import sanitize_stream
17
17
  from webscout.AIbase import Provider
18
18
 
19
19
  class Toolbaz(Provider):
@@ -26,6 +26,7 @@ class Toolbaz(Provider):
26
26
  "gemini-2.0-flash-thinking",
27
27
  "gemini-2.0-flash",
28
28
  "gemini-1.5-flash",
29
+ "o3-mini",
29
30
  "gpt-4o-latest",
30
31
  "gpt-4o",
31
32
  "deepseek-r1",
@@ -111,6 +112,13 @@ class Toolbaz(Provider):
111
112
  )
112
113
  self.conversation.history_offset = history_offset
113
114
 
115
+ @staticmethod
116
+ def _toolbaz_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
117
+ """Removes [model:...] tags from a string chunk."""
118
+ if isinstance(chunk, str):
119
+ return re.sub(r"\[model:.*?\]", "", chunk)
120
+ return None
121
+
114
122
  def random_string(self, length):
115
123
  return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
116
124
 
@@ -207,53 +215,23 @@ class Toolbaz(Provider):
207
215
  )
208
216
  resp.raise_for_status()
209
217
 
210
- buffer = ""
211
- tag_start = "[model:"
212
218
  streaming_text = ""
213
219
 
214
- # Iterate over bytes and decode manually
215
- for chunk_bytes in resp.iter_content(chunk_size=1024): # Read in larger chunks
216
- if chunk_bytes:
217
- text = chunk_bytes.decode(errors="ignore")
218
- buffer += text
219
-
220
- processed_buffer = ""
221
- last_processed_index = 0
222
- # Find all complete tags and process text between them
223
- for match in re.finditer(r"\[model:.*?\]", buffer):
224
- # Add text before the tag
225
- segment = buffer[last_processed_index:match.start()]
226
- if segment:
227
- processed_buffer += segment
228
- last_processed_index = match.end()
229
-
230
- # Add remaining text after the last complete tag
231
- processed_buffer += buffer[last_processed_index:]
232
-
233
- # Now, check for incomplete tag at the end
234
- last_tag_start_index = processed_buffer.rfind(tag_start)
235
-
236
- if last_tag_start_index != -1:
237
- # Text before the potential incomplete tag
238
- text_to_yield = processed_buffer[:last_tag_start_index]
239
- # Keep the potential incomplete tag start for the next iteration
240
- buffer = processed_buffer[last_tag_start_index:]
241
- else:
242
- # No potential incomplete tag found, yield everything processed
243
- text_to_yield = processed_buffer
244
- buffer = "" # Clear buffer as everything is processed
245
-
246
- if text_to_yield:
247
- streaming_text += text_to_yield
248
- # Yield dict or raw string
249
- yield {"text": text_to_yield} if not raw else text_to_yield
250
-
251
- # Process any remaining text in the buffer after the loop finishes
252
- # Remove any potential tags (complete or incomplete)
253
- final_text = re.sub(r"\[model:.*?\]", "", buffer)
254
- if final_text:
255
- streaming_text += final_text
256
- yield {"text": final_text} if not raw else final_text
220
+ # Use sanitize_stream with the custom extractor
221
+ # It will decode bytes and yield processed string chunks
222
+ processed_stream = sanitize_stream(
223
+ data=resp.iter_content(chunk_size=None), # Pass byte iterator
224
+ intro_value=None, # No simple prefix
225
+ to_json=False, # Content is text
226
+ content_extractor=self._toolbaz_extractor, # Use the tag remover
227
+ yield_raw_on_error=True # Yield even if extractor somehow fails (though unlikely for regex)
228
+ )
229
+
230
+ for content_chunk in processed_stream:
231
+ # content_chunk is the string with tags removed
232
+ if content_chunk and isinstance(content_chunk, str):
233
+ streaming_text += content_chunk
234
+ yield {"text": content_chunk} if not raw else content_chunk
257
235
 
258
236
  self.last_response = {"text": streaming_text}
259
237
  self.conversation.update_chat_history(prompt, streaming_text)
@@ -4,10 +4,10 @@ import json
4
4
 
5
5
  from webscout.AIutel import Optimizers
6
6
  from webscout.AIutel import Conversation
7
- from webscout.AIutel import AwesomePrompts, sanitize_stream
7
+ from webscout.AIutel import AwesomePrompts, sanitize_stream # Import sanitize_stream
8
8
  from webscout.AIbase import Provider
9
9
  from webscout import exceptions
10
- from typing import Union, Any, AsyncGenerator, Dict
10
+ from typing import Optional, Union, Any, AsyncGenerator, Dict
11
11
  from webscout.litagent import LitAgent
12
12
 
13
13
  class TurboSeek(Provider):
@@ -88,6 +88,13 @@ class TurboSeek(Provider):
88
88
  )
89
89
  self.conversation.history_offset = history_offset
90
90
 
91
+ @staticmethod
92
+ def _turboseek_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
93
+ """Extracts content from TurboSeek stream JSON objects."""
94
+ if isinstance(chunk, dict) and "text" in chunk:
95
+ return chunk.get("text") # json.loads already handles unicode escapes
96
+ return None
97
+
91
98
  def ask(
92
99
  self,
93
100
  prompt: str,
@@ -142,24 +149,24 @@ class TurboSeek(Provider):
142
149
  raise exceptions.FailedToGenerateResponseError(
143
150
  f"Failed to generate response - ({response.status_code}, {response.reason}) - {response.text}"
144
151
  )
152
+
145
153
  streaming_text = ""
146
- # Iterate over bytes and decode manually
147
- for value_bytes in response.iter_lines():
148
- try:
149
- if value_bytes and value_bytes.startswith(b"data: "): # Check for bytes
150
- # Decode bytes to string
151
- line = value_bytes[6:].decode('utf-8')
152
- data = json.loads(line)
153
- if "text" in data:
154
- # Decode potential unicode escapes
155
- content = data["text"].encode().decode('unicode_escape')
156
- streaming_text += content
157
- resp = dict(text=content)
158
- self.last_response.update(resp) # Update last_response incrementally
159
- # Yield raw bytes or dict based on flag
160
- yield value_bytes if raw else resp
161
- except (json.decoder.JSONDecodeError, UnicodeDecodeError):
162
- pass # Ignore lines that are not valid JSON or cannot be decoded
154
+ # Use sanitize_stream with the custom extractor
155
+ processed_stream = sanitize_stream(
156
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
157
+ intro_value="data:",
158
+ to_json=True, # Stream sends JSON
159
+ content_extractor=self._turboseek_extractor, # Use the specific extractor
160
+ yield_raw_on_error=False # Skip non-JSON lines or lines where extractor fails
161
+ )
162
+
163
+ for content_chunk in processed_stream:
164
+ # content_chunk is the string extracted by _turboseek_extractor
165
+ if content_chunk and isinstance(content_chunk, str):
166
+ streaming_text += content_chunk
167
+ self.last_response.update(dict(text=streaming_text)) # Update last_response incrementally
168
+ yield dict(text=content_chunk) if not raw else content_chunk # Yield dict or raw string
169
+
163
170
  # Update conversation history after stream finishes
164
171
  if streaming_text: # Only update if content was received
165
172
  self.conversation.update_chat_history(
@@ -174,21 +181,15 @@ class TurboSeek(Provider):
174
181
  def for_non_stream():
175
182
  # Aggregate the stream using the updated for_stream logic
176
183
  full_text = ""
177
- for chunk_data in for_stream():
178
- # Ensure chunk_data is a dict (not raw) and has 'text'
179
- if isinstance(chunk_data, dict) and "text" in chunk_data:
180
- full_text += chunk_data["text"]
181
- # If raw=True, chunk_data is bytes, decode and process if needed (though raw non-stream is less common)
182
- elif isinstance(chunk_data, bytes):
183
- try:
184
- if chunk_data.startswith(b"data: "):
185
- line = chunk_data[6:].decode('utf-8')
186
- data = json.loads(line)
187
- if "text" in data:
188
- content = data["text"].encode().decode('unicode_escape')
189
- full_text += content
190
- except (json.decoder.JSONDecodeError, UnicodeDecodeError):
191
- pass
184
+ try:
185
+ # Ensure raw=False so for_stream yields dicts
186
+ for chunk_data in for_stream():
187
+ if isinstance(chunk_data, dict) and "text" in chunk_data:
188
+ full_text += chunk_data["text"]
189
+ elif isinstance(chunk_data, str): # Handle case where raw=True was passed
190
+ full_text += chunk_data
191
+ except Exception as e:
192
+ raise exceptions.FailedToGenerateResponseError(f"Failed to aggregate non-stream response: {e}") from e
192
193
  # last_response and history are updated within for_stream
193
194
  # Ensure last_response reflects the complete aggregated text
194
195
  self.last_response = {"text": full_text}
@@ -241,7 +242,7 @@ class TurboSeek(Provider):
241
242
  str: Message extracted
242
243
  """
243
244
  assert isinstance(response, dict), "Response should be of dict data-type only"
244
- # Text is already decoded in ask method
245
+ # Unicode escapes are handled by json.loads within sanitize_stream
245
246
  return response.get("text", "")
246
247
 
247
248
  if __name__ == '__main__':
@@ -250,13 +251,9 @@ if __name__ == '__main__':
250
251
  try: # Add try-except block for testing
251
252
  ai = TurboSeek(timeout=60)
252
253
  print("[bold blue]Testing Stream:[/bold blue]")
253
- response_stream = ai.chat("hello buddy", stream=True)
254
- full_stream_response = ""
254
+ response_stream = ai.chat("yooooooooooo", stream=True)
255
255
  for chunk in response_stream:
256
256
  print(chunk, end="", flush=True)
257
- full_stream_response += chunk
258
- print("\n[bold green]Stream Test Complete.[/bold green]\n")
259
-
260
257
  # Optional: Test non-stream
261
258
  # print("[bold blue]Testing Non-Stream:[/bold blue]")
262
259
  # response_non_stream = ai.chat("What is the capital of France?", stream=False)
@@ -267,4 +264,3 @@ if __name__ == '__main__':
267
264
  print(f"\n[bold red]API Error:[/bold red] {e}")
268
265
  except Exception as e:
269
266
  print(f"\n[bold red]An unexpected error occurred:[/bold red] {e}")
270
-