webscout 8.2.3__py3-none-any.whl → 8.2.5__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (122) hide show
  1. webscout/AIutel.py +226 -14
  2. webscout/Bard.py +579 -206
  3. webscout/DWEBS.py +78 -35
  4. webscout/Extra/gguf.py +2 -0
  5. webscout/Extra/tempmail/base.py +1 -1
  6. webscout/Provider/AISEARCH/hika_search.py +4 -0
  7. webscout/Provider/AISEARCH/scira_search.py +2 -5
  8. webscout/Provider/Aitopia.py +75 -51
  9. webscout/Provider/AllenAI.py +181 -147
  10. webscout/Provider/ChatGPTClone.py +97 -86
  11. webscout/Provider/ChatSandbox.py +342 -0
  12. webscout/Provider/Cloudflare.py +79 -32
  13. webscout/Provider/Deepinfra.py +135 -94
  14. webscout/Provider/ElectronHub.py +103 -39
  15. webscout/Provider/ExaChat.py +36 -20
  16. webscout/Provider/GPTWeb.py +103 -47
  17. webscout/Provider/GithubChat.py +52 -49
  18. webscout/Provider/GizAI.py +283 -0
  19. webscout/Provider/Glider.py +39 -28
  20. webscout/Provider/Groq.py +222 -91
  21. webscout/Provider/HeckAI.py +93 -69
  22. webscout/Provider/HuggingFaceChat.py +113 -106
  23. webscout/Provider/Hunyuan.py +94 -83
  24. webscout/Provider/Jadve.py +104 -79
  25. webscout/Provider/LambdaChat.py +142 -123
  26. webscout/Provider/Llama3.py +94 -39
  27. webscout/Provider/MCPCore.py +315 -0
  28. webscout/Provider/Marcus.py +95 -37
  29. webscout/Provider/Netwrck.py +94 -52
  30. webscout/Provider/OPENAI/__init__.py +4 -1
  31. webscout/Provider/OPENAI/ai4chat.py +286 -0
  32. webscout/Provider/OPENAI/chatgptclone.py +35 -14
  33. webscout/Provider/OPENAI/deepinfra.py +37 -0
  34. webscout/Provider/OPENAI/exachat.py +4 -0
  35. webscout/Provider/OPENAI/groq.py +354 -0
  36. webscout/Provider/OPENAI/heckai.py +6 -2
  37. webscout/Provider/OPENAI/mcpcore.py +376 -0
  38. webscout/Provider/OPENAI/multichat.py +368 -0
  39. webscout/Provider/OPENAI/netwrck.py +3 -1
  40. webscout/Provider/OPENAI/scirachat.py +2 -4
  41. webscout/Provider/OPENAI/textpollinations.py +20 -22
  42. webscout/Provider/OPENAI/toolbaz.py +1 -0
  43. webscout/Provider/OpenGPT.py +48 -38
  44. webscout/Provider/PI.py +178 -93
  45. webscout/Provider/PizzaGPT.py +66 -36
  46. webscout/Provider/StandardInput.py +42 -30
  47. webscout/Provider/TeachAnything.py +95 -52
  48. webscout/Provider/TextPollinationsAI.py +138 -78
  49. webscout/Provider/TwoAI.py +162 -81
  50. webscout/Provider/TypliAI.py +305 -0
  51. webscout/Provider/Venice.py +97 -58
  52. webscout/Provider/VercelAI.py +33 -14
  53. webscout/Provider/WiseCat.py +65 -28
  54. webscout/Provider/Writecream.py +37 -11
  55. webscout/Provider/WritingMate.py +135 -63
  56. webscout/Provider/__init__.py +9 -27
  57. webscout/Provider/ai4chat.py +6 -7
  58. webscout/Provider/asksteve.py +53 -44
  59. webscout/Provider/cerebras.py +77 -31
  60. webscout/Provider/chatglm.py +47 -37
  61. webscout/Provider/copilot.py +0 -3
  62. webscout/Provider/elmo.py +109 -60
  63. webscout/Provider/granite.py +102 -54
  64. webscout/Provider/hermes.py +95 -48
  65. webscout/Provider/koala.py +1 -1
  66. webscout/Provider/learnfastai.py +113 -54
  67. webscout/Provider/llama3mitril.py +86 -51
  68. webscout/Provider/llmchat.py +88 -46
  69. webscout/Provider/llmchatco.py +110 -115
  70. webscout/Provider/meta.py +41 -37
  71. webscout/Provider/multichat.py +67 -28
  72. webscout/Provider/scira_chat.py +49 -30
  73. webscout/Provider/scnet.py +106 -53
  74. webscout/Provider/searchchat.py +87 -88
  75. webscout/Provider/sonus.py +113 -63
  76. webscout/Provider/toolbaz.py +115 -82
  77. webscout/Provider/turboseek.py +90 -43
  78. webscout/Provider/tutorai.py +82 -64
  79. webscout/Provider/typefully.py +85 -35
  80. webscout/Provider/typegpt.py +118 -61
  81. webscout/Provider/uncovr.py +132 -76
  82. webscout/Provider/x0gpt.py +69 -26
  83. webscout/Provider/yep.py +79 -66
  84. webscout/cli.py +256 -0
  85. webscout/conversation.py +34 -22
  86. webscout/exceptions.py +23 -0
  87. webscout/prompt_manager.py +56 -42
  88. webscout/version.py +1 -1
  89. webscout/webscout_search.py +65 -47
  90. webscout/webscout_search_async.py +81 -126
  91. webscout/yep_search.py +93 -43
  92. {webscout-8.2.3.dist-info → webscout-8.2.5.dist-info}/METADATA +183 -50
  93. {webscout-8.2.3.dist-info → webscout-8.2.5.dist-info}/RECORD +97 -113
  94. {webscout-8.2.3.dist-info → webscout-8.2.5.dist-info}/WHEEL +1 -1
  95. webscout-8.2.5.dist-info/entry_points.txt +3 -0
  96. {webscout-8.2.3.dist-info → webscout-8.2.5.dist-info}/top_level.txt +0 -1
  97. inferno/__init__.py +0 -6
  98. inferno/__main__.py +0 -9
  99. inferno/cli.py +0 -6
  100. webscout/Local/__init__.py +0 -12
  101. webscout/Local/__main__.py +0 -9
  102. webscout/Local/api.py +0 -576
  103. webscout/Local/cli.py +0 -516
  104. webscout/Local/config.py +0 -75
  105. webscout/Local/llm.py +0 -287
  106. webscout/Local/model_manager.py +0 -253
  107. webscout/Local/server.py +0 -721
  108. webscout/Local/utils.py +0 -93
  109. webscout/Provider/C4ai.py +0 -432
  110. webscout/Provider/ChatGPTES.py +0 -237
  111. webscout/Provider/Chatify.py +0 -175
  112. webscout/Provider/DeepSeek.py +0 -196
  113. webscout/Provider/Llama.py +0 -200
  114. webscout/Provider/Phind.py +0 -535
  115. webscout/Provider/WebSim.py +0 -228
  116. webscout/Provider/askmyai.py +0 -158
  117. webscout/Provider/gaurish.py +0 -244
  118. webscout/Provider/labyrinth.py +0 -340
  119. webscout/Provider/lepton.py +0 -194
  120. webscout/Provider/llamatutor.py +0 -192
  121. webscout-8.2.3.dist-info/entry_points.txt +0 -5
  122. {webscout-8.2.3.dist-info → webscout-8.2.5.dist-info/licenses}/LICENSE.md +0 -0
@@ -1,13 +1,10 @@
1
- from os import system
2
- import requests
3
- import json
1
+ from curl_cffi.requests import Session
4
2
  import uuid
5
3
  import re
6
- from datetime import datetime
7
- from typing import Any, Dict, Optional, Union, Generator
4
+ from typing import Any, Dict, Optional, Union
8
5
  from webscout.AIutel import Optimizers
9
6
  from webscout.AIutel import Conversation
10
- from webscout.AIutel import AwesomePrompts
7
+ from webscout.AIutel import AwesomePrompts, sanitize_stream # Import sanitize_stream
11
8
  from webscout.AIbase import Provider
12
9
  from webscout import exceptions
13
10
  from webscout.litagent import LitAgent
@@ -98,9 +95,9 @@ class StandardInputAI(Provider):
98
95
  "ph_phc_f3wUUyCfmKlKtkc2pfT7OsdcW2mBEVGN2A87yEYbG3c_posthog": '''%7B%22distinct_id%22%3A%220195c7cc-ac8f-79ff-b901-e14a78fc2a67%22%2C%22%24sesid%22%3A%5B1744688627860%2C%220196377f-9f12-77e6-a9ea-0e9669423803%22%2C1744687832850%5D%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fstandard-input.com%2F%22%7D%7D'''
99
96
  }
100
97
 
101
- self.session = requests.Session()
98
+ self.session = Session() # Use curl_cffi Session
102
99
  self.session.headers.update(self.headers)
103
- self.session.proxies.update(proxies)
100
+ self.session.proxies = proxies # Assign proxies directly
104
101
 
105
102
  self.is_conversation = is_conversation
106
103
  self.max_tokens_to_sample = max_tokens
@@ -153,6 +150,17 @@ class StandardInputAI(Provider):
153
150
 
154
151
  return self.fingerprint
155
152
 
153
+ @staticmethod
154
+ def _standardinput_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
155
+ """Extracts content from the StandardInput 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
+
156
164
  def ask(
157
165
  self,
158
166
  prompt: str,
@@ -183,45 +191,48 @@ class StandardInputAI(Provider):
183
191
  }
184
192
 
185
193
  try:
186
- response = self.session.post(self.url, cookies=self.cookies, json=payload, stream=True, timeout=self.timeout)
194
+ # Use curl_cffi post with impersonate
195
+ response = self.session.post(
196
+ self.url,
197
+ cookies=self.cookies,
198
+ json=payload,
199
+ stream=True,
200
+ timeout=self.timeout,
201
+ impersonate="chrome120" # Add impersonate
202
+ )
203
+
187
204
  if response.status_code != 200:
188
- # Try to get response content for better error messages
189
205
  try:
190
206
  error_content = response.text
191
207
  except:
192
208
  error_content = "<could not read response content>"
193
209
 
194
210
  if response.status_code in [403, 429]:
195
- print(f"Received status code {response.status_code}, refreshing identity...")
196
211
  self.refresh_identity()
197
- response = self.session.post(self.url, cookies=self.cookies, json=payload, stream=True, timeout=self.timeout)
212
+ response = self.session.post(
213
+ self.url, cookies=self.cookies, json=payload, stream=True,
214
+ timeout=self.timeout, impersonate="chrome120"
215
+ )
198
216
  if not response.ok:
199
217
  raise exceptions.FailedToGenerateResponseError(
200
218
  f"Failed to generate response after identity refresh - ({response.status_code}, {response.reason}) - {error_content}"
201
219
  )
202
- print("Identity refreshed successfully.")
203
220
  else:
204
221
  raise exceptions.FailedToGenerateResponseError(
205
222
  f"Request failed with status code {response.status_code}. Response: {error_content}"
206
223
  )
207
224
 
208
225
  full_response = ""
209
- debug_lines = []
210
-
211
- # Process the streaming response
212
- for i, line in enumerate(response.iter_lines(decode_unicode=True)):
213
- if line:
214
- try:
215
- line_str = line
216
- debug_lines.append(line_str)
217
-
218
- # Extract content from the response
219
- match = re.search(r'0:"(.*?)"', line_str)
220
- if match:
221
- content = match.group(1)
222
- full_response += content
223
- continue
224
- except: pass
226
+ # Use sanitize_stream
227
+ processed_stream = sanitize_stream(
228
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
229
+ intro_value=None, # No simple prefix
230
+ to_json=False, # Content is not JSON
231
+ content_extractor=self._standardinput_extractor # Use the specific extractor
232
+ )
233
+ for content_chunk in processed_stream:
234
+ if content_chunk and isinstance(content_chunk, str):
235
+ full_response += content_chunk
225
236
 
226
237
  self.last_response = {"text": full_response}
227
238
  self.conversation.update_chat_history(prompt, full_response)
@@ -243,7 +254,8 @@ class StandardInputAI(Provider):
243
254
 
244
255
  def get_message(self, response: dict) -> str:
245
256
  assert isinstance(response, dict), "Response should be of dict data-type only"
246
- return response["text"].replace('\\n', '\n').replace('\\n\\n', '\n\n')
257
+ # Extractor handles formatting
258
+ return response.get("text", "").replace('\\n', '\n').replace('\\n\\n', '\n\n')
247
259
 
248
260
  if __name__ == "__main__":
249
261
  print("-" * 100)
@@ -1,20 +1,25 @@
1
- import requests
2
- from requests.exceptions import RequestException
1
+ from curl_cffi.requests import Session # Import Session
2
+ from curl_cffi import CurlError # Import CurlError
3
3
  from typing import Union, Any, Dict
4
- from webscout.AIutel import Conversation, Optimizers
5
- from webscout.litagent import LitAgent
4
+ from webscout.AIbase import Provider # Import Provider base class
5
+ from webscout import exceptions # Import custom exceptions
6
+ from webscout.conversation import Conversation
7
+ from webscout.AIutel import Optimizers, sanitize_stream # Import sanitize_stream
6
8
  from webscout.prompt_manager import AwesomePrompts
9
+ from webscout.litagent import LitAgent
7
10
 
8
- class TeachAnything:
11
+ # Inherit from Provider
12
+ class TeachAnything(Provider):
9
13
  """
10
14
  A class to interact with the Teach-Anything API.
11
15
  """
12
-
16
+ # Add AVAILABLE_MODELS if applicable, otherwise remove model param
17
+ # AVAILABLE_MODELS = ["default"] # Example
13
18
 
14
19
  def __init__(
15
20
  self,
16
21
  is_conversation: bool = True,
17
- max_tokens: int = 600,
22
+ max_tokens: int = 600, # Note: max_tokens is not used by this API
18
23
  timeout: int = 30,
19
24
  intro: str = None,
20
25
  filepath: str = None,
@@ -22,6 +27,7 @@ class TeachAnything:
22
27
  proxies: dict = {},
23
28
  history_offset: int = 10250,
24
29
  act: str = None,
30
+ # model: str = "default" # Remove if not used
25
31
  ) -> None:
26
32
  """
27
33
  Initializes the Teach-Anything API with given parameters.
@@ -40,7 +46,8 @@ class TeachAnything:
40
46
  """
41
47
 
42
48
 
43
- self.session = requests.Session()
49
+ # Initialize curl_cffi Session
50
+ self.session = Session()
44
51
  self.is_conversation = is_conversation
45
52
  self.max_tokens_to_sample = max_tokens
46
53
  self.api_endpoint = "https://www.teach-anything.com/api/generate"
@@ -48,8 +55,6 @@ class TeachAnything:
48
55
  self.last_response = {}
49
56
  self.headers = {
50
57
  "authority": "www.teach-anything.com",
51
- "path": "/api/generate",
52
- "scheme": "https",
53
58
  "accept": "*/*",
54
59
  "accept-encoding": "gzip, deflate, br, zstd",
55
60
  "accept-language": "en-US,en;q=0.9,en-IN;q=0.8",
@@ -57,13 +62,16 @@ class TeachAnything:
57
62
  "origin": "https://www.teach-anything.com",
58
63
  "referer": "https://www.teach-anything.com/",
59
64
  "user-agent": LitAgent().random(),
65
+ # Add sec-ch-ua headers if needed for impersonation consistency
60
66
  }
61
67
  self.__available_optimizers = (
62
68
  method
63
69
  for method in dir(Optimizers)
64
70
  if callable(getattr(Optimizers, method)) and not method.startswith("__")
65
71
  )
72
+ # Update curl_cffi session headers and proxies
66
73
  self.session.headers.update(self.headers)
74
+ self.session.proxies = proxies # Assign proxies directly
67
75
  Conversation.intro = (
68
76
  AwesomePrompts().get_act(
69
77
  act, raise_not_found=True, default=None, case_insensitive=True
@@ -75,12 +83,12 @@ class TeachAnything:
75
83
  is_conversation, self.max_tokens_to_sample, filepath, update_file
76
84
  )
77
85
  self.conversation.history_offset = history_offset
78
- self.session.proxies = proxies
86
+
79
87
  def ask(
80
88
  self,
81
89
  prompt: str,
82
- stream: bool = False,
83
- raw: bool = False,
90
+ stream: bool = False, # Keep stream param for interface, but API doesn't stream
91
+ raw: bool = False, # Keep raw param for interface
84
92
  optimizer: str = None,
85
93
  conversationally: bool = False,
86
94
  ) -> dict:
@@ -110,31 +118,49 @@ class TeachAnything:
110
118
  payload = {
111
119
  "prompt": conversation_prompt
112
120
  }
113
- def for_stream():
114
- response = self.session.post(self.api_endpoint, headers=self.headers, json=payload, timeout=self.timeout)
115
- if not response.ok:
116
- raise RequestException(
117
- f"Failed to generate response - ({response.status_code}, {response.reason}) - {response.text}"
118
- )
119
121
 
120
- resp = response.text
121
- self.last_response.update(dict(text=resp))
122
- self.conversation.update_chat_history(
123
- prompt, self.get_message(self.last_response)
122
+ # API does not stream, so implement non-stream logic directly
123
+ try:
124
+ # Use curl_cffi session post with impersonate
125
+ response = self.session.post(
126
+ self.api_endpoint,
127
+ # headers are set on the session
128
+ json=payload,
129
+ timeout=self.timeout,
130
+ impersonate="chrome110" # Use a common impersonation profile
131
+ )
132
+ response.raise_for_status() # Check for HTTP errors
133
+
134
+ resp_text_raw = response.text # Get raw response text
135
+
136
+ # Process the text using sanitize_stream (even though it's not streaming)
137
+ # This keeps the pattern consistent, though it won't do much here
138
+ processed_stream = sanitize_stream(
139
+ data=resp_text_raw,
140
+ intro_value=None, # No prefix
141
+ to_json=False # It's plain text
124
142
  )
125
- return self.last_response
126
-
127
- def for_non_stream():
128
- for _ in for_stream():
129
- pass
130
- return self.last_response
131
143
 
132
- return for_stream() if stream else for_non_stream()
144
+ # Extract the single result from the generator
145
+ resp_text = "".join(list(processed_stream)) # Aggregate potential chunks (should be one)
146
+
147
+ self.last_response = {"text": resp_text}
148
+ self.conversation.update_chat_history(prompt, resp_text)
149
+
150
+ # Return dict or raw string based on raw flag
151
+ return resp_text if raw else self.last_response
152
+
153
+ except CurlError as e: # Catch CurlError
154
+ raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}") from e
155
+ except Exception as e: # Catch other potential exceptions (like HTTPError)
156
+ err_text = getattr(e, 'response', None) and getattr(e.response, 'text', '')
157
+ raise exceptions.FailedToGenerateResponseError(f"An unexpected error occurred ({type(e).__name__}): {e} - {err_text}") from e
158
+
133
159
 
134
160
  def chat(
135
161
  self,
136
162
  prompt: str,
137
- stream: bool = False,
163
+ stream: bool = False, # Keep stream param for interface consistency
138
164
  optimizer: str = None,
139
165
  conversationally: bool = False,
140
166
  ) -> str:
@@ -148,23 +174,22 @@ class TeachAnything:
148
174
  str: Response generated
149
175
  """
150
176
 
151
- def for_stream():
152
- for response in self.ask(
153
- prompt, True, optimizer=optimizer, conversationally=conversationally
154
- ):
155
- yield self.get_message(response)
156
-
157
- def for_non_stream():
158
- return self.get_message(
159
- self.ask(
160
- prompt,
161
- False,
162
- optimizer=optimizer,
163
- conversationally=conversationally,
164
- )
165
- )
166
-
167
- return for_stream() if stream else for_non_stream()
177
+ # Since ask() now handles both stream=True/False by returning the full response dict/str:
178
+ response_data = self.ask(
179
+ prompt,
180
+ stream=False, # Call ask in non-stream mode internally
181
+ raw=False, # Ensure ask returns dict
182
+ optimizer=optimizer,
183
+ conversationally=conversationally
184
+ )
185
+ # If stream=True was requested, simulate streaming by yielding the full message at once
186
+ if stream:
187
+ def stream_wrapper():
188
+ yield self.get_message(response_data)
189
+ return stream_wrapper()
190
+ else:
191
+ # If stream=False, return the full message directly
192
+ return self.get_message(response_data)
168
193
 
169
194
  def get_message(self, response: dict) -> str:
170
195
  """Retrieves message only from response
@@ -180,8 +205,26 @@ class TeachAnything:
180
205
 
181
206
 
182
207
  if __name__ == '__main__':
208
+ # Ensure curl_cffi is installed
183
209
  from rich import print
184
- ai = TeachAnything()
185
- response = ai.chat("hi")
186
- for chunk in response:
187
- print(chunk, end="", flush=True)
210
+ try: # Add try-except block for testing
211
+ ai = TeachAnything(timeout=60)
212
+ print("[bold blue]Testing Chat (Non-Stream Simulation):[/bold blue]")
213
+ # Test non-stream first as API doesn't truly stream
214
+ response_non_stream = ai.chat("hi", stream=False)
215
+ print(response_non_stream)
216
+ print("[bold green]Non-Stream Test Complete.[/bold green]\n")
217
+
218
+ # Test stream interface (will yield the full response at once)
219
+ print("[bold blue]Testing Chat (Stream Simulation):[/bold blue]")
220
+ response_stream = ai.chat("hello again", stream=True)
221
+ full_stream_response = ""
222
+ for chunk in response_stream:
223
+ print(chunk, end="", flush=True)
224
+ full_stream_response += chunk
225
+ print("\n[bold green]Stream Test Complete.[/bold green]")
226
+
227
+ except exceptions.FailedToGenerateResponseError as e:
228
+ print(f"\n[bold red]API Error:[/bold red] {e}")
229
+ except Exception as e:
230
+ print(f"\n[bold red]An unexpected error occurred:[/bold red] {e}")
@@ -1,7 +1,10 @@
1
- import requests
1
+ from curl_cffi.requests import Session
2
+ from curl_cffi import CurlError
2
3
  import json
3
4
  from typing import Union, Any, Dict, Generator, Optional, List
4
- from webscout.AIutel import Optimizers, Conversation, AwesomePrompts
5
+
6
+ import requests
7
+ from webscout.AIutel import Optimizers, Conversation, AwesomePrompts, sanitize_stream # Import sanitize_stream
5
8
  from webscout.AIbase import Provider
6
9
  from webscout import exceptions
7
10
  from webscout.litagent import LitAgent as Lit
@@ -12,34 +15,32 @@ class TextPollinationsAI(Provider):
12
15
  """
13
16
 
14
17
  AVAILABLE_MODELS = [
15
- "openai", # OpenAI GPT-4.1-nano (Azure) - vision capable
16
- "openai-large", # OpenAI GPT-4.1 mini (Azure) - vision capable
17
- "openai-reasoning", # OpenAI o4-mini (Azure) - vision capable, reasoning
18
- "qwen-coder", # Qwen 2.5 Coder 32B (Scaleway)
19
- "llama", # Llama 3.3 70B (Cloudflare)
20
- "llamascout", # Llama 4 Scout 17B (Cloudflare)
21
- "mistral", # Mistral Small 3 (Scaleway) - vision capable
22
- "unity", # Unity Mistral Large (Scaleway) - vision capable, uncensored
23
- "midijourney", # Midijourney (Azure)
24
- "rtist", # Rtist (Azure)
25
- "searchgpt", # SearchGPT (Azure) - vision capable
26
- "evil", # Evil (Scaleway) - vision capable, uncensored
27
- "deepseek-reasoning", # DeepSeek-R1 Distill Qwen 32B (Cloudflare) - reasoning
28
- "deepseek-reasoning-large", # DeepSeek R1 - Llama 70B (Scaleway) - reasoning
29
- "phi", # Phi-4 Instruct (Cloudflare) - vision and audio capable
30
- "llama-vision", # Llama 3.2 11B Vision (Cloudflare) - vision capable
31
- "gemini", # gemini-2.5-flash-preview-04-17 (Azure) - vision and audio capable
32
- "hormoz", # Hormoz 8b (Modal)
33
- "hypnosis-tracy", # Hypnosis Tracy 7B (Azure) - audio capable
34
- "deepseek", # DeepSeek-V3 (DeepSeek)
35
- "sur", # Sur AI Assistant (Mistral) (Scaleway) - vision capable
36
- "openai-audio", # OpenAI GPT-4o-audio-preview (Azure) - vision and audio capable
18
+ "openai",
19
+ "openai-large",
20
+ "qwen-coder",
21
+ "llama",
22
+ "llamascout",
23
+ "mistral",
24
+ "unity",
25
+ "midijourney",
26
+ "rtist",
27
+ "searchgpt",
28
+ "evil",
29
+ "deepseek-reasoning",
30
+ "deepseek-reasoning-large",
31
+ "phi",
32
+ "llama-vision",
33
+ "hormoz",
34
+ "hypnosis-tracy",
35
+ "deepseek",
36
+ "sur",
37
+ "openai-audio",
37
38
  ]
39
+ _models_url = "https://text.pollinations.ai/models"
38
40
 
39
- def __init__(
40
- self,
41
+ def __init__(self,
41
42
  is_conversation: bool = True,
42
- max_tokens: int = 8096,
43
+ max_tokens: int = 8096, # Note: max_tokens is not directly used by this API endpoint
43
44
  timeout: int = 30,
44
45
  intro: str = None,
45
46
  filepath: str = None,
@@ -51,10 +52,7 @@ class TextPollinationsAI(Provider):
51
52
  system_prompt: str = "You are a helpful AI assistant.",
52
53
  ):
53
54
  """Initializes the TextPollinationsAI API client."""
54
- if model not in self.AVAILABLE_MODELS:
55
- raise ValueError(f"Invalid model: {model}. Choose from: {self.AVAILABLE_MODELS}")
56
-
57
- self.session = requests.Session()
55
+ self.session = Session()
58
56
  self.is_conversation = is_conversation
59
57
  self.max_tokens_to_sample = max_tokens
60
58
  self.api_endpoint = "https://text.pollinations.ai/openai"
@@ -64,15 +62,21 @@ class TextPollinationsAI(Provider):
64
62
  self.model = model
65
63
  self.system_prompt = system_prompt
66
64
 
65
+ # Validate against the hardcoded list
66
+ if model not in self.AVAILABLE_MODELS:
67
+ raise ValueError(f"Invalid model: {model}. Choose from: {self.AVAILABLE_MODELS}")
68
+
67
69
  self.headers = {
68
70
  'Accept': '*/*',
69
71
  'Accept-Language': 'en-US,en;q=0.9',
70
72
  'User-Agent': Lit().random(),
71
73
  'Content-Type': 'application/json',
74
+ # Add sec-ch-ua headers if needed for impersonation consistency
72
75
  }
73
76
 
77
+ # Update curl_cffi session headers and proxies
74
78
  self.session.headers.update(self.headers)
75
- self.session.proxies = proxies
79
+ self.session.proxies = proxies # Assign proxies directly
76
80
 
77
81
  self.__available_optimizers = (
78
82
  method for method in dir(Optimizers)
@@ -92,6 +96,7 @@ class TextPollinationsAI(Provider):
92
96
  )
93
97
  self.conversation.history_offset = history_offset
94
98
 
99
+
95
100
  def ask(
96
101
  self,
97
102
  prompt: str,
@@ -128,52 +133,95 @@ class TextPollinationsAI(Provider):
128
133
  payload["tool_choice"] = tool_choice
129
134
 
130
135
  def for_stream():
131
- response = self.session.post(
132
- self.api_endpoint,
133
- headers=self.headers,
134
- json=payload,
135
- stream=True,
136
- timeout=self.timeout
137
- )
136
+ try: # Add try block for CurlError
137
+ # Use curl_cffi session post with impersonate
138
+ response = self.session.post(
139
+ self.api_endpoint,
140
+ # headers are set on the session
141
+ json=payload,
142
+ stream=True,
143
+ timeout=self.timeout,
144
+ impersonate="chrome120" # Add impersonate
145
+ )
146
+
147
+ if not response.ok:
148
+ raise exceptions.FailedToGenerateResponseError(
149
+ f"Failed to generate response - ({response.status_code}, {response.reason}) - {response.text}"
150
+ )
138
151
 
139
- if not response.ok:
140
- raise exceptions.FailedToGenerateResponseError(
141
- f"Failed to generate response - ({response.status_code}, {response.reason}) - {response.text}"
152
+ streaming_text = ""
153
+ # Use sanitize_stream
154
+ processed_stream = sanitize_stream(
155
+ data=response.iter_content(chunk_size=None), # Pass byte iterator
156
+ intro_value="data:",
157
+ to_json=True, # Stream sends JSON
158
+ skip_markers=["[DONE]"],
159
+ # Extractor handles both content and tool_calls
160
+ content_extractor=lambda chunk: chunk.get('choices', [{}])[0].get('delta') if isinstance(chunk, dict) else None,
161
+ yield_raw_on_error=False # Skip non-JSON or lines where extractor fails
142
162
  )
143
163
 
144
- full_response = ""
145
- for line in response.iter_lines():
146
- if line:
147
- line = line.decode('utf-8').strip()
148
- if line == "data: [DONE]":
149
- break
150
- if line.startswith('data: '):
151
- try:
152
- json_data = json.loads(line[6:])
153
- if 'choices' in json_data and len(json_data['choices']) > 0:
154
- choice = json_data['choices'][0]
155
- if 'delta' in choice:
156
- if 'content' in choice['delta']:
157
- content = choice['delta']['content']
158
- full_response += content
159
- yield content if raw else dict(text=content)
160
- elif 'tool_calls' in choice['delta']:
161
- # Handle tool calls in streaming response
162
- tool_calls = choice['delta']['tool_calls']
163
- yield tool_calls if raw else dict(tool_calls=tool_calls)
164
- except json.JSONDecodeError:
165
- continue
166
-
167
- self.last_response.update(dict(text=full_response))
168
- self.conversation.update_chat_history(
169
- prompt, self.get_message(self.last_response)
170
- )
164
+ for delta in processed_stream:
165
+ # delta is the extracted 'delta' object or None
166
+ if delta and isinstance(delta, dict):
167
+ if 'content' in delta and delta['content'] is not None:
168
+ content = delta['content']
169
+ streaming_text += content
170
+ yield content if raw else dict(text=content)
171
+ elif 'tool_calls' in delta:
172
+ tool_calls = delta['tool_calls']
173
+ yield tool_calls if raw else dict(tool_calls=tool_calls)
174
+
175
+ # Update history and last response after stream finishes
176
+ self.last_response.update(dict(text=streaming_text)) # Store aggregated text
177
+ if streaming_text: # Only update history if text was received
178
+ self.conversation.update_chat_history(
179
+ prompt, streaming_text # Use the fully aggregated text
180
+ )
181
+ except CurlError as e: # Catch CurlError
182
+ raise exceptions.FailedToGenerateResponseError(f"Request failed (CurlError): {e}") from e
183
+ except Exception as e: # Catch other potential exceptions
184
+ raise exceptions.FailedToGenerateResponseError(f"An unexpected error occurred ({type(e).__name__}): {e}") from e
185
+
171
186
 
172
187
  def for_non_stream():
173
- for _ in for_stream():
174
- pass
188
+ # Aggregate the stream using the updated for_stream logic
189
+ final_content = ""
190
+ tool_calls_aggregated = None # To store potential tool calls
191
+ try: # Add try block for potential errors during aggregation
192
+ for chunk_data in for_stream():
193
+ if isinstance(chunk_data, dict):
194
+ if "text" in chunk_data:
195
+ final_content += chunk_data["text"]
196
+ elif "tool_calls" in chunk_data:
197
+ # Aggregate tool calls (simple aggregation, might need refinement)
198
+ if tool_calls_aggregated is None:
199
+ tool_calls_aggregated = []
200
+ tool_calls_aggregated.extend(chunk_data["tool_calls"])
201
+ elif isinstance(chunk_data, str): # Handle raw stream case
202
+ final_content += chunk_data
203
+ # Handle raw tool calls list if raw=True
204
+ elif isinstance(chunk_data, list) and raw:
205
+ if tool_calls_aggregated is None:
206
+ tool_calls_aggregated = []
207
+ tool_calls_aggregated.extend(chunk_data)
208
+ except Exception as e:
209
+ # If aggregation fails but some text was received, use it. Otherwise, re-raise.
210
+ if not final_content and not tool_calls_aggregated:
211
+ raise exceptions.FailedToGenerateResponseError(f"Failed to get non-stream response: {str(e)}") from e
212
+
213
+
214
+ # last_response and history are updated within for_stream (for text)
215
+ # Return a dict containing text and/or tool_calls
216
+ result = {}
217
+ if final_content:
218
+ result["text"] = final_content
219
+ if tool_calls_aggregated:
220
+ result["tool_calls"] = tool_calls_aggregated
221
+ self.last_response = result # Update last_response with aggregated result
175
222
  return self.last_response
176
223
 
224
+
177
225
  return for_stream() if stream else for_non_stream()
178
226
 
179
227
  def chat(
@@ -215,8 +263,10 @@ class TextPollinationsAI(Provider):
215
263
  elif "tool_calls" in response:
216
264
  # For tool calls, return a string representation
217
265
  return json.dumps(response["tool_calls"])
266
+ return "" # Return empty string if neither text nor tool_calls found
218
267
 
219
268
  if __name__ == "__main__":
269
+ # Ensure curl_cffi is installed
220
270
  print("-" * 80)
221
271
  print(f"{'Model':<50} {'Status':<10} {'Response'}")
222
272
  print("-" * 80)
@@ -225,22 +275,32 @@ if __name__ == "__main__":
225
275
  working = 0
226
276
  total = len(TextPollinationsAI.AVAILABLE_MODELS)
227
277
 
278
+
228
279
  for model in TextPollinationsAI.AVAILABLE_MODELS:
229
280
  try:
230
281
  test_ai = TextPollinationsAI(model=model, timeout=60)
231
- response = test_ai.chat("Say 'Hello' in one word", stream=True)
282
+ # Test stream first
283
+ response_stream = test_ai.chat("Say 'Hello' in one word", stream=True)
232
284
  response_text = ""
233
- for chunk in response:
285
+ print(f"\r{model:<50} {'Streaming...':<10}", end="", flush=True)
286
+ for chunk in response_stream:
234
287
  response_text += chunk
235
- print(f"\r{model:<50} {'Testing...':<10}", end="", flush=True)
236
288
 
237
289
  if response_text and len(response_text.strip()) > 0:
238
290
  status = "✓"
239
- # Truncate response if too long
240
- display_text = response_text.strip()[:50] + "..." if len(response_text.strip()) > 50 else response_text.strip()
291
+ # Clean and truncate response
292
+ clean_text = response_text.strip()
293
+ display_text = clean_text[:50] + "..." if len(clean_text) > 50 else clean_text
241
294
  else:
242
- status = "✗"
243
- display_text = "Empty or invalid response"
295
+ status = "✗ (Stream)"
296
+ display_text = "Empty or invalid stream response"
244
297
  print(f"\r{model:<50} {status:<10} {display_text}")
298
+
299
+ # Optional: Add non-stream test if needed
300
+ # print(f"\r{model:<50} {'Non-Stream...':<10}", end="", flush=True)
301
+ # response_non_stream = test_ai.chat("Say 'Hi' again", stream=False)
302
+ # if not response_non_stream or len(response_non_stream.strip()) == 0:
303
+ # print(f"\r{model:<50} {'✗ (Non-Stream)':<10} Empty non-stream response")
304
+
245
305
  except Exception as e:
246
306
  print(f"\r{model:<50} {'✗':<10} {str(e)}")