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,8 +1,11 @@
1
- import requests
1
+ from typing import Any, Dict, Optional, Union
2
+ from curl_cffi import CurlError
3
+ from curl_cffi.requests import Session
4
+ from webscout import exceptions
2
5
  from webscout.AIutel import Optimizers
3
6
  from webscout.AIutel import Conversation
4
- from webscout.AIutel import AwesomePrompts
5
- from webscout.AIbase import Provider
7
+ from webscout.AIutel import AwesomePrompts, sanitize_stream # Import sanitize_stream
8
+ from webscout.AIbase import Provider
6
9
  from webscout.litagent import LitAgent
7
10
 
8
11
  class AskSteve(Provider):
@@ -36,7 +39,7 @@ class AskSteve(Provider):
36
39
  act (str|int, optional): Awesome prompt key or index. (Used as intro). Defaults to None.
37
40
  system_prompt (str, optional): System prompt for AskSteve. Defaults to the provided string.
38
41
  """
39
- self.session = requests.Session()
42
+ self.session = Session() # Use curl_cffi Session
40
43
  self.is_conversation = is_conversation
41
44
  self.max_tokens_to_sample = max_tokens
42
45
  self.api_endpoint = "https://quickstart.asksteve.to/quickStartRequest"
@@ -73,7 +76,15 @@ class AskSteve(Provider):
73
76
  is_conversation, self.max_tokens_to_sample, filepath, update_file
74
77
  )
75
78
  self.conversation.history_offset = history_offset
76
- self.session.proxies = proxies
79
+ self.session.proxies = proxies # Assign proxies directly
80
+ @staticmethod
81
+ def _asksteve_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
82
+ """Extracts content from AskSteve JSON response."""
83
+ if isinstance(chunk, dict) and "candidates" in chunk and len(chunk["candidates"]) > 0:
84
+ parts = chunk["candidates"][0].get("content", {}).get("parts", [])
85
+ if parts and isinstance(parts[0].get("text"), str):
86
+ return parts[0]["text"]
87
+ return None
77
88
 
78
89
  def ask(
79
90
  self,
@@ -115,37 +126,43 @@ class AskSteve(Provider):
115
126
  "prompt": conversation_prompt
116
127
  }
117
128
 
118
- def for_stream():
129
+
130
+ # This API doesn't stream, so we process the full response
131
+ try:
119
132
  response = self.session.post(
120
133
  self.api_endpoint,
121
134
  headers=self.headers,
122
135
  json=payload,
123
- stream=True,
136
+ stream=False, # API doesn't stream
124
137
  timeout=self.timeout,
138
+ impersonate="chrome120" # Add impersonate
125
139
  )
126
- if not response.ok:
127
- raise Exception(
128
- f"Failed to generate response - ({response.status_code}, {response.reason}) - {response.text}"
129
- )
130
-
131
- response_data = response.json()
132
- if "candidates" in response_data and len(response_data["candidates"]) > 0:
133
- text = response_data["candidates"][0]["content"]["parts"][0]["text"]
134
- self.last_response.update(dict(text=text))
135
- yield dict(text=text) if not raw else text
136
- else:
137
- raise Exception("No response generated")
140
+ response.raise_for_status()
141
+ response_text_raw = response.text # Get raw text
142
+
143
+ # Process the full JSON text using sanitize_stream
144
+ processed_stream = sanitize_stream(
145
+ data=response_text_raw,
146
+ to_json=True, # Parse the whole text as JSON
147
+ intro_value=None,
148
+ content_extractor=self._asksteve_extractor, # Use the specific extractor
149
+ yield_raw_on_error=False
150
+ )
151
+ # Extract the single result
152
+ text = next(processed_stream, None)
153
+ text = text if isinstance(text, str) else "" # Ensure it's a string
138
154
 
155
+ self.last_response.update(dict(text=text))
139
156
  self.conversation.update_chat_history(
140
157
  prompt, self.get_message(self.last_response)
141
158
  )
159
+ # Return dict or raw string based on raw flag
160
+ return text if raw else self.last_response
142
161
 
143
- def for_non_stream():
144
- for _ in for_stream():
145
- pass
146
- return self.last_response
147
-
148
- return for_stream() if stream else for_non_stream()
162
+ except CurlError as e:
163
+ raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}") from e
164
+ except Exception as e: # Catch other potential errors
165
+ raise exceptions.FailedToGenerateResponseError(f"Failed to get response ({type(e).__name__}): {e}") from e
149
166
 
150
167
  def chat(
151
168
  self,
@@ -164,23 +181,15 @@ class AskSteve(Provider):
164
181
  str: Response generated
165
182
  """
166
183
 
167
- def for_stream():
168
- for response in self.ask(
169
- prompt, True, optimizer=optimizer, conversationally=conversationally
170
- ):
171
- yield self.get_message(response)
172
-
173
- def for_non_stream():
174
- return self.get_message(
175
- self.ask(
176
- prompt,
177
- False,
178
- optimizer=optimizer,
179
- conversationally=conversationally,
180
- )
181
- )
182
-
183
- return for_stream() if stream else for_non_stream()
184
+ # Since ask() doesn't truly stream, we just call it once.
185
+ response_data = self.ask(
186
+ prompt,
187
+ stream=False, # Always False for this API
188
+ raw=False, # Get the dict back
189
+ optimizer=optimizer,
190
+ conversationally=conversationally,
191
+ )
192
+ return self.get_message(response_data)
184
193
 
185
194
  def get_message(self, response: dict) -> str:
186
195
  """Retrieves message only from response
@@ -192,12 +201,12 @@ class AskSteve(Provider):
192
201
  str: Message extracted
193
202
  """
194
203
  assert isinstance(response, dict), "Response should be of dict data-type only"
195
- return response["text"]
204
+ return response.get("text", "") # Use .get for safety
196
205
 
197
206
 
198
207
  if __name__ == "__main__":
199
208
  from rich import print
200
209
  ai = AskSteve()
201
- response = ai.chat("hi", stream=True)
210
+ response = ai.chat("write a short poem about AI", stream=True)
202
211
  for chunk in response:
203
212
  print(chunk, end="", flush=True)
@@ -1,10 +1,11 @@
1
1
 
2
2
  import re
3
- import requests
3
+ import curl_cffi
4
+ from curl_cffi.requests import Session
4
5
  import json
5
6
  import os
6
7
  from typing import Any, Dict, Optional, Generator, List, Union
7
- from webscout.AIutel import Optimizers, Conversation, AwesomePrompts
8
+ from webscout.AIutel import Optimizers, Conversation, AwesomePrompts, sanitize_stream # Import sanitize_stream
8
9
  from webscout.AIbase import Provider
9
10
  from webscout import exceptions
10
11
  from webscout.litagent import LitAgent as UserAgent
@@ -17,7 +18,9 @@ class Cerebras(Provider):
17
18
  AVAILABLE_MODELS = [
18
19
  "llama3.1-8b",
19
20
  "llama-3.3-70b",
20
- "deepseek-r1-distill-llama-70b"
21
+ "deepseek-r1-distill-llama-70b",
22
+ "llama-4-scout-17b-16e-instruct"
23
+
21
24
  ]
22
25
 
23
26
  def __init__(
@@ -49,6 +52,8 @@ class Cerebras(Provider):
49
52
  self.max_tokens_to_sample = max_tokens
50
53
  self.last_response = {}
51
54
 
55
+ self.session = Session() # Initialize curl_cffi session
56
+
52
57
  # Get API key first
53
58
  try:
54
59
  self.api_key = self.get_demo_api_key(cookie_path)
@@ -74,6 +79,9 @@ class Cerebras(Provider):
74
79
  is_conversation, self.max_tokens_to_sample, filepath, update_file
75
80
  )
76
81
  self.conversation.history_offset = history_offset
82
+
83
+ # Apply proxies to the session
84
+ self.session.proxies = proxies
77
85
 
78
86
  # Rest of the class implementation remains the same...
79
87
  @staticmethod
@@ -88,7 +96,14 @@ class Cerebras(Provider):
88
96
  """Refines the input text by removing surrounding quotes."""
89
97
  return text.strip('"')
90
98
 
91
- def get_demo_api_key(self, cookie_path: str) -> str:
99
+ @staticmethod
100
+ def _cerebras_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
101
+ """Extracts content from Cerebras stream JSON objects."""
102
+ if isinstance(chunk, dict):
103
+ return chunk.get("choices", [{}])[0].get("delta", {}).get("content")
104
+ return None
105
+
106
+ def get_demo_api_key(self, cookie_path: str) -> str: # Keep this using requests or switch to curl_cffi
92
107
  """Retrieves the demo API key using the provided cookie."""
93
108
  try:
94
109
  with open(cookie_path, "r") as file:
@@ -114,17 +129,19 @@ class Cerebras(Provider):
114
129
  }
115
130
 
116
131
  try:
117
- response = requests.post(
132
+ # Use the initialized curl_cffi session
133
+ response = self.session.post(
118
134
  "https://inference.cerebras.ai/api/graphql",
119
135
  cookies=cookies,
120
136
  headers=headers,
121
137
  json=json_data,
122
138
  timeout=self.timeout,
139
+ impersonate="chrome120" # Add impersonate
123
140
  )
124
141
  response.raise_for_status()
125
- api_key = response.json()["data"]["GetMyDemoApiKey"]
142
+ api_key = response.json().get("data", {}).get("GetMyDemoApiKey")
126
143
  return api_key
127
- except requests.exceptions.RequestException as e:
144
+ except curl_cffi.CurlError as e:
128
145
  raise exceptions.APIConnectionError(f"Failed to retrieve API key: {e}")
129
146
  except KeyError:
130
147
  raise exceptions.InvalidResponseError("API key not found in response.")
@@ -144,41 +161,48 @@ class Cerebras(Provider):
144
161
  }
145
162
 
146
163
  try:
147
- response = requests.post(
164
+ # Use the initialized curl_cffi session
165
+ response = self.session.post(
148
166
  "https://api.cerebras.ai/v1/chat/completions",
149
167
  headers=headers,
150
168
  json=payload,
151
169
  stream=stream,
152
- timeout=self.timeout
170
+ timeout=self.timeout,
171
+ impersonate="chrome120" # Add impersonate
153
172
  )
154
173
  response.raise_for_status()
155
174
 
156
175
  if stream:
157
176
  def generate_stream():
158
- for line in response.iter_lines():
159
- if line:
160
- line = line.decode('utf-8')
161
- if line.startswith('data:'):
162
- try:
163
- data = json.loads(line[6:])
164
- if data.get('choices') and data['choices'][0].get('delta', {}).get('content'):
165
- content = data['choices'][0]['delta']['content']
166
- yield content
167
- except json.JSONDecodeError:
168
- continue
177
+ # Use sanitize_stream
178
+ processed_stream = sanitize_stream(
179
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
180
+ intro_value="data:",
181
+ to_json=True, # Stream sends JSON
182
+ content_extractor=self._cerebras_extractor, # Use the specific extractor
183
+ yield_raw_on_error=False # Skip non-JSON lines or lines where extractor fails
184
+ )
185
+ for content_chunk in processed_stream:
186
+ if content_chunk and isinstance(content_chunk, str):
187
+ yield content_chunk # Yield the extracted text chunk
169
188
 
170
189
  return generate_stream()
171
190
  else:
172
191
  response_json = response.json()
173
- return response_json['choices'][0]['message']['content']
192
+ # Extract content for non-streaming response
193
+ content = response_json.get("choices", [{}])[0].get("message", {}).get("content")
194
+ return content if content else "" # Return empty string if not found
174
195
 
175
- except requests.exceptions.RequestException as e:
196
+ except curl_cffi.CurlError as e:
197
+ raise exceptions.APIConnectionError(f"Request failed (CurlError): {e}") from e
198
+ except Exception as e: # Catch other potential errors
176
199
  raise exceptions.APIConnectionError(f"Request failed: {e}")
177
200
 
178
201
  def ask(
179
202
  self,
180
203
  prompt: str,
181
204
  stream: bool = False,
205
+ raw: bool = False, # Add raw parameter for consistency
182
206
  optimizer: str = None,
183
207
  conversationally: bool = False,
184
208
  ) -> Union[Dict, Generator]:
@@ -199,11 +223,23 @@ class Cerebras(Provider):
199
223
 
200
224
  try:
201
225
  response = self._make_request(messages, stream)
202
- if stream:
203
- return response
204
226
 
205
- self.last_response = response
206
- return response
227
+ if stream:
228
+ # Wrap the generator to yield dicts or raw strings
229
+ def stream_wrapper():
230
+ full_text = ""
231
+ for chunk in response:
232
+ full_text += chunk
233
+ yield chunk if raw else {"text": chunk}
234
+ # Update history after stream finishes
235
+ self.last_response = {"text": full_text}
236
+ self.conversation.update_chat_history(prompt, full_text)
237
+ return stream_wrapper()
238
+ else:
239
+ # Non-streaming response is already the full text string
240
+ self.last_response = {"text": response}
241
+ self.conversation.update_chat_history(prompt, response)
242
+ return self.last_response if not raw else response # Return dict or raw string
207
243
 
208
244
  except Exception as e:
209
245
  raise exceptions.FailedToGenerateResponseError(f"Error during request: {e}")
@@ -216,14 +252,24 @@ class Cerebras(Provider):
216
252
  conversationally: bool = False,
217
253
  ) -> Union[str, Generator]:
218
254
  """Chat with the model."""
219
- response = self.ask(prompt, stream, optimizer, conversationally)
255
+ # Ask returns a generator for stream=True, dict/str for stream=False
256
+ response_gen_or_dict = self.ask(prompt, stream, raw=False, optimizer=optimizer, conversationally=conversationally)
257
+
220
258
  if stream:
221
- return response
222
- return response
259
+ # Wrap the generator from ask() to get message text
260
+ def stream_wrapper():
261
+ for chunk_dict in response_gen_or_dict:
262
+ yield self.get_message(chunk_dict)
263
+ return stream_wrapper()
264
+ else:
265
+ # Non-streaming response is already a dict
266
+ return self.get_message(response_gen_or_dict)
223
267
 
224
268
  def get_message(self, response: str) -> str:
225
269
  """Retrieves message from response."""
226
- return response
270
+ # Updated to handle dict input from ask()
271
+ assert isinstance(response, dict), "Response should be of dict data-type only for get_message"
272
+ return response.get("text", "")
227
273
 
228
274
 
229
275
  if __name__ == "__main__":
@@ -231,7 +277,7 @@ if __name__ == "__main__":
231
277
 
232
278
  # Example usage
233
279
  cerebras = Cerebras(
234
- cookie_path='cookie.json',
280
+ cookie_path=r'cookies.json',
235
281
  model='llama3.1-8b',
236
282
  system_prompt="You are a helpful AI assistant."
237
283
  )
@@ -1,11 +1,12 @@
1
- import requests
1
+ from curl_cffi import CurlError
2
+ from curl_cffi.requests import Session
2
3
  import json
3
4
  from typing import Any, Dict, Optional, Generator, List, Union
4
5
  import uuid
5
6
 
6
7
  from webscout.AIutel import Optimizers
7
8
  from webscout.AIutel import Conversation
8
- from webscout.AIutel import AwesomePrompts
9
+ from webscout.AIutel import AwesomePrompts, sanitize_stream # Import sanitize_stream
9
10
  from webscout.AIbase import Provider
10
11
  from webscout import exceptions
11
12
  from webscout.litagent import LitAgent
@@ -29,7 +30,7 @@ class ChatGLM(Provider):
29
30
  plus_model: bool = True,
30
31
  ):
31
32
  """Initializes the ChatGLM API client."""
32
- self.session = requests.Session()
33
+ self.session = Session() # Use curl_cffi Session
33
34
  self.is_conversation = is_conversation
34
35
  self.max_tokens_to_sample = max_tokens
35
36
  self.api_endpoint = "https://chatglm.cn/chatglm/mainchat-api/guest/stream"
@@ -55,7 +56,7 @@ class ChatGLM(Provider):
55
56
  if callable(getattr(Optimizers, method)) and not method.startswith("__")
56
57
  )
57
58
  self.session.headers.update(self.headers)
58
- Conversation.intro = (
59
+ Conversation.intro = ( # type: ignore
59
60
  AwesomePrompts().get_act(
60
61
  act, raise_not_found=True, default=None, case_insensitive=True
61
62
  )
@@ -66,7 +67,16 @@ class ChatGLM(Provider):
66
67
  is_conversation, self.max_tokens_to_sample, filepath, update_file
67
68
  )
68
69
  self.conversation.history_offset = history_offset
69
- self.session.proxies = proxies
70
+ self.session.proxies = proxies # Assign proxies directly
71
+
72
+ @staticmethod
73
+ def _chatglm_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
74
+ """Extracts content from ChatGLM stream JSON objects."""
75
+ if isinstance(chunk, dict):
76
+ parts = chunk.get('parts', [])
77
+ if parts and isinstance(parts[0].get('content'), list) and parts[0]['content']:
78
+ return parts[0]['content'][0].get('text')
79
+ return None
70
80
 
71
81
  def ask(
72
82
  self,
@@ -119,45 +129,45 @@ class ChatGLM(Provider):
119
129
  }
120
130
 
121
131
  def for_stream():
132
+ streaming_text = "" # Initialize outside try block
133
+ last_processed_content = "" # Track the last processed content
122
134
  try:
123
- with self.session.post(
124
- self.api_endpoint, json=payload, stream=True, timeout=self.timeout
125
- ) as response:
126
- response.raise_for_status()
127
-
128
- streaming_text = ""
129
- last_processed_content = "" # Track the last processed content
130
- for chunk in response.iter_lines():
131
- if chunk:
132
- decoded_chunk = chunk.decode('utf-8')
133
- if decoded_chunk.startswith('data: '):
134
- try:
135
- json_data = json.loads(decoded_chunk[6:])
136
- parts = json_data.get('parts', [])
137
- if parts:
138
- content = parts[0].get('content', [])
139
- if content:
140
- text = content[0].get('text', '')
141
- new_text = text[len(last_processed_content):]
142
- if new_text: # Check for new content
143
- streaming_text += new_text
144
- last_processed_content = text
145
- yield new_text if raw else dict(text=new_text)
146
- except json.JSONDecodeError:
147
- continue
135
+ response = self.session.post(
136
+ self.api_endpoint, json=payload, stream=True, timeout=self.timeout,
137
+ impersonate="chrome120" # Add impersonate
138
+ )
139
+ response.raise_for_status()
140
+
141
+ # Use sanitize_stream
142
+ processed_stream = sanitize_stream(
143
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
144
+ intro_value="data:",
145
+ to_json=True, # Stream sends JSON
146
+ content_extractor=self._chatglm_extractor, # Use the specific extractor
147
+ yield_raw_on_error=False # Skip non-JSON lines or lines where extractor fails
148
+ )
148
149
 
150
+ for current_full_text in processed_stream:
151
+ # current_full_text is the full text extracted by _chatglm_extractor
152
+ if current_full_text and isinstance(current_full_text, str):
153
+ new_text = current_full_text[len(last_processed_content):]
154
+ if new_text: # Check for new content
155
+ streaming_text += new_text
156
+ last_processed_content = current_full_text # Update tracker
157
+ yield new_text if raw else dict(text=new_text)
158
+
159
+ except CurlError as e:
160
+ raise exceptions.ProviderConnectionError(f"Request failed (CurlError): {e}") from e
161
+ except Exception as e:
162
+ raise exceptions.FailedToGenerateResponseError(f"An unexpected error occurred ({type(e).__name__}): {e}") from e
163
+ finally:
164
+ # Update history after stream finishes or fails
165
+ if streaming_text:
149
166
  self.last_response.update(dict(text=streaming_text))
150
167
  self.conversation.update_chat_history(
151
168
  prompt, self.get_message(self.last_response)
152
169
  )
153
170
 
154
- except requests.exceptions.RequestException as e:
155
- raise exceptions.ProviderConnectionError(f"Request failed: {e}")
156
- except json.JSONDecodeError as e:
157
- raise exceptions.InvalidResponseError(f"Failed to decode JSON: {e}")
158
- except Exception as e:
159
- raise exceptions.FailedToGenerateResponseError(f"An unexpected error occurred: {e}")
160
-
161
171
  def for_non_stream():
162
172
  for _ in for_stream():
163
173
  pass
webscout/Provider/elmo.py CHANGED
@@ -1,13 +1,14 @@
1
1
  from curl_cffi.requests import Session
2
2
  from curl_cffi import CurlError
3
3
  import json
4
- from typing import Union, Any, Dict, Generator
4
+ from typing import Optional, Union, Any, Dict, Generator
5
5
  from webscout import exceptions
6
6
  from webscout.AIutel import Optimizers
7
- from webscout.AIutel import Conversation
7
+ from webscout.AIutel import Conversation, sanitize_stream # Import sanitize_stream
8
8
  from webscout.AIutel import AwesomePrompts
9
9
  from webscout.AIbase import Provider
10
10
  from webscout.litagent import LitAgent
11
+ import re # Import re for the extractor
11
12
 
12
13
 
13
14
  class Elmo(Provider):
@@ -84,6 +85,17 @@ class Elmo(Provider):
84
85
  )
85
86
  self.conversation.history_offset = history_offset
86
87
 
88
+ @staticmethod
89
+ def _elmo_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
90
+ """Extracts content from the Elmo stream format '0:"..."'."""
91
+ if isinstance(chunk, str):
92
+ match = re.search(r'0:"(.*?)"(?=,|$)', chunk) # Look for 0:"...", possibly followed by comma or end of string
93
+ if match:
94
+ # Decode potential unicode escapes like \u00e9 and handle escaped quotes/backslashes
95
+ content = match.group(1).encode().decode('unicode_escape')
96
+ return content.replace('\\\\', '\\').replace('\\"', '"')
97
+ return None
98
+
87
99
  def ask(
88
100
  self,
89
101
  prompt: str,
@@ -144,7 +156,7 @@ class Elmo(Provider):
144
156
  }
145
157
 
146
158
  def for_stream():
147
- full_response = "" # Initialize outside try block
159
+ streaming_text = "" # Initialize outside try block
148
160
  try:
149
161
  # Use curl_cffi session post with impersonate
150
162
  # Note: The API expects 'text/plain' but we send JSON.
@@ -159,40 +171,32 @@ class Elmo(Provider):
159
171
  )
160
172
  response.raise_for_status() # Check for HTTP errors
161
173
 
162
- # Iterate over bytes and decode manually
163
- for line_bytes in response.iter_lines():
164
- if line_bytes:
165
- try:
166
- line = line_bytes.decode('utf-8')
167
- if line.startswith('0:'):
168
- # Extract content after '0:"' and before the closing '"'
169
- match = line.split(':"', 1)
170
- if len(match) > 1:
171
- chunk = match[1]
172
- if chunk.endswith('"'):
173
- chunk = chunk[:-1] # Remove trailing quote
174
-
175
- # Handle potential escape sequences like \\n
176
- formatted_output = chunk.encode().decode('unicode_escape')
174
+ # Use sanitize_stream
175
+ processed_stream = sanitize_stream(
176
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
177
+ intro_value=None, # No simple prefix
178
+ to_json=False, # Content is text after extraction
179
+ content_extractor=self._elmo_extractor, # Use the specific extractor
180
+ yield_raw_on_error=True
181
+ )
177
182
 
178
- if formatted_output: # Ensure content is not None or empty
179
- full_response += formatted_output
180
- resp = dict(text=formatted_output)
181
- # Yield dict or raw string chunk
182
- yield resp if not raw else formatted_output
183
- except (UnicodeDecodeError, IndexError):
184
- continue # Ignore lines that cannot be decoded or parsed
183
+ for content_chunk in processed_stream:
184
+ if content_chunk and isinstance(content_chunk, str):
185
+ streaming_text += content_chunk
186
+ resp = dict(text=content_chunk)
187
+ yield resp if not raw else content_chunk
185
188
 
186
- # Update history after stream finishes
187
- self.last_response = dict(text=full_response)
188
- self.conversation.update_chat_history(
189
- prompt, full_response
190
- )
191
189
  except CurlError as e: # Catch CurlError
192
190
  raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}") from e
193
191
  except Exception as e: # Catch other potential exceptions (like HTTPError)
194
192
  err_text = getattr(e, 'response', None) and getattr(e.response, 'text', '')
195
193
  raise exceptions.FailedToGenerateResponseError(f"Failed to generate response ({type(e).__name__}): {e} - {err_text}") from e
194
+ finally:
195
+ # Update history after stream finishes
196
+ self.last_response = dict(text=streaming_text)
197
+ self.conversation.update_chat_history(
198
+ prompt, streaming_text
199
+ )
196
200
 
197
201
  def for_non_stream():
198
202
  # Aggregate the stream using the updated for_stream logic
@@ -210,7 +214,9 @@ class Elmo(Provider):
210
214
  if not collected_text:
211
215
  raise exceptions.FailedToGenerateResponseError(f"Failed to get non-stream response: {str(e)}") from e
212
216
 
213
- # last_response and history are updated within for_stream
217
+ # Update last_response and history *after* aggregation for non-stream
218
+ self.last_response = {"text": collected_text}
219
+ self.conversation.update_chat_history(prompt, collected_text)
214
220
  # Return the final aggregated response dict or raw string
215
221
  return collected_text if raw else self.last_response
216
222
 
@@ -265,7 +271,7 @@ class Elmo(Provider):
265
271
  str: Message extracted
266
272
  """
267
273
  assert isinstance(response, dict), "Response should be of dict data-type only"
268
- return response["text"]
274
+ return response.get("text", "") # Use .get for safety
269
275
 
270
276
 
271
277
  if __name__ == "__main__":