webscout 8.3.7__py3-none-any.whl → 2025.10.11__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 (273) hide show
  1. webscout/AIauto.py +250 -250
  2. webscout/AIbase.py +379 -379
  3. webscout/AIutel.py +60 -60
  4. webscout/Bard.py +1012 -1012
  5. webscout/Bing_search.py +417 -417
  6. webscout/DWEBS.py +529 -529
  7. webscout/Extra/Act.md +309 -309
  8. webscout/Extra/GitToolkit/__init__.py +10 -10
  9. webscout/Extra/GitToolkit/gitapi/README.md +110 -110
  10. webscout/Extra/GitToolkit/gitapi/__init__.py +11 -11
  11. webscout/Extra/GitToolkit/gitapi/repository.py +195 -195
  12. webscout/Extra/GitToolkit/gitapi/user.py +96 -96
  13. webscout/Extra/GitToolkit/gitapi/utils.py +61 -61
  14. webscout/Extra/YTToolkit/README.md +375 -375
  15. webscout/Extra/YTToolkit/YTdownloader.py +956 -956
  16. webscout/Extra/YTToolkit/__init__.py +2 -2
  17. webscout/Extra/YTToolkit/transcriber.py +475 -475
  18. webscout/Extra/YTToolkit/ytapi/README.md +44 -44
  19. webscout/Extra/YTToolkit/ytapi/__init__.py +6 -6
  20. webscout/Extra/YTToolkit/ytapi/channel.py +307 -307
  21. webscout/Extra/YTToolkit/ytapi/errors.py +13 -13
  22. webscout/Extra/YTToolkit/ytapi/extras.py +118 -118
  23. webscout/Extra/YTToolkit/ytapi/https.py +88 -88
  24. webscout/Extra/YTToolkit/ytapi/patterns.py +61 -61
  25. webscout/Extra/YTToolkit/ytapi/playlist.py +58 -58
  26. webscout/Extra/YTToolkit/ytapi/pool.py +7 -7
  27. webscout/Extra/YTToolkit/ytapi/query.py +39 -39
  28. webscout/Extra/YTToolkit/ytapi/stream.py +62 -62
  29. webscout/Extra/YTToolkit/ytapi/utils.py +62 -62
  30. webscout/Extra/YTToolkit/ytapi/video.py +232 -232
  31. webscout/Extra/autocoder/__init__.py +9 -9
  32. webscout/Extra/autocoder/autocoder.py +1105 -1105
  33. webscout/Extra/autocoder/autocoder_utiles.py +332 -332
  34. webscout/Extra/gguf.md +429 -429
  35. webscout/Extra/gguf.py +1213 -1213
  36. webscout/Extra/tempmail/README.md +487 -487
  37. webscout/Extra/tempmail/__init__.py +27 -27
  38. webscout/Extra/tempmail/async_utils.py +140 -140
  39. webscout/Extra/tempmail/base.py +160 -160
  40. webscout/Extra/tempmail/cli.py +186 -186
  41. webscout/Extra/tempmail/emailnator.py +84 -84
  42. webscout/Extra/tempmail/mail_tm.py +360 -360
  43. webscout/Extra/tempmail/temp_mail_io.py +291 -291
  44. webscout/Extra/weather.md +281 -281
  45. webscout/Extra/weather.py +193 -193
  46. webscout/Litlogger/README.md +10 -10
  47. webscout/Litlogger/__init__.py +15 -15
  48. webscout/Litlogger/formats.py +13 -13
  49. webscout/Litlogger/handlers.py +121 -121
  50. webscout/Litlogger/levels.py +13 -13
  51. webscout/Litlogger/logger.py +134 -134
  52. webscout/Provider/AISEARCH/Perplexity.py +332 -332
  53. webscout/Provider/AISEARCH/README.md +279 -279
  54. webscout/Provider/AISEARCH/__init__.py +16 -1
  55. webscout/Provider/AISEARCH/felo_search.py +206 -206
  56. webscout/Provider/AISEARCH/genspark_search.py +323 -323
  57. webscout/Provider/AISEARCH/hika_search.py +185 -185
  58. webscout/Provider/AISEARCH/iask_search.py +410 -410
  59. webscout/Provider/AISEARCH/monica_search.py +219 -219
  60. webscout/Provider/AISEARCH/scira_search.py +316 -316
  61. webscout/Provider/AISEARCH/stellar_search.py +177 -177
  62. webscout/Provider/AISEARCH/webpilotai_search.py +255 -255
  63. webscout/Provider/Aitopia.py +314 -314
  64. webscout/Provider/Apriel.py +306 -0
  65. webscout/Provider/ChatGPTClone.py +236 -236
  66. webscout/Provider/ChatSandbox.py +343 -343
  67. webscout/Provider/Cloudflare.py +324 -324
  68. webscout/Provider/Cohere.py +208 -208
  69. webscout/Provider/Deepinfra.py +370 -366
  70. webscout/Provider/ExaAI.py +260 -260
  71. webscout/Provider/ExaChat.py +308 -308
  72. webscout/Provider/Flowith.py +221 -221
  73. webscout/Provider/GMI.py +293 -0
  74. webscout/Provider/Gemini.py +164 -164
  75. webscout/Provider/GeminiProxy.py +167 -167
  76. webscout/Provider/GithubChat.py +371 -372
  77. webscout/Provider/Groq.py +800 -800
  78. webscout/Provider/HeckAI.py +383 -383
  79. webscout/Provider/Jadve.py +282 -282
  80. webscout/Provider/K2Think.py +307 -307
  81. webscout/Provider/Koboldai.py +205 -205
  82. webscout/Provider/LambdaChat.py +423 -423
  83. webscout/Provider/Nemotron.py +244 -244
  84. webscout/Provider/Netwrck.py +248 -248
  85. webscout/Provider/OLLAMA.py +395 -395
  86. webscout/Provider/OPENAI/Cloudflare.py +393 -393
  87. webscout/Provider/OPENAI/FalconH1.py +451 -451
  88. webscout/Provider/OPENAI/FreeGemini.py +296 -296
  89. webscout/Provider/OPENAI/K2Think.py +431 -431
  90. webscout/Provider/OPENAI/NEMOTRON.py +240 -240
  91. webscout/Provider/OPENAI/PI.py +427 -427
  92. webscout/Provider/OPENAI/README.md +959 -959
  93. webscout/Provider/OPENAI/TogetherAI.py +345 -345
  94. webscout/Provider/OPENAI/TwoAI.py +465 -465
  95. webscout/Provider/OPENAI/__init__.py +33 -18
  96. webscout/Provider/OPENAI/base.py +248 -248
  97. webscout/Provider/OPENAI/chatglm.py +528 -0
  98. webscout/Provider/OPENAI/chatgpt.py +592 -592
  99. webscout/Provider/OPENAI/chatgptclone.py +521 -521
  100. webscout/Provider/OPENAI/chatsandbox.py +202 -202
  101. webscout/Provider/OPENAI/deepinfra.py +318 -314
  102. webscout/Provider/OPENAI/e2b.py +1665 -1665
  103. webscout/Provider/OPENAI/exaai.py +420 -420
  104. webscout/Provider/OPENAI/exachat.py +452 -452
  105. webscout/Provider/OPENAI/friendli.py +232 -232
  106. webscout/Provider/OPENAI/{refact.py → gmi.py} +324 -274
  107. webscout/Provider/OPENAI/groq.py +364 -364
  108. webscout/Provider/OPENAI/heckai.py +314 -314
  109. webscout/Provider/OPENAI/llmchatco.py +337 -337
  110. webscout/Provider/OPENAI/netwrck.py +355 -355
  111. webscout/Provider/OPENAI/oivscode.py +290 -290
  112. webscout/Provider/OPENAI/opkfc.py +518 -518
  113. webscout/Provider/OPENAI/pydantic_imports.py +1 -1
  114. webscout/Provider/OPENAI/scirachat.py +535 -535
  115. webscout/Provider/OPENAI/sonus.py +308 -308
  116. webscout/Provider/OPENAI/standardinput.py +442 -442
  117. webscout/Provider/OPENAI/textpollinations.py +340 -340
  118. webscout/Provider/OPENAI/toolbaz.py +419 -416
  119. webscout/Provider/OPENAI/typefully.py +362 -362
  120. webscout/Provider/OPENAI/utils.py +295 -295
  121. webscout/Provider/OPENAI/venice.py +436 -436
  122. webscout/Provider/OPENAI/wisecat.py +387 -387
  123. webscout/Provider/OPENAI/writecream.py +166 -166
  124. webscout/Provider/OPENAI/x0gpt.py +378 -378
  125. webscout/Provider/OPENAI/yep.py +389 -389
  126. webscout/Provider/OpenGPT.py +230 -230
  127. webscout/Provider/Openai.py +243 -243
  128. webscout/Provider/PI.py +405 -405
  129. webscout/Provider/Perplexitylabs.py +430 -430
  130. webscout/Provider/QwenLM.py +272 -272
  131. webscout/Provider/STT/__init__.py +16 -1
  132. webscout/Provider/Sambanova.py +257 -257
  133. webscout/Provider/StandardInput.py +309 -309
  134. webscout/Provider/TTI/README.md +82 -82
  135. webscout/Provider/TTI/__init__.py +33 -18
  136. webscout/Provider/TTI/aiarta.py +413 -413
  137. webscout/Provider/TTI/base.py +136 -136
  138. webscout/Provider/TTI/bing.py +243 -243
  139. webscout/Provider/TTI/gpt1image.py +149 -149
  140. webscout/Provider/TTI/imagen.py +196 -196
  141. webscout/Provider/TTI/infip.py +211 -211
  142. webscout/Provider/TTI/magicstudio.py +232 -232
  143. webscout/Provider/TTI/monochat.py +219 -219
  144. webscout/Provider/TTI/piclumen.py +214 -214
  145. webscout/Provider/TTI/pixelmuse.py +232 -232
  146. webscout/Provider/TTI/pollinations.py +232 -232
  147. webscout/Provider/TTI/together.py +288 -288
  148. webscout/Provider/TTI/utils.py +12 -12
  149. webscout/Provider/TTI/venice.py +367 -367
  150. webscout/Provider/TTS/README.md +192 -192
  151. webscout/Provider/TTS/__init__.py +33 -18
  152. webscout/Provider/TTS/parler.py +110 -110
  153. webscout/Provider/TTS/streamElements.py +333 -333
  154. webscout/Provider/TTS/utils.py +280 -280
  155. webscout/Provider/TeachAnything.py +237 -237
  156. webscout/Provider/TextPollinationsAI.py +310 -310
  157. webscout/Provider/TogetherAI.py +356 -356
  158. webscout/Provider/TwoAI.py +312 -312
  159. webscout/Provider/TypliAI.py +311 -311
  160. webscout/Provider/UNFINISHED/ChatHub.py +208 -208
  161. webscout/Provider/UNFINISHED/ChutesAI.py +313 -313
  162. webscout/Provider/UNFINISHED/GizAI.py +294 -294
  163. webscout/Provider/UNFINISHED/Marcus.py +198 -198
  164. webscout/Provider/UNFINISHED/Qodo.py +477 -477
  165. webscout/Provider/UNFINISHED/VercelAIGateway.py +338 -338
  166. webscout/Provider/UNFINISHED/XenAI.py +324 -324
  167. webscout/Provider/UNFINISHED/Youchat.py +330 -330
  168. webscout/Provider/UNFINISHED/liner.py +334 -0
  169. webscout/Provider/UNFINISHED/liner_api_request.py +262 -262
  170. webscout/Provider/UNFINISHED/puterjs.py +634 -634
  171. webscout/Provider/UNFINISHED/samurai.py +223 -223
  172. webscout/Provider/UNFINISHED/test_lmarena.py +119 -119
  173. webscout/Provider/Venice.py +250 -250
  174. webscout/Provider/VercelAI.py +256 -256
  175. webscout/Provider/WiseCat.py +231 -231
  176. webscout/Provider/WrDoChat.py +366 -366
  177. webscout/Provider/__init__.py +33 -18
  178. webscout/Provider/ai4chat.py +174 -174
  179. webscout/Provider/akashgpt.py +331 -331
  180. webscout/Provider/cerebras.py +446 -446
  181. webscout/Provider/chatglm.py +394 -301
  182. webscout/Provider/cleeai.py +211 -211
  183. webscout/Provider/elmo.py +282 -282
  184. webscout/Provider/geminiapi.py +208 -208
  185. webscout/Provider/granite.py +261 -261
  186. webscout/Provider/hermes.py +263 -263
  187. webscout/Provider/julius.py +223 -223
  188. webscout/Provider/learnfastai.py +309 -309
  189. webscout/Provider/llama3mitril.py +214 -214
  190. webscout/Provider/llmchat.py +243 -243
  191. webscout/Provider/llmchatco.py +290 -290
  192. webscout/Provider/meta.py +801 -801
  193. webscout/Provider/oivscode.py +309 -309
  194. webscout/Provider/scira_chat.py +383 -383
  195. webscout/Provider/searchchat.py +292 -292
  196. webscout/Provider/sonus.py +258 -258
  197. webscout/Provider/toolbaz.py +370 -367
  198. webscout/Provider/turboseek.py +273 -273
  199. webscout/Provider/typefully.py +207 -207
  200. webscout/Provider/yep.py +372 -372
  201. webscout/__init__.py +30 -31
  202. webscout/__main__.py +5 -5
  203. webscout/auth/api_key_manager.py +189 -189
  204. webscout/auth/config.py +175 -175
  205. webscout/auth/models.py +185 -185
  206. webscout/auth/routes.py +664 -664
  207. webscout/auth/simple_logger.py +236 -236
  208. webscout/cli.py +523 -523
  209. webscout/conversation.py +438 -438
  210. webscout/exceptions.py +361 -361
  211. webscout/litagent/Readme.md +298 -298
  212. webscout/litagent/__init__.py +28 -28
  213. webscout/litagent/agent.py +581 -581
  214. webscout/litagent/constants.py +59 -59
  215. webscout/litprinter/__init__.py +58 -58
  216. webscout/models.py +181 -181
  217. webscout/optimizers.py +419 -419
  218. webscout/prompt_manager.py +288 -288
  219. webscout/sanitize.py +1078 -1078
  220. webscout/scout/README.md +401 -401
  221. webscout/scout/__init__.py +8 -8
  222. webscout/scout/core/__init__.py +6 -6
  223. webscout/scout/core/crawler.py +297 -297
  224. webscout/scout/core/scout.py +706 -706
  225. webscout/scout/core/search_result.py +95 -95
  226. webscout/scout/core/text_analyzer.py +62 -62
  227. webscout/scout/core/text_utils.py +277 -277
  228. webscout/scout/core/web_analyzer.py +51 -51
  229. webscout/scout/element.py +599 -599
  230. webscout/scout/parsers/__init__.py +69 -69
  231. webscout/scout/parsers/html5lib_parser.py +172 -172
  232. webscout/scout/parsers/html_parser.py +236 -236
  233. webscout/scout/parsers/lxml_parser.py +178 -178
  234. webscout/scout/utils.py +37 -37
  235. webscout/swiftcli/Readme.md +323 -323
  236. webscout/swiftcli/__init__.py +95 -95
  237. webscout/swiftcli/core/__init__.py +7 -7
  238. webscout/swiftcli/core/cli.py +308 -308
  239. webscout/swiftcli/core/context.py +104 -104
  240. webscout/swiftcli/core/group.py +241 -241
  241. webscout/swiftcli/decorators/__init__.py +28 -28
  242. webscout/swiftcli/decorators/command.py +221 -221
  243. webscout/swiftcli/decorators/options.py +220 -220
  244. webscout/swiftcli/decorators/output.py +302 -302
  245. webscout/swiftcli/exceptions.py +21 -21
  246. webscout/swiftcli/plugins/__init__.py +9 -9
  247. webscout/swiftcli/plugins/base.py +135 -135
  248. webscout/swiftcli/plugins/manager.py +269 -269
  249. webscout/swiftcli/utils/__init__.py +59 -59
  250. webscout/swiftcli/utils/formatting.py +252 -252
  251. webscout/swiftcli/utils/parsing.py +267 -267
  252. webscout/update_checker.py +117 -117
  253. webscout/version.py +1 -1
  254. webscout/webscout_search.py +1183 -1183
  255. webscout/webscout_search_async.py +649 -649
  256. webscout/yep_search.py +346 -346
  257. webscout/zeroart/README.md +89 -89
  258. webscout/zeroart/__init__.py +134 -134
  259. webscout/zeroart/base.py +66 -66
  260. webscout/zeroart/effects.py +100 -100
  261. webscout/zeroart/fonts.py +1238 -1238
  262. {webscout-8.3.7.dist-info → webscout-2025.10.11.dist-info}/METADATA +937 -937
  263. webscout-2025.10.11.dist-info/RECORD +300 -0
  264. webscout/Provider/AISEARCH/DeepFind.py +0 -254
  265. webscout/Provider/OPENAI/Qwen3.py +0 -303
  266. webscout/Provider/OPENAI/qodo.py +0 -630
  267. webscout/Provider/OPENAI/xenai.py +0 -514
  268. webscout/tempid.py +0 -134
  269. webscout-8.3.7.dist-info/RECORD +0 -301
  270. {webscout-8.3.7.dist-info → webscout-2025.10.11.dist-info}/WHEEL +0 -0
  271. {webscout-8.3.7.dist-info → webscout-2025.10.11.dist-info}/entry_points.txt +0 -0
  272. {webscout-8.3.7.dist-info → webscout-2025.10.11.dist-info}/licenses/LICENSE.md +0 -0
  273. {webscout-8.3.7.dist-info → webscout-2025.10.11.dist-info}/top_level.txt +0 -0
webscout/Provider/PI.py CHANGED
@@ -1,405 +1,405 @@
1
- from uuid import uuid4
2
- from curl_cffi.requests import Session
3
- from curl_cffi import CurlError
4
- import json
5
- import re
6
- import threading
7
- from webscout.AIutel import Optimizers
8
- from webscout.AIutel import Conversation, sanitize_stream # Import sanitize_stream
9
- from webscout.AIutel import AwesomePrompts
10
- from webscout.AIbase import Provider
11
- from typing import Dict, Union, Any, Optional
12
- from webscout.litagent import LitAgent
13
- from webscout import exceptions
14
-
15
- class PiAI(Provider):
16
- """
17
- PiAI is a provider class for interacting with the Pi.ai chat API.
18
-
19
- Attributes:
20
- knowledge_cutoff (str): The knowledge cutoff date for the model
21
- AVAILABLE_VOICES (Dict[str, int]): Available voice options for audio responses
22
- AVAILABLE_MODELS (List[str]): Available model options for the API
23
- """
24
- required_auth = False
25
- AVAILABLE_MODELS = ["inflection_3_pi"]
26
- AVAILABLE_VOICES: Dict[str, int] = {
27
- "voice1": 1,
28
- "voice2": 2,
29
- "voice3": 3,
30
- "voice4": 4,
31
- "voice5": 5,
32
- "voice6": 6,
33
- "voice7": 7,
34
- "voice8": 8
35
- }
36
-
37
- def __init__(
38
- self,
39
- is_conversation: bool = True,
40
- max_tokens: int = 2048, # Note: max_tokens is not used by this API
41
- timeout: int = 30,
42
- intro: str = None,
43
- filepath: str = None,
44
- update_file: bool = True,
45
- proxies: dict = {},
46
- history_offset: int = 10250,
47
- act: str = None,
48
- voice: bool = False,
49
- voice_name: str = "voice3",
50
- output_file: str = "PiAI.mp3",
51
- model: str = "inflection_3_pi", # Note: model is not used by this API
52
- ):
53
- """
54
- Initializes PiAI with voice support.
55
-
56
- Args:
57
- voice (bool): Enable/disable voice output
58
- voice_name (str): Name of the voice to use (if None, uses default)
59
- output_file (str): Path to save voice output (default: PiAI.mp3)
60
- """
61
- # Voice settings
62
- self.voice_enabled = voice
63
- self.voice_name = voice_name
64
- self.output_file = output_file
65
-
66
- if voice and voice_name and voice_name not in self.AVAILABLE_VOICES:
67
- raise ValueError(f"Voice '{voice_name}' not available. Choose from: {list(self.AVAILABLE_VOICES.keys())}")
68
-
69
- # Initialize curl_cffi Session instead of cloudscraper/requests
70
- self.session = Session()
71
- self.primary_url = 'https://pi.ai/api/chat'
72
- self.fallback_url = 'https://pi.ai/api/v2/chat'
73
- self.url = self.primary_url
74
- self.headers = {
75
- 'Accept': 'text/event-stream',
76
- 'Accept-Encoding': 'gzip, deflate, br, zstd',
77
- 'Accept-Language': 'en-US,en;q=0.9,en-IN;q=0.8',
78
- 'Content-Type': 'application/json',
79
- 'DNT': '1',
80
- 'Origin': 'https://pi.ai',
81
- 'Referer': 'https://pi.ai/talk',
82
- 'Sec-Fetch-Dest': 'empty',
83
- 'Sec-Fetch-Mode': 'cors',
84
- 'Sec-Fetch-Site': 'same-origin',
85
- 'User-Agent': LitAgent().random(),
86
- 'X-Api-Version': '3'
87
- }
88
- self.cookies = {
89
- '__cf_bm': uuid4().hex
90
- }
91
-
92
- # Update curl_cffi session headers, proxies, and cookies
93
- self.session.headers.update(self.headers)
94
- self.session.proxies = proxies # Assign proxies directly
95
- # Set cookies on the session object for curl_cffi
96
- for name, value in self.cookies.items():
97
- self.session.cookies.set(name, value)
98
-
99
- self.is_conversation = is_conversation
100
- self.max_tokens_to_sample = max_tokens
101
- self.timeout = timeout
102
- self.last_response = {} if self.is_conversation else {'text': ""}
103
- self.conversation_id = None
104
-
105
- self.__available_optimizers = (
106
- method
107
- for method in dir(Optimizers)
108
- if callable(getattr(Optimizers, method)) and not method.startswith("__")
109
- )
110
-
111
- # Setup conversation
112
- Conversation.intro = (
113
- AwesomePrompts().get_act(
114
- act, raise_not_found=True, default=None, case_insensitive=True
115
- ) if act else intro or Conversation.intro
116
- )
117
- self.conversation = Conversation(
118
- is_conversation, self.max_tokens_to_sample, filepath, update_file
119
- )
120
- self.conversation.history_offset = history_offset
121
- self.session.proxies = proxies
122
-
123
- if self.is_conversation:
124
- self.start_conversation()
125
-
126
- @staticmethod
127
- def _pi_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
128
- """Extracts text content from PiAI stream JSON objects."""
129
- if isinstance(chunk, dict) and 'text' in chunk and chunk['text'] is not None:
130
- return chunk.get("text")
131
- return None
132
-
133
- def start_conversation(self) -> str:
134
- """
135
- Initializes a new conversation and returns the conversation ID.
136
- """
137
- try:
138
- # Use curl_cffi session post with impersonate
139
- # Cookies are handled by the session
140
- response = self.session.post(
141
- "https://pi.ai/api/chat/start",
142
- # headers are set on the session
143
- # cookies=self.cookies, # Handled by session
144
- json={},
145
- timeout=self.timeout,
146
- # proxies are set on the session
147
- impersonate="chrome110" # Use a common impersonation profile
148
- )
149
- response.raise_for_status() # Check for HTTP errors
150
-
151
- data = response.json()
152
- # Ensure the expected structure before accessing
153
- if 'conversations' in data and data['conversations'] and 'sid' in data['conversations'][0]:
154
- self.conversation_id = data['conversations'][0]['sid']
155
- return self.conversation_id
156
- else:
157
- raise exceptions.FailedToGenerateResponseError(f"Unexpected response structure from start API: {data}")
158
-
159
- except CurlError as e: # Catch CurlError
160
- raise exceptions.FailedToGenerateResponseError(f"Failed to start conversation (CurlError): {e}") from e
161
- except Exception as e: # Catch other potential exceptions (like HTTPError, JSONDecodeError)
162
- # Extract error text from the response if available
163
- err_text = e.response.text if hasattr(e, 'response') and hasattr(e.response, 'text') else ''
164
- raise exceptions.FailedToGenerateResponseError(f"Failed to start conversation ({type(e).__name__}): {e} - {err_text}") from e
165
-
166
- def ask(
167
- self,
168
- prompt: str,
169
- stream: bool = False,
170
- raw: bool = False,
171
- optimizer: str = None,
172
- conversationally: bool = False,
173
- voice: bool = None,
174
- voice_name: str = None,
175
- output_file: str = None
176
- ) -> Union[dict, str, Any]:
177
- """
178
- Interact with Pi.ai by sending a prompt and receiving a response.
179
- Now supports raw streaming and non-streaming output, matching the pattern in other providers.
180
-
181
- Args:
182
- prompt (str): The prompt to send
183
- stream (bool): Whether to stream the response
184
- raw (bool): Return raw response format
185
- optimizer (str): Prompt optimizer to use
186
- conversationally (bool): Use conversation context
187
- voice (bool): Override default voice setting
188
- voice_name (str): Override default voice name
189
- output_file (str): Override default output file path
190
- """
191
- # Voice configuration
192
- voice = self.voice_enabled if voice is None else voice
193
- voice_name = self.voice_name if voice_name is None else voice_name
194
- output_file = self.output_file if output_file is None else output_file
195
-
196
- if voice and voice_name and voice_name not in self.AVAILABLE_VOICES:
197
- raise ValueError(f"Voice '{voice_name}' not available. Choose from: {list(self.AVAILABLE_VOICES.keys())}")
198
-
199
- conversation_prompt = self.conversation.gen_complete_prompt(prompt)
200
- if optimizer:
201
- if optimizer in self.__available_optimizers:
202
- conversation_prompt = getattr(Optimizers, optimizer)(
203
- conversation_prompt if conversationally else prompt
204
- )
205
- else:
206
- raise Exception(f"Optimizer is not one of {self.__available_optimizers}")
207
-
208
- data = {
209
- 'text': conversation_prompt,
210
- 'conversation': self.conversation_id
211
- }
212
-
213
- def process_stream():
214
- try:
215
- current_url = self.url
216
- response = self.session.post(
217
- current_url,
218
- json=data,
219
- stream=True,
220
- timeout=self.timeout,
221
- impersonate="chrome110"
222
- )
223
- if not response.ok and current_url == self.primary_url:
224
- current_url = self.fallback_url
225
- response = self.session.post(
226
- current_url,
227
- json=data,
228
- stream=True,
229
- timeout=self.timeout,
230
- impersonate="chrome110"
231
- )
232
- response.raise_for_status()
233
-
234
- sids = []
235
- streaming_text = ""
236
- full_raw_data_for_sids = ""
237
-
238
- processed_stream = sanitize_stream(
239
- data=response.iter_lines(),
240
- intro_value="data: ",
241
- to_json=True,
242
- content_extractor=self._pi_extractor,
243
- raw=raw
244
- )
245
- for content in processed_stream:
246
- if raw:
247
- yield content
248
- else:
249
- if content and isinstance(content, str):
250
- streaming_text += content
251
- yield {"text": streaming_text}
252
- # SID extraction for voice
253
- for line_bytes in response.iter_lines():
254
- if line_bytes:
255
- line = line_bytes.decode('utf-8')
256
- full_raw_data_for_sids += line + "\n"
257
- sids = re.findall(r'"sid":"(.*?)"', full_raw_data_for_sids)
258
- second_sid = sids[1] if len(sids) >= 2 else None
259
- if voice and voice_name and second_sid:
260
- threading.Thread(
261
- target=self.download_audio_threaded,
262
- args=(voice_name, second_sid, output_file)
263
- ).start()
264
- if not raw:
265
- self.last_response = dict(text=streaming_text)
266
- self.conversation.update_chat_history(prompt, streaming_text)
267
- except CurlError as e:
268
- raise exceptions.FailedToGenerateResponseError(f"API request failed (CurlError): {e}") from e
269
- except Exception as e:
270
- err_text = getattr(e, 'response', None) and getattr(e.response, 'text', '')
271
- raise exceptions.FailedToGenerateResponseError(f"API request failed ({type(e).__name__}): {e} - {err_text}") from e
272
-
273
- if stream:
274
- return process_stream()
275
- else:
276
- full_response = ""
277
- for chunk in process_stream():
278
- if raw:
279
- if isinstance(chunk, str):
280
- full_response += chunk
281
- else:
282
- if isinstance(chunk, dict) and "text" in chunk:
283
- full_response = chunk["text"]
284
- if not raw:
285
- self.last_response = {"text": full_response}
286
- self.conversation.update_chat_history(prompt, full_response)
287
- return self.last_response
288
- else:
289
- return full_response
290
-
291
- def chat(
292
- self,
293
- prompt: str,
294
- stream: bool = False,
295
- optimizer: str = None,
296
- conversationally: bool = False,
297
- voice: bool = None,
298
- voice_name: str = None,
299
- output_file: str = None,
300
- raw: bool = False, # Added raw parameter
301
- ) -> Union[str, Any]:
302
- """
303
- Generates a response based on the provided prompt.
304
-
305
- Args:
306
- prompt (str): The prompt to send
307
- stream (bool): Whether to stream the response
308
- optimizer (str): Prompt optimizer to use
309
- conversationally (bool): Use conversation context
310
- voice (bool): Override default voice setting
311
- voice_name (str): Override default voice name
312
- output_file (str): Override default output file path
313
- """
314
- # Use instance defaults if not specified
315
- voice = self.voice_enabled if voice is None else voice
316
- voice_name = self.voice_name if voice_name is None else voice_name
317
- output_file = self.output_file if output_file is None else output_file
318
-
319
- if voice and voice_name and voice_name not in self.AVAILABLE_VOICES:
320
- raise ValueError(f"Voice '{voice_name}' not available. Choose from: {list(self.AVAILABLE_VOICES.keys())}")
321
-
322
- if stream:
323
- def stream_generator():
324
- gen = self.ask(
325
- prompt,
326
- stream=True,
327
- raw=raw,
328
- optimizer=optimizer,
329
- conversationally=conversationally,
330
- voice=voice,
331
- voice_name=voice_name,
332
- output_file=output_file
333
- )
334
- for response in gen:
335
- if raw:
336
- yield response
337
- else:
338
- yield self.get_message(response)
339
- return stream_generator()
340
- else:
341
- response_data = self.ask(
342
- prompt,
343
- stream=False,
344
- raw=raw,
345
- optimizer=optimizer,
346
- conversationally=conversationally,
347
- voice=voice,
348
- voice_name=voice_name,
349
- output_file=output_file
350
- )
351
- if raw:
352
- return response_data
353
- else:
354
- return self.get_message(response_data)
355
-
356
- def get_message(self, response: dict) -> str:
357
- """Retrieves message only from response"""
358
- assert isinstance(response, dict), "Response should be of dict data-type only"
359
- return response["text"]
360
-
361
- def download_audio_threaded(self, voice_name: str, second_sid: str, output_file: str) -> None:
362
- """Downloads audio in a separate thread."""
363
- params = {
364
- 'mode': 'eager',
365
- 'voice': f'voice{self.AVAILABLE_VOICES[voice_name]}',
366
- 'messageSid': second_sid,
367
- }
368
-
369
- try:
370
- # Use curl_cffi session get with impersonate
371
- audio_response = self.session.get(
372
- 'https://pi.ai/api/chat/voice',
373
- params=params,
374
- # cookies are handled by the session
375
- # headers are set on the session
376
- timeout=self.timeout,
377
- # proxies are set on the session
378
- impersonate="chrome110" # Use a common impersonation profile
379
- )
380
- audio_response.raise_for_status() # Check for HTTP errors
381
-
382
- with open(output_file, "wb") as file:
383
- file.write(audio_response.content)
384
-
385
- except CurlError: # Catch CurlError
386
- # Optionally log the error
387
- pass
388
- except Exception: # Catch other potential exceptions
389
- # Optionally log the error
390
- pass
391
-
392
- if __name__ == '__main__':
393
- # Ensure curl_cffi is installed
394
- from rich import print
395
- try: # Add try-except block for testing
396
- ai = PiAI(timeout=60)
397
- print("[bold blue]Testing Chat (Stream):[/bold blue]")
398
- response = ai.chat("hi", stream=True, raw=False)
399
- full_response = ""
400
- for chunk in response:
401
- print(chunk, end="", flush=True)
402
- except exceptions.FailedToGenerateResponseError as e:
403
- print(f"\n[bold red]API Error:[/bold red] {e}")
404
- except Exception as e:
405
- print(f"\n[bold red]An unexpected error occurred:[/bold red] {e}")
1
+ from uuid import uuid4
2
+ from curl_cffi.requests import Session
3
+ from curl_cffi import CurlError
4
+ import json
5
+ import re
6
+ import threading
7
+ from webscout.AIutel import Optimizers
8
+ from webscout.AIutel import Conversation, sanitize_stream # Import sanitize_stream
9
+ from webscout.AIutel import AwesomePrompts
10
+ from webscout.AIbase import Provider
11
+ from typing import Dict, Union, Any, Optional
12
+ from webscout.litagent import LitAgent
13
+ from webscout import exceptions
14
+
15
+ class PiAI(Provider):
16
+ """
17
+ PiAI is a provider class for interacting with the Pi.ai chat API.
18
+
19
+ Attributes:
20
+ knowledge_cutoff (str): The knowledge cutoff date for the model
21
+ AVAILABLE_VOICES (Dict[str, int]): Available voice options for audio responses
22
+ AVAILABLE_MODELS (List[str]): Available model options for the API
23
+ """
24
+ required_auth = False
25
+ AVAILABLE_MODELS = ["inflection_3_pi"]
26
+ AVAILABLE_VOICES: Dict[str, int] = {
27
+ "voice1": 1,
28
+ "voice2": 2,
29
+ "voice3": 3,
30
+ "voice4": 4,
31
+ "voice5": 5,
32
+ "voice6": 6,
33
+ "voice7": 7,
34
+ "voice8": 8
35
+ }
36
+
37
+ def __init__(
38
+ self,
39
+ is_conversation: bool = True,
40
+ max_tokens: int = 2048, # Note: max_tokens is not used by this API
41
+ timeout: int = 30,
42
+ intro: str = None,
43
+ filepath: str = None,
44
+ update_file: bool = True,
45
+ proxies: dict = {},
46
+ history_offset: int = 10250,
47
+ act: str = None,
48
+ voice: bool = False,
49
+ voice_name: str = "voice3",
50
+ output_file: str = "PiAI.mp3",
51
+ model: str = "inflection_3_pi", # Note: model is not used by this API
52
+ ):
53
+ """
54
+ Initializes PiAI with voice support.
55
+
56
+ Args:
57
+ voice (bool): Enable/disable voice output
58
+ voice_name (str): Name of the voice to use (if None, uses default)
59
+ output_file (str): Path to save voice output (default: PiAI.mp3)
60
+ """
61
+ # Voice settings
62
+ self.voice_enabled = voice
63
+ self.voice_name = voice_name
64
+ self.output_file = output_file
65
+
66
+ if voice and voice_name and voice_name not in self.AVAILABLE_VOICES:
67
+ raise ValueError(f"Voice '{voice_name}' not available. Choose from: {list(self.AVAILABLE_VOICES.keys())}")
68
+
69
+ # Initialize curl_cffi Session instead of cloudscraper/requests
70
+ self.session = Session()
71
+ self.primary_url = 'https://pi.ai/api/chat'
72
+ self.fallback_url = 'https://pi.ai/api/v2/chat'
73
+ self.url = self.primary_url
74
+ self.headers = {
75
+ 'Accept': 'text/event-stream',
76
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
77
+ 'Accept-Language': 'en-US,en;q=0.9,en-IN;q=0.8',
78
+ 'Content-Type': 'application/json',
79
+ 'DNT': '1',
80
+ 'Origin': 'https://pi.ai',
81
+ 'Referer': 'https://pi.ai/talk',
82
+ 'Sec-Fetch-Dest': 'empty',
83
+ 'Sec-Fetch-Mode': 'cors',
84
+ 'Sec-Fetch-Site': 'same-origin',
85
+ 'User-Agent': LitAgent().random(),
86
+ 'X-Api-Version': '3'
87
+ }
88
+ self.cookies = {
89
+ '__cf_bm': uuid4().hex
90
+ }
91
+
92
+ # Update curl_cffi session headers, proxies, and cookies
93
+ self.session.headers.update(self.headers)
94
+ self.session.proxies = proxies # Assign proxies directly
95
+ # Set cookies on the session object for curl_cffi
96
+ for name, value in self.cookies.items():
97
+ self.session.cookies.set(name, value)
98
+
99
+ self.is_conversation = is_conversation
100
+ self.max_tokens_to_sample = max_tokens
101
+ self.timeout = timeout
102
+ self.last_response = {} if self.is_conversation else {'text': ""}
103
+ self.conversation_id = None
104
+
105
+ self.__available_optimizers = (
106
+ method
107
+ for method in dir(Optimizers)
108
+ if callable(getattr(Optimizers, method)) and not method.startswith("__")
109
+ )
110
+
111
+ # Setup conversation
112
+ Conversation.intro = (
113
+ AwesomePrompts().get_act(
114
+ act, raise_not_found=True, default=None, case_insensitive=True
115
+ ) if act else intro or Conversation.intro
116
+ )
117
+ self.conversation = Conversation(
118
+ is_conversation, self.max_tokens_to_sample, filepath, update_file
119
+ )
120
+ self.conversation.history_offset = history_offset
121
+ self.session.proxies = proxies
122
+
123
+ if self.is_conversation:
124
+ self.start_conversation()
125
+
126
+ @staticmethod
127
+ def _pi_extractor(chunk: Union[str, Dict[str, Any]]) -> Optional[str]:
128
+ """Extracts text content from PiAI stream JSON objects."""
129
+ if isinstance(chunk, dict) and 'text' in chunk and chunk['text'] is not None:
130
+ return chunk.get("text")
131
+ return None
132
+
133
+ def start_conversation(self) -> str:
134
+ """
135
+ Initializes a new conversation and returns the conversation ID.
136
+ """
137
+ try:
138
+ # Use curl_cffi session post with impersonate
139
+ # Cookies are handled by the session
140
+ response = self.session.post(
141
+ "https://pi.ai/api/chat/start",
142
+ # headers are set on the session
143
+ # cookies=self.cookies, # Handled by session
144
+ json={},
145
+ timeout=self.timeout,
146
+ # proxies are set on the session
147
+ impersonate="chrome110" # Use a common impersonation profile
148
+ )
149
+ response.raise_for_status() # Check for HTTP errors
150
+
151
+ data = response.json()
152
+ # Ensure the expected structure before accessing
153
+ if 'conversations' in data and data['conversations'] and 'sid' in data['conversations'][0]:
154
+ self.conversation_id = data['conversations'][0]['sid']
155
+ return self.conversation_id
156
+ else:
157
+ raise exceptions.FailedToGenerateResponseError(f"Unexpected response structure from start API: {data}")
158
+
159
+ except CurlError as e: # Catch CurlError
160
+ raise exceptions.FailedToGenerateResponseError(f"Failed to start conversation (CurlError): {e}") from e
161
+ except Exception as e: # Catch other potential exceptions (like HTTPError, JSONDecodeError)
162
+ # Extract error text from the response if available
163
+ err_text = e.response.text if hasattr(e, 'response') and hasattr(e.response, 'text') else ''
164
+ raise exceptions.FailedToGenerateResponseError(f"Failed to start conversation ({type(e).__name__}): {e} - {err_text}") from e
165
+
166
+ def ask(
167
+ self,
168
+ prompt: str,
169
+ stream: bool = False,
170
+ raw: bool = False,
171
+ optimizer: str = None,
172
+ conversationally: bool = False,
173
+ voice: bool = None,
174
+ voice_name: str = None,
175
+ output_file: str = None
176
+ ) -> Union[dict, str, Any]:
177
+ """
178
+ Interact with Pi.ai by sending a prompt and receiving a response.
179
+ Now supports raw streaming and non-streaming output, matching the pattern in other providers.
180
+
181
+ Args:
182
+ prompt (str): The prompt to send
183
+ stream (bool): Whether to stream the response
184
+ raw (bool): Return raw response format
185
+ optimizer (str): Prompt optimizer to use
186
+ conversationally (bool): Use conversation context
187
+ voice (bool): Override default voice setting
188
+ voice_name (str): Override default voice name
189
+ output_file (str): Override default output file path
190
+ """
191
+ # Voice configuration
192
+ voice = self.voice_enabled if voice is None else voice
193
+ voice_name = self.voice_name if voice_name is None else voice_name
194
+ output_file = self.output_file if output_file is None else output_file
195
+
196
+ if voice and voice_name and voice_name not in self.AVAILABLE_VOICES:
197
+ raise ValueError(f"Voice '{voice_name}' not available. Choose from: {list(self.AVAILABLE_VOICES.keys())}")
198
+
199
+ conversation_prompt = self.conversation.gen_complete_prompt(prompt)
200
+ if optimizer:
201
+ if optimizer in self.__available_optimizers:
202
+ conversation_prompt = getattr(Optimizers, optimizer)(
203
+ conversation_prompt if conversationally else prompt
204
+ )
205
+ else:
206
+ raise Exception(f"Optimizer is not one of {self.__available_optimizers}")
207
+
208
+ data = {
209
+ 'text': conversation_prompt,
210
+ 'conversation': self.conversation_id
211
+ }
212
+
213
+ def process_stream():
214
+ try:
215
+ current_url = self.url
216
+ response = self.session.post(
217
+ current_url,
218
+ json=data,
219
+ stream=True,
220
+ timeout=self.timeout,
221
+ impersonate="chrome110"
222
+ )
223
+ if not response.ok and current_url == self.primary_url:
224
+ current_url = self.fallback_url
225
+ response = self.session.post(
226
+ current_url,
227
+ json=data,
228
+ stream=True,
229
+ timeout=self.timeout,
230
+ impersonate="chrome110"
231
+ )
232
+ response.raise_for_status()
233
+
234
+ sids = []
235
+ streaming_text = ""
236
+ full_raw_data_for_sids = ""
237
+
238
+ processed_stream = sanitize_stream(
239
+ data=response.iter_lines(),
240
+ intro_value="data: ",
241
+ to_json=True,
242
+ content_extractor=self._pi_extractor,
243
+ raw=raw
244
+ )
245
+ for content in processed_stream:
246
+ if raw:
247
+ yield content
248
+ else:
249
+ if content and isinstance(content, str):
250
+ streaming_text += content
251
+ yield {"text": streaming_text}
252
+ # SID extraction for voice
253
+ for line_bytes in response.iter_lines():
254
+ if line_bytes:
255
+ line = line_bytes.decode('utf-8')
256
+ full_raw_data_for_sids += line + "\n"
257
+ sids = re.findall(r'"sid":"(.*?)"', full_raw_data_for_sids)
258
+ second_sid = sids[1] if len(sids) >= 2 else None
259
+ if voice and voice_name and second_sid:
260
+ threading.Thread(
261
+ target=self.download_audio_threaded,
262
+ args=(voice_name, second_sid, output_file)
263
+ ).start()
264
+ if not raw:
265
+ self.last_response = dict(text=streaming_text)
266
+ self.conversation.update_chat_history(prompt, streaming_text)
267
+ except CurlError as e:
268
+ raise exceptions.FailedToGenerateResponseError(f"API request failed (CurlError): {e}") from e
269
+ except Exception as e:
270
+ err_text = getattr(e, 'response', None) and getattr(e.response, 'text', '')
271
+ raise exceptions.FailedToGenerateResponseError(f"API request failed ({type(e).__name__}): {e} - {err_text}") from e
272
+
273
+ if stream:
274
+ return process_stream()
275
+ else:
276
+ full_response = ""
277
+ for chunk in process_stream():
278
+ if raw:
279
+ if isinstance(chunk, str):
280
+ full_response += chunk
281
+ else:
282
+ if isinstance(chunk, dict) and "text" in chunk:
283
+ full_response = chunk["text"]
284
+ if not raw:
285
+ self.last_response = {"text": full_response}
286
+ self.conversation.update_chat_history(prompt, full_response)
287
+ return self.last_response
288
+ else:
289
+ return full_response
290
+
291
+ def chat(
292
+ self,
293
+ prompt: str,
294
+ stream: bool = False,
295
+ optimizer: str = None,
296
+ conversationally: bool = False,
297
+ voice: bool = None,
298
+ voice_name: str = None,
299
+ output_file: str = None,
300
+ raw: bool = False, # Added raw parameter
301
+ ) -> Union[str, Any]:
302
+ """
303
+ Generates a response based on the provided prompt.
304
+
305
+ Args:
306
+ prompt (str): The prompt to send
307
+ stream (bool): Whether to stream the response
308
+ optimizer (str): Prompt optimizer to use
309
+ conversationally (bool): Use conversation context
310
+ voice (bool): Override default voice setting
311
+ voice_name (str): Override default voice name
312
+ output_file (str): Override default output file path
313
+ """
314
+ # Use instance defaults if not specified
315
+ voice = self.voice_enabled if voice is None else voice
316
+ voice_name = self.voice_name if voice_name is None else voice_name
317
+ output_file = self.output_file if output_file is None else output_file
318
+
319
+ if voice and voice_name and voice_name not in self.AVAILABLE_VOICES:
320
+ raise ValueError(f"Voice '{voice_name}' not available. Choose from: {list(self.AVAILABLE_VOICES.keys())}")
321
+
322
+ if stream:
323
+ def stream_generator():
324
+ gen = self.ask(
325
+ prompt,
326
+ stream=True,
327
+ raw=raw,
328
+ optimizer=optimizer,
329
+ conversationally=conversationally,
330
+ voice=voice,
331
+ voice_name=voice_name,
332
+ output_file=output_file
333
+ )
334
+ for response in gen:
335
+ if raw:
336
+ yield response
337
+ else:
338
+ yield self.get_message(response)
339
+ return stream_generator()
340
+ else:
341
+ response_data = self.ask(
342
+ prompt,
343
+ stream=False,
344
+ raw=raw,
345
+ optimizer=optimizer,
346
+ conversationally=conversationally,
347
+ voice=voice,
348
+ voice_name=voice_name,
349
+ output_file=output_file
350
+ )
351
+ if raw:
352
+ return response_data
353
+ else:
354
+ return self.get_message(response_data)
355
+
356
+ def get_message(self, response: dict) -> str:
357
+ """Retrieves message only from response"""
358
+ assert isinstance(response, dict), "Response should be of dict data-type only"
359
+ return response["text"]
360
+
361
+ def download_audio_threaded(self, voice_name: str, second_sid: str, output_file: str) -> None:
362
+ """Downloads audio in a separate thread."""
363
+ params = {
364
+ 'mode': 'eager',
365
+ 'voice': f'voice{self.AVAILABLE_VOICES[voice_name]}',
366
+ 'messageSid': second_sid,
367
+ }
368
+
369
+ try:
370
+ # Use curl_cffi session get with impersonate
371
+ audio_response = self.session.get(
372
+ 'https://pi.ai/api/chat/voice',
373
+ params=params,
374
+ # cookies are handled by the session
375
+ # headers are set on the session
376
+ timeout=self.timeout,
377
+ # proxies are set on the session
378
+ impersonate="chrome110" # Use a common impersonation profile
379
+ )
380
+ audio_response.raise_for_status() # Check for HTTP errors
381
+
382
+ with open(output_file, "wb") as file:
383
+ file.write(audio_response.content)
384
+
385
+ except CurlError: # Catch CurlError
386
+ # Optionally log the error
387
+ pass
388
+ except Exception: # Catch other potential exceptions
389
+ # Optionally log the error
390
+ pass
391
+
392
+ if __name__ == '__main__':
393
+ # Ensure curl_cffi is installed
394
+ from rich import print
395
+ try: # Add try-except block for testing
396
+ ai = PiAI(timeout=60)
397
+ print("[bold blue]Testing Chat (Stream):[/bold blue]")
398
+ response = ai.chat("hi", stream=True, raw=False)
399
+ full_response = ""
400
+ for chunk in response:
401
+ print(chunk, end="", flush=True)
402
+ except exceptions.FailedToGenerateResponseError as e:
403
+ print(f"\n[bold red]API Error:[/bold red] {e}")
404
+ except Exception as e:
405
+ print(f"\n[bold red]An unexpected error occurred:[/bold red] {e}")