webscout 8.3.2__py3-none-any.whl → 8.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of webscout might be problematic. Click here for more details.

Files changed (94) hide show
  1. webscout/AIutel.py +146 -37
  2. webscout/Bing_search.py +1 -2
  3. webscout/Provider/AISEARCH/__init__.py +1 -0
  4. webscout/Provider/AISEARCH/stellar_search.py +132 -0
  5. webscout/Provider/ExaChat.py +84 -58
  6. webscout/Provider/HeckAI.py +85 -80
  7. webscout/Provider/Jadve.py +56 -50
  8. webscout/Provider/MiniMax.py +207 -0
  9. webscout/Provider/Nemotron.py +41 -13
  10. webscout/Provider/Netwrck.py +34 -51
  11. webscout/Provider/OPENAI/BLACKBOXAI.py +0 -1
  12. webscout/Provider/OPENAI/MiniMax.py +298 -0
  13. webscout/Provider/OPENAI/README.md +30 -29
  14. webscout/Provider/OPENAI/TogetherAI.py +4 -17
  15. webscout/Provider/OPENAI/__init__.py +3 -1
  16. webscout/Provider/OPENAI/autoproxy.py +752 -17
  17. webscout/Provider/OPENAI/base.py +7 -76
  18. webscout/Provider/OPENAI/deepinfra.py +42 -108
  19. webscout/Provider/OPENAI/flowith.py +179 -166
  20. webscout/Provider/OPENAI/friendli.py +233 -0
  21. webscout/Provider/OPENAI/monochat.py +329 -0
  22. webscout/Provider/OPENAI/pydantic_imports.py +1 -172
  23. webscout/Provider/OPENAI/toolbaz.py +1 -0
  24. webscout/Provider/OPENAI/typegpt.py +1 -1
  25. webscout/Provider/OPENAI/utils.py +19 -42
  26. webscout/Provider/OPENAI/x0gpt.py +14 -2
  27. webscout/Provider/OpenGPT.py +54 -32
  28. webscout/Provider/PI.py +58 -84
  29. webscout/Provider/StandardInput.py +32 -13
  30. webscout/Provider/TTI/README.md +9 -9
  31. webscout/Provider/TTI/__init__.py +2 -1
  32. webscout/Provider/TTI/aiarta.py +92 -78
  33. webscout/Provider/TTI/infip.py +212 -0
  34. webscout/Provider/TTI/monochat.py +220 -0
  35. webscout/Provider/TeachAnything.py +11 -3
  36. webscout/Provider/TextPollinationsAI.py +78 -70
  37. webscout/Provider/TogetherAI.py +32 -48
  38. webscout/Provider/Venice.py +37 -46
  39. webscout/Provider/VercelAI.py +27 -24
  40. webscout/Provider/WiseCat.py +35 -35
  41. webscout/Provider/WrDoChat.py +22 -26
  42. webscout/Provider/WritingMate.py +26 -22
  43. webscout/Provider/__init__.py +2 -2
  44. webscout/Provider/granite.py +48 -57
  45. webscout/Provider/koala.py +51 -39
  46. webscout/Provider/learnfastai.py +49 -64
  47. webscout/Provider/llmchat.py +79 -93
  48. webscout/Provider/llmchatco.py +63 -78
  49. webscout/Provider/multichat.py +51 -40
  50. webscout/Provider/oivscode.py +1 -1
  51. webscout/Provider/scira_chat.py +159 -96
  52. webscout/Provider/scnet.py +13 -13
  53. webscout/Provider/searchchat.py +13 -13
  54. webscout/Provider/sonus.py +12 -11
  55. webscout/Provider/toolbaz.py +25 -8
  56. webscout/Provider/turboseek.py +41 -42
  57. webscout/Provider/typefully.py +27 -12
  58. webscout/Provider/typegpt.py +41 -46
  59. webscout/Provider/uncovr.py +55 -90
  60. webscout/Provider/x0gpt.py +33 -17
  61. webscout/Provider/yep.py +79 -96
  62. webscout/auth/__init__.py +12 -1
  63. webscout/auth/providers.py +27 -5
  64. webscout/auth/routes.py +128 -104
  65. webscout/auth/server.py +367 -312
  66. webscout/client.py +121 -116
  67. webscout/litagent/Readme.md +68 -55
  68. webscout/litagent/agent.py +99 -9
  69. webscout/version.py +1 -1
  70. {webscout-8.3.2.dist-info → webscout-8.3.3.dist-info}/METADATA +102 -90
  71. {webscout-8.3.2.dist-info → webscout-8.3.3.dist-info}/RECORD +75 -87
  72. webscout/Provider/TTI/fastflux.py +0 -233
  73. webscout/Provider/Writecream.py +0 -246
  74. webscout/auth/static/favicon.svg +0 -11
  75. webscout/auth/swagger_ui.py +0 -203
  76. webscout/auth/templates/components/authentication.html +0 -237
  77. webscout/auth/templates/components/base.html +0 -103
  78. webscout/auth/templates/components/endpoints.html +0 -750
  79. webscout/auth/templates/components/examples.html +0 -491
  80. webscout/auth/templates/components/footer.html +0 -75
  81. webscout/auth/templates/components/header.html +0 -27
  82. webscout/auth/templates/components/models.html +0 -286
  83. webscout/auth/templates/components/navigation.html +0 -70
  84. webscout/auth/templates/static/api.js +0 -455
  85. webscout/auth/templates/static/icons.js +0 -168
  86. webscout/auth/templates/static/main.js +0 -784
  87. webscout/auth/templates/static/particles.js +0 -201
  88. webscout/auth/templates/static/styles.css +0 -3353
  89. webscout/auth/templates/static/ui.js +0 -374
  90. webscout/auth/templates/swagger_ui.html +0 -170
  91. {webscout-8.3.2.dist-info → webscout-8.3.3.dist-info}/WHEEL +0 -0
  92. {webscout-8.3.2.dist-info → webscout-8.3.3.dist-info}/entry_points.txt +0 -0
  93. {webscout-8.3.2.dist-info → webscout-8.3.3.dist-info}/licenses/LICENSE.md +0 -0
  94. {webscout-8.3.2.dist-info → webscout-8.3.3.dist-info}/top_level.txt +0 -0
@@ -129,8 +129,8 @@ class LLMChatCo(Provider):
129
129
  optimizer: str = None,
130
130
  conversationally: bool = False,
131
131
  web_search: bool = False,
132
- ) -> Union[Dict[str, Any], Generator[Any, None, None]]:
133
- """Chat with LLMChat.co with streaming capabilities"""
132
+ ) -> Union[Dict[str, Any], Generator[Any, None, None], str]:
133
+ """Chat with LLMChat.co with streaming capabilities and raw output support using sanitize_stream."""
134
134
 
135
135
  conversation_prompt = self.conversation.gen_complete_prompt(prompt)
136
136
  if optimizer:
@@ -143,7 +143,6 @@ class LLMChatCo(Provider):
143
143
  f"Optimizer is not one of {self.__available_optimizers}"
144
144
  )
145
145
 
146
-
147
146
  # Generate a unique ID for this message
148
147
  thread_item_id = ''.join(str(uuid.uuid4()).split('-'))[:20]
149
148
  messages = [
@@ -164,79 +163,59 @@ class LLMChatCo(Provider):
164
163
  }
165
164
 
166
165
  def for_stream():
167
- full_response = "" # Initialize outside try block
166
+ full_response = ""
168
167
  try:
169
- # Use curl_cffi session post with impersonate
170
168
  response = self.session.post(
171
169
  self.api_endpoint,
172
170
  json=payload,
173
- # headers are set on the session
174
171
  stream=True,
175
172
  timeout=self.timeout,
176
- # proxies are set on the session
177
- impersonate="chrome110" # Use a common impersonation profile
173
+ impersonate="chrome110"
178
174
  )
179
- response.raise_for_status() # Check for HTTP errors
175
+ response.raise_for_status()
180
176
 
181
- # Use sanitize_stream
182
- # Note: This won't handle SSE 'event:' lines, only 'data:' lines.
183
- # The original code checked for event == 'answer'. We assume relevant data is JSON after 'data:'.
184
177
  processed_stream = sanitize_stream(
185
- data=response.iter_content(chunk_size=None), # Pass byte iterator
178
+ data=response.iter_content(chunk_size=None),
186
179
  intro_value="data:",
187
- to_json=True, # Stream sends JSON
188
- content_extractor=self._llmchatco_extractor, # Use the specific extractor
189
- yield_raw_on_error=False # Skip non-JSON lines or lines where extractor fails
180
+ to_json=True,
181
+ content_extractor=self._llmchatco_extractor,
182
+ yield_raw_on_error=False,
183
+ raw=raw
190
184
  )
191
185
 
192
186
  last_yielded_text = ""
193
187
  for current_full_text in processed_stream:
194
- # current_full_text is the full text extracted by _llmchatco_extractor
195
188
  if current_full_text and isinstance(current_full_text, str):
196
- # Calculate the new part of the text
197
189
  new_text = current_full_text[len(last_yielded_text):]
198
190
  if new_text:
199
- full_response = current_full_text # Keep track of the latest full text
200
- last_yielded_text = current_full_text # Update tracker
201
- resp = dict(text=new_text)
202
- # Yield dict or raw string chunk
203
- yield resp if not raw else new_text
204
-
205
- # Update history after stream finishes
191
+ full_response = current_full_text
192
+ last_yielded_text = current_full_text
193
+ if raw:
194
+ yield new_text
195
+ else:
196
+ yield dict(text=new_text)
206
197
  self.last_response = dict(text=full_response)
207
198
  self.last_assistant_response = full_response
208
199
  self.conversation.update_chat_history(
209
200
  prompt, full_response
210
201
  )
211
-
212
- except CurlError as e: # Catch CurlError
202
+ except CurlError as e:
213
203
  raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}") from e
214
- except Exception as e: # Catch other potential exceptions (like HTTPError)
204
+ except Exception as e:
215
205
  err_text = getattr(e, 'response', None) and getattr(e.response, 'text', '')
216
206
  raise exceptions.FailedToGenerateResponseError(f"Unexpected error ({type(e).__name__}): {str(e)} - {err_text}") from e
217
-
218
207
  def for_non_stream():
219
- # Aggregate the stream using the updated for_stream logic
220
208
  full_response_text = ""
221
209
  try:
222
- # Ensure raw=False so for_stream yields dicts
223
210
  for chunk_data in for_stream():
224
- if isinstance(chunk_data, dict) and "text" in chunk_data:
211
+ if raw and isinstance(chunk_data, str):
212
+ full_response_text += chunk_data
213
+ elif isinstance(chunk_data, dict) and "text" in chunk_data:
225
214
  full_response_text += chunk_data["text"]
226
- # Handle raw string case if raw=True was passed
227
- elif raw and isinstance(chunk_data, str):
228
- full_response_text += chunk_data
229
-
230
215
  except Exception as e:
231
- # If aggregation fails but some text was received, use it. Otherwise, re-raise.
232
216
  if not full_response_text:
233
217
  raise exceptions.FailedToGenerateResponseError(f"Failed to get non-stream response: {str(e)}") from e
234
-
235
- # last_response and history are updated within for_stream
236
- # Return the final aggregated response dict or raw string
237
218
  return full_response_text if raw else self.last_response
238
-
239
-
240
219
  return for_stream() if stream else for_non_stream()
241
220
 
242
221
  def chat(
@@ -246,31 +225,33 @@ class LLMChatCo(Provider):
246
225
  optimizer: str = None,
247
226
  conversationally: bool = False,
248
227
  web_search: bool = False,
228
+ raw: bool = False
249
229
  ) -> Union[str, Generator[str, None, None]]:
250
- """Generate response with streaming capabilities"""
251
-
230
+ """Generate response with streaming capabilities and raw output support"""
252
231
  def for_stream_chat():
253
- # ask() yields dicts or strings when streaming
254
232
  gen = self.ask(
255
- prompt, stream=True, raw=False, # Ensure ask yields dicts
233
+ prompt, stream=True, raw=raw,
256
234
  optimizer=optimizer, conversationally=conversationally,
257
235
  web_search=web_search
258
236
  )
259
- for response_dict in gen:
260
- yield self.get_message(response_dict) # get_message expects dict
261
-
237
+ for response in gen:
238
+ if raw:
239
+ yield response
240
+ else:
241
+ yield self.get_message(response)
262
242
  def for_non_stream_chat():
263
- # ask() returns dict or str when not streaming
264
243
  response_data = self.ask(
265
244
  prompt,
266
245
  stream=False,
267
- raw=False, # Ensure ask returns dict
246
+ raw=raw,
268
247
  optimizer=optimizer,
269
248
  conversationally=conversationally,
270
249
  web_search=web_search
271
250
  )
272
- return self.get_message(response_data) # get_message expects dict
273
-
251
+ if raw:
252
+ return response_data if isinstance(response_data, str) else self.get_message(response_data)
253
+ else:
254
+ return self.get_message(response_data)
274
255
  return for_stream_chat() if stream else for_non_stream_chat()
275
256
 
276
257
  def get_message(self, response: Dict[str, Any]) -> str:
@@ -279,28 +260,32 @@ class LLMChatCo(Provider):
279
260
  return response["text"]
280
261
 
281
262
  if __name__ == "__main__":
282
- # Ensure curl_cffi is installed
283
- print("-" * 80)
284
- print(f"{'Model':<50} {'Status':<10} {'Response'}")
285
- print("-" * 80)
286
-
287
- # Test all available models
288
- working = 0
289
- total = len(LLMChatCo.AVAILABLE_MODELS)
290
-
291
- for model in LLMChatCo.AVAILABLE_MODELS:
292
- try:
293
- test_ai = LLMChatCo(model=model, timeout=60)
294
- response = test_ai.chat("Say 'Hello' in one word")
295
- response_text = response
296
-
297
- if response_text and len(response_text.strip()) > 0:
298
- status = "✓"
299
- # Truncate response if too long
300
- display_text = response_text.strip()[:50] + "..." if len(response_text.strip()) > 50 else response_text.strip()
301
- else:
302
- status = "✗"
303
- display_text = "Empty or invalid response"
304
- print(f"{model:<50} {status:<10} {display_text}")
305
- except Exception as e:
306
- print(f"{model:<50} {'✗':<10} {str(e)}")
263
+ # # Ensure curl_cffi is installed
264
+ # print("-" * 80)
265
+ # print(f"{'Model':<50} {'Status':<10} {'Response'}")
266
+ # print("-" * 80)
267
+
268
+ # # Test all available models
269
+ # working = 0
270
+ # total = len(LLMChatCo.AVAILABLE_MODELS)
271
+
272
+ # for model in LLMChatCo.AVAILABLE_MODELS:
273
+ # try:
274
+ # test_ai = LLMChatCo(model=model, timeout=60)
275
+ # response = test_ai.chat("Say 'Hello' in one word")
276
+ # response_text = response
277
+
278
+ # if response_text and len(response_text.strip()) > 0:
279
+ # status = "✓"
280
+ # # Truncate response if too long
281
+ # display_text = response_text.strip()[:50] + "..." if len(response_text.strip()) > 50 else response_text.strip()
282
+ # else:
283
+ # status = "✗"
284
+ # display_text = "Empty or invalid response"
285
+ # print(f"{model:<50} {status:<10} {display_text}")
286
+ # except Exception as e:
287
+ # print(f"{model:<50} {'✗':<10} {str(e)}")
288
+ ai = LLMChatCo()
289
+ response = ai.chat("yooo", stream=True, raw=False)
290
+ for chunk in response:
291
+ print(chunk, end="", flush=True)
@@ -2,7 +2,7 @@ from curl_cffi.requests import Session
2
2
  from curl_cffi import CurlError
3
3
  import json
4
4
  import uuid
5
- from typing import Any, Dict, Union
5
+ from typing import Any, Dict, Union, Generator
6
6
  from datetime import datetime
7
7
  from webscout.AIutel import Optimizers, Conversation, AwesomePrompts, sanitize_stream # Import sanitize_stream
8
8
  from webscout.AIbase import Provider
@@ -257,10 +257,9 @@ class MultiChatAI(Provider):
257
257
  raw: bool = False, # Keep raw param for interface consistency
258
258
  optimizer: str = None,
259
259
  conversationally: bool = False,
260
- # Add stream parameter for consistency, though API doesn't stream
261
260
  stream: bool = False
262
- ) -> Dict[str, Any]:
263
- """Sends a prompt to the MultiChatAI API and returns the response."""
261
+ ) -> Union[Dict[str, Any], str, Generator[str, None, None]]:
262
+ """Sends a prompt to the MultiChatAI API and returns the response. Supports raw output and direct text streaming."""
264
263
  conversation_prompt = self.conversation.gen_complete_prompt(prompt)
265
264
  if optimizer:
266
265
  if optimizer in self.__available_optimizers:
@@ -275,26 +274,32 @@ class MultiChatAI(Provider):
275
274
  "customModelId": "",
276
275
  }
277
276
 
278
- # API does not stream, implement non-stream logic directly
279
277
  response = self._make_request(payload)
280
278
  try:
281
- # Use response.text which is already decoded
282
- response_text_raw = response.text # Get raw text
283
-
284
- # Process the text using sanitize_stream (even though it's not streaming)
285
- processed_stream = sanitize_stream(
286
- data=response_text_raw,
287
- intro_value=None, # No prefix
288
- to_json=False # It's plain text
289
- )
290
- # Aggregate the single result
291
- full_response = "".join(list(processed_stream)).strip()
292
-
293
- self.last_response = {"text": full_response} # Store processed text
294
- self.conversation.update_chat_history(prompt, full_response)
295
- # Return dict or raw string based on raw flag
296
- return full_response if raw else self.last_response
297
- except Exception as e: # Catch potential errors during text processing
279
+ response_text_raw = response.text
280
+ if stream:
281
+ chunk_size = 64
282
+ text = response_text_raw
283
+ for i in range(0, len(text), chunk_size):
284
+ chunk = text[i:i+chunk_size]
285
+ if raw:
286
+ yield chunk
287
+ else:
288
+ yield {"text": chunk}
289
+ self.last_response = {"text": text}
290
+ self.conversation.update_chat_history(prompt, text)
291
+ else:
292
+ processed_stream = sanitize_stream(
293
+ data=response_text_raw,
294
+ intro_value=None,
295
+ to_json=False,
296
+ raw=raw
297
+ )
298
+ full_response = "".join(list(processed_stream)).strip()
299
+ self.last_response = {"text": full_response}
300
+ self.conversation.update_chat_history(prompt, full_response)
301
+ return full_response if raw else self.last_response
302
+ except Exception as e:
298
303
  raise exceptions.FailedToGenerateResponseError(f"Failed to process response: {e}") from e
299
304
 
300
305
  def chat(
@@ -302,26 +307,32 @@ class MultiChatAI(Provider):
302
307
  prompt: str,
303
308
  optimizer: str = None,
304
309
  conversationally: bool = False,
305
- # Add stream parameter for consistency
306
- stream: bool = False
307
- ) -> str:
308
- """Generate response."""
309
- # Since ask() now handles both stream=True/False by returning the full response dict/str:
310
- response_data = self.ask(
311
- prompt,
312
- stream=False, # Call ask in non-stream mode internally
313
- raw=False, # Ensure ask returns dict
314
- optimizer=optimizer,
315
- conversationally=conversationally
316
- )
317
- # If stream=True was requested, simulate streaming by yielding the full message at once
310
+ stream: bool = False,
311
+ raw: bool = False
312
+ ) -> Union[str, Generator[str, None, None]]:
313
+ """Generate response. Supports raw output and streaming."""
318
314
  if stream:
319
- def stream_wrapper():
320
- yield self.get_message(response_data)
321
- return stream_wrapper()
315
+ # Streaming mode: yield chunks from ask
316
+ return self.ask(
317
+ prompt,
318
+ raw=raw,
319
+ optimizer=optimizer,
320
+ conversationally=conversationally,
321
+ stream=True
322
+ )
322
323
  else:
323
- # If stream=False, return the full message directly
324
- return self.get_message(response_data)
324
+ # Non-streaming mode: return full message
325
+ response_data = self.ask(
326
+ prompt,
327
+ raw=raw,
328
+ optimizer=optimizer,
329
+ conversationally=conversationally,
330
+ stream=False
331
+ )
332
+ if raw:
333
+ return response_data if isinstance(response_data, str) else self.get_message(response_data)
334
+ else:
335
+ return self.get_message(response_data)
325
336
 
326
337
  def get_message(self, response: Union[Dict[str, Any], str]) -> str:
327
338
  """
@@ -303,7 +303,7 @@ class oivscode(Provider):
303
303
  if __name__ == "__main__":
304
304
  from rich import print
305
305
  chatbot = oivscode()
306
- print(chatbot.fetch_available_models())
306
+ chatbot.fetch_available_models()
307
307
  response = chatbot.chat(input(">>> "), stream=True)
308
308
  for chunk in response:
309
309
  print(chunk, end="", flush=True)
@@ -154,14 +154,20 @@ class SciraAI(Provider):
154
154
  return self.fingerprint
155
155
 
156
156
  @staticmethod
157
- def _scira_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
158
- """Extracts content from the Scira stream format '0:"..."'."""
157
+ def _scira_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[dict]:
158
+ """Extracts g and 0 chunks from the Scira stream format.
159
+ Returns a dict: {"g": [g1, g2, ...], "0": zero} if present.
160
+ """
159
161
  if isinstance(chunk, str):
160
- match = re.search(r'0:"(.*?)"(?=,|$)', chunk) # Look for 0:"...", possibly followed by comma or end of string
161
- if match:
162
- # Decode potential unicode escapes like \u00e9 and handle escaped quotes/backslashes
163
- content = match.group(1).encode().decode('unicode_escape')
164
- return content.replace('\\\\', '\\').replace('\\"', '"')
162
+ g_matches = re.findall(r'g:"(.*?)"', chunk)
163
+ zero_match = re.search(r'0:"(.*?)"(?=,|$)', chunk)
164
+ result = {}
165
+ if g_matches:
166
+ result["g"] = [g.encode().decode('unicode_escape').replace('\\', '\\').replace('\\"', '"') for g in g_matches]
167
+ if zero_match:
168
+ result["0"] = zero_match.group(1).encode().decode('unicode_escape').replace('\\', '\\').replace('\\"', '"')
169
+ if result:
170
+ return result
165
171
  return None
166
172
 
167
173
  def ask(
@@ -169,7 +175,9 @@ class SciraAI(Provider):
169
175
  prompt: str,
170
176
  optimizer: str = None,
171
177
  conversationally: bool = False,
172
- ) -> Dict[str, Any]: # Note: Stream parameter removed as API doesn't seem to support it
178
+ stream: bool = True, # Default to True, always stream
179
+ raw: bool = False, # Added raw parameter
180
+ ) -> Union[Dict[str, Any], Any]:
173
181
  conversation_prompt = self.conversation.gen_complete_prompt(prompt)
174
182
  if optimizer:
175
183
  if optimizer in self.__available_optimizers:
@@ -194,107 +202,162 @@ class SciraAI(Provider):
194
202
  "timezone": "Asia/Calcutta"
195
203
  }
196
204
 
197
- try:
198
- # Use curl_cffi post with impersonate
199
- response = self.session.post(
200
- self.url,
201
- json=payload,
202
- timeout=self.timeout,
203
- impersonate="chrome120" # Add impersonate
204
- )
205
- if response.status_code != 200:
206
- # Try to get response content for better error messages
207
- try: # Use try-except for reading response content
208
- error_content = response.text
209
- except:
210
- error_content = "<could not read response content>"
211
-
212
- if response.status_code in [403, 429]:
213
- print(f"Received status code {response.status_code}, refreshing identity...")
214
- self.refresh_identity()
215
- response = self.session.post(
216
- self.url, json=payload, timeout=self.timeout,
217
- impersonate="chrome120" # Add impersonate to retry
218
- )
219
- if not response.ok:
205
+ def for_stream():
206
+ try:
207
+ response = self.session.post(
208
+ self.url,
209
+ json=payload,
210
+ timeout=self.timeout,
211
+ impersonate="chrome120",
212
+ stream=True
213
+ )
214
+ if response.status_code != 200:
215
+ try:
216
+ error_content = response.text
217
+ except:
218
+ error_content = "<could not read response content>"
219
+
220
+ if response.status_code in [403, 429]:
221
+ print(f"Received status code {response.status_code}, refreshing identity...")
222
+ self.refresh_identity()
223
+ response = self.session.post(
224
+ self.url, json=payload, timeout=self.timeout,
225
+ impersonate="chrome120", stream=True
226
+ )
227
+ if not response.ok:
228
+ raise exceptions.FailedToGenerateResponseError(
229
+ f"Failed to generate response after identity refresh - ({response.status_code}, {response.reason}) - {error_content}"
230
+ )
231
+ print("Identity refreshed successfully.")
232
+ else:
220
233
  raise exceptions.FailedToGenerateResponseError(
221
- f"Failed to generate response after identity refresh - ({response.status_code}, {response.reason}) - {error_content}"
234
+ f"Request failed with status code {response.status_code}. Response: {error_content}"
222
235
  )
223
- print("Identity refreshed successfully.")
224
- else:
225
- raise exceptions.FailedToGenerateResponseError(
226
- f"Request failed with status code {response.status_code}. Response: {error_content}"
227
- )
228
-
229
- response_text_raw = response.text # Get raw response text
230
-
231
- # Process the text using sanitize_stream line by line
232
- processed_stream = sanitize_stream(
233
- data=response_text_raw.splitlines(), # Split into lines
234
- intro_value=None, # No simple prefix
235
- to_json=False, # Content is not JSON
236
- content_extractor=self._scira_extractor # Use the specific extractor
237
- )
238
236
 
239
- # Aggregate the results from the generator
237
+ processed_stream = sanitize_stream(
238
+ data=response.iter_content(chunk_size=None),
239
+ intro_value=None,
240
+ to_json=False,
241
+ content_extractor=self._scira_extractor,
242
+ raw=raw
243
+ )
244
+
245
+ streaming_response = ""
246
+ in_think = False
247
+ for content in processed_stream:
248
+ if content is None:
249
+ continue
250
+ if isinstance(content, dict):
251
+ # Handle g chunks
252
+ g_chunks = content.get("g", [])
253
+ zero_chunk = content.get("0")
254
+ if g_chunks:
255
+ if not in_think:
256
+ if raw:
257
+ yield "<think>\n\n"
258
+ else:
259
+ yield "<think>\n\n"
260
+ in_think = True
261
+ for g in g_chunks:
262
+ if raw:
263
+ yield g
264
+ else:
265
+ yield dict(text=g)
266
+ if zero_chunk is not None:
267
+ if in_think:
268
+ if raw:
269
+ yield "</think>\n\n"
270
+ else:
271
+ yield "</think>\n\n"
272
+ in_think = False
273
+ if raw:
274
+ yield zero_chunk
275
+ else:
276
+ streaming_response += zero_chunk
277
+ yield dict(text=zero_chunk)
278
+ else:
279
+ # fallback for old string/list logic
280
+ if raw:
281
+ yield content
282
+ else:
283
+ if content and isinstance(content, str):
284
+ streaming_response += content
285
+ yield dict(text=content)
286
+ if not raw:
287
+ self.last_response = {"text": streaming_response}
288
+ self.conversation.update_chat_history(prompt, streaming_response)
289
+ except CurlError as e:
290
+ raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}") from e
291
+ except Exception as e:
292
+ raise exceptions.FailedToGenerateResponseError(f"Request failed: {e}")
293
+
294
+ def for_non_stream():
295
+ # Always use streaming logic, but aggregate the result
240
296
  full_response = ""
241
- for content in processed_stream:
242
- if content and isinstance(content, str):
243
- full_response += content
244
-
245
- self.last_response = {"text": full_response}
246
- self.conversation.update_chat_history(prompt, full_response)
247
- return {"text": full_response}
248
- except CurlError as e: # Catch CurlError
249
- raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}") from e
250
- except Exception as e:
251
- raise exceptions.FailedToGenerateResponseError(f"Request failed: {e}")
297
+ for chunk in for_stream():
298
+ if raw:
299
+ if isinstance(chunk, str):
300
+ full_response += chunk
301
+ else:
302
+ if isinstance(chunk, dict) and "text" in chunk:
303
+ full_response += chunk["text"]
304
+ if not raw:
305
+ self.last_response = {"text": full_response}
306
+ self.conversation.update_chat_history(prompt, full_response)
307
+ return {"text": full_response}
308
+ else:
309
+ return full_response
310
+
311
+ return for_stream() if stream else for_non_stream()
252
312
 
253
313
  def chat(
254
314
  self,
255
315
  prompt: str,
256
316
  optimizer: str = None,
257
317
  conversationally: bool = False,
258
- ) -> str:
259
- return self.get_message(
260
- self.ask(
261
- prompt, optimizer=optimizer, conversationally=conversationally
318
+ stream: bool = True, # Default to True, always stream
319
+ raw: bool = False, # Added raw parameter
320
+ ) -> Any:
321
+ def for_stream():
322
+ for response in self.ask(
323
+ prompt, optimizer=optimizer, conversationally=conversationally, stream=True, raw=raw
324
+ ):
325
+ if raw:
326
+ yield response
327
+ else:
328
+ if isinstance(response, dict):
329
+ yield self.get_message(response)
330
+ else:
331
+ # For <think> and </think> tags (strings), yield as is
332
+ yield response
333
+ def for_non_stream():
334
+ result = self.ask(
335
+ prompt, optimizer=optimizer, conversationally=conversationally, stream=False, raw=raw
262
336
  )
263
- )
337
+ if raw:
338
+ return result
339
+ else:
340
+ if isinstance(result, dict):
341
+ return self.get_message(result)
342
+ else:
343
+ return result
344
+ return for_stream() if stream else for_non_stream()
264
345
 
265
346
  def get_message(self, response: dict) -> str:
266
- assert isinstance(response, dict), "Response should be of dict data-type only"
267
- # Extractor handles formatting
268
- return response.get("text", "").replace('\\n', '\n').replace('\\n\\n', '\n\n')
269
-
270
- if __name__ == "__main__":
271
- print("-" * 100)
272
- print(f"{'Model':<50} {'Status':<10} {'Response'}")
273
- print("-" * 100)
274
-
275
- test_prompt = "Say 'Hello' in one word"
347
+ """
348
+ Retrieves message only from response
276
349
 
277
- # Test each model
278
- for model in SciraAI.AVAILABLE_MODELS:
279
- print(f"\rTesting {model}...", end="")
350
+ Args:
351
+ response (dict): Response generated by `self.ask`
280
352
 
281
- try:
282
- test_ai = SciraAI(model=model, timeout=120) # Increased timeout
283
- response = test_ai.chat(test_prompt)
353
+ Returns:
354
+ str: Message extracted
355
+ """
356
+ assert isinstance(response, dict), "Response should be of dict data-type only"
357
+ return response.get("text", "")
284
358
 
285
- if response and len(response.strip()) > 0:
286
- status = "✓"
287
- # Clean and truncate response
288
- clean_text = response.strip().encode('utf-8', errors='ignore').decode('utf-8')
289
- display_text = clean_text[:50] + "..." if len(clean_text) > 50 else clean_text
290
- else:
291
- status = "✗"
292
- display_text = "Empty or invalid response"
293
-
294
- print(f"\r{model:<50} {status:<10} {display_text}")
295
- except Exception as e:
296
- error_msg = str(e)
297
- # Truncate very long error messages
298
- if len(error_msg) > 100:
299
- error_msg = error_msg[:97] + "..."
300
- print(f"\r{model:<50} {'✗':<10} Error: {error_msg}")
359
+ if __name__ == "__main__":
360
+ ai = SciraAI()
361
+ resp = ai.chat("What is the capital of France?", stream=True, raw=False)
362
+ for chunk in resp:
363
+ print(chunk, end="", flush=True)