webscout 4.6__py3-none-any.whl → 4.8__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 (44) hide show
  1. webscout/Agents/functioncall.py +97 -37
  2. webscout/Bard.py +365 -0
  3. webscout/Local/_version.py +1 -1
  4. webscout/Provider/Andi.py +7 -1
  5. webscout/Provider/BasedGPT.py +11 -5
  6. webscout/Provider/Berlin4h.py +11 -5
  7. webscout/Provider/Blackboxai.py +10 -4
  8. webscout/Provider/Cohere.py +11 -5
  9. webscout/Provider/DARKAI.py +25 -7
  10. webscout/Provider/Deepinfra.py +2 -1
  11. webscout/Provider/Deepseek.py +25 -9
  12. webscout/Provider/DiscordRocks.py +389 -0
  13. webscout/Provider/{ChatGPTUK.py → Farfalle.py} +80 -67
  14. webscout/Provider/Gemini.py +1 -1
  15. webscout/Provider/Groq.py +244 -110
  16. webscout/Provider/Llama.py +13 -5
  17. webscout/Provider/Llama3.py +15 -2
  18. webscout/Provider/OLLAMA.py +8 -7
  19. webscout/Provider/Perplexity.py +422 -52
  20. webscout/Provider/Phind.py +6 -5
  21. webscout/Provider/PizzaGPT.py +7 -1
  22. webscout/Provider/__init__.py +15 -31
  23. webscout/Provider/ai4chat.py +193 -0
  24. webscout/Provider/koala.py +11 -5
  25. webscout/Provider/{VTLchat.py → liaobots.py} +120 -104
  26. webscout/Provider/meta.py +779 -0
  27. webscout/exceptions.py +6 -0
  28. webscout/version.py +1 -1
  29. webscout/webai.py +2 -64
  30. webscout/webscout_search.py +1 -1
  31. {webscout-4.6.dist-info → webscout-4.8.dist-info}/METADATA +254 -297
  32. {webscout-4.6.dist-info → webscout-4.8.dist-info}/RECORD +36 -40
  33. webscout/Provider/FreeGemini.py +0 -169
  34. webscout/Provider/Geminiflash.py +0 -152
  35. webscout/Provider/Geminipro.py +0 -152
  36. webscout/Provider/Leo.py +0 -469
  37. webscout/Provider/OpenGPT.py +0 -867
  38. webscout/Provider/Xjai.py +0 -230
  39. webscout/Provider/Yepchat.py +0 -478
  40. webscout/Provider/Youchat.py +0 -225
  41. {webscout-4.6.dist-info → webscout-4.8.dist-info}/LICENSE.md +0 -0
  42. {webscout-4.6.dist-info → webscout-4.8.dist-info}/WHEEL +0 -0
  43. {webscout-4.6.dist-info → webscout-4.8.dist-info}/entry_points.txt +0 -0
  44. {webscout-4.6.dist-info → webscout-4.8.dist-info}/top_level.txt +0 -0
@@ -13,10 +13,10 @@ import io
13
13
  import re
14
14
  import json
15
15
  import yaml
16
- from ..AIutel import Optimizers
17
- from ..AIutel import Conversation
18
- from ..AIutel import AwesomePrompts, sanitize_stream
19
- from ..AIbase import Provider, AsyncProvider
16
+ from webscout.AIutel import Optimizers
17
+ from webscout.AIutel import Conversation
18
+ from webscout.AIutel import AwesomePrompts, sanitize_stream
19
+ from webscout.AIbase import Provider, AsyncProvider
20
20
  from webscout import exceptions
21
21
  from typing import Any, AsyncGenerator, Dict
22
22
  import logging
@@ -182,6 +182,7 @@ class OLLAMA(Provider):
182
182
  assert isinstance(response, dict), "Response should be of dict data-type only"
183
183
  return response["text"]
184
184
  if __name__ == "__main__":
185
- ollama_provider = OLLAMA(model="qwen2:0.5b")
186
- response = ollama_provider.chat("What is the meaning of life?")
187
- print(response)
185
+ ollama_provider = OLLAMA(model="qwen:0.5b")
186
+ response = ollama_provider.chat("hi")
187
+ for r in response:
188
+ print(r, end="", flush=True)
@@ -1,37 +1,29 @@
1
+ import json
1
2
  import time
2
3
  import uuid
3
- from selenium import webdriver
4
- from selenium.webdriver.chrome.options import Options
5
- from selenium.webdriver.common.by import By
6
- from selenium.webdriver.support import expected_conditions as EC
7
- from selenium.webdriver.support.ui import WebDriverWait
8
- import click
9
- import requests
10
- from requests import get
4
+ from typing import Iterable, Dict, Any, Generator
5
+
6
+ from os import listdir
11
7
  from uuid import uuid4
12
- from re import findall
13
- from requests.exceptions import RequestException
14
- from curl_cffi.requests import get, RequestsError
15
- import g4f
16
- from random import randint
17
- from PIL import Image
18
- import io
19
- import re
20
- import json
8
+ from time import sleep, time
9
+ from threading import Thread
10
+ from json import loads, dumps
11
+ from random import getrandbits
12
+ from websocket import WebSocketApp
13
+ from requests import Session, get, post
21
14
  import yaml
22
- from ..AIutel import Optimizers
23
- from ..AIutel import Conversation
24
- from ..AIutel import AwesomePrompts, sanitize_stream
25
- from ..AIbase import Provider, AsyncProvider
26
- from Helpingai_T2 import Perplexity
15
+
16
+ from webscout.AIutel import Optimizers
17
+ from webscout.AIutel import Conversation
18
+ from webscout.AIutel import AwesomePrompts, sanitize_stream
19
+ from webscout.AIbase import Provider, AsyncProvider
27
20
  from webscout import exceptions
28
- from typing import Any, AsyncGenerator, Dict
29
- import logging
30
- import httpx
31
- #------------------------------------------------------PERPLEXITY--------------------------------------------------------
32
- class PERPLEXITY(Provider):
21
+
22
+
23
+ class Perplexity(Provider):
33
24
  def __init__(
34
25
  self,
26
+ email: str = None,
35
27
  is_conversation: bool = True,
36
28
  max_tokens: int = 600,
37
29
  timeout: int = 30,
@@ -42,11 +34,12 @@ class PERPLEXITY(Provider):
42
34
  history_offset: int = 10250,
43
35
  act: str = None,
44
36
  quiet: bool = False,
45
- ):
37
+ ) -> None:
46
38
  """Instantiates PERPLEXITY
47
39
 
48
40
  Args:
49
- is_conversation (bool, optional): Flag for chatting conversationally. Defaults to True
41
+ email (str, optional): Your perplexity.ai email. Defaults to None.
42
+ is_conversation (bool, optional): Flag for chatting conversationally. Defaults to True.
50
43
  max_tokens (int, optional): Maximum number of tokens to be generated upon completion. Defaults to 600.
51
44
  timeout (int, optional): Http request timeout. Defaults to 30.
52
45
  intro (str, optional): Conversation introductory prompt. Defaults to None.
@@ -63,6 +56,44 @@ class PERPLEXITY(Provider):
63
56
  self.web_results: dict = {}
64
57
  self.quiet = quiet
65
58
 
59
+ self.session: Session = Session()
60
+ self.user_agent: dict = {
61
+ "User-Agent": "Ask/2.9.1/2406 (iOS; iPhone; Version 17.1) isiOSOnMac/false",
62
+ "X-Client-Name": "Perplexity-iOS",
63
+ "X-App-ApiClient": "ios",
64
+ }
65
+ self.session.headers.update(self.user_agent)
66
+
67
+ if email and ".perplexity_session" in listdir():
68
+ self._recover_session(email)
69
+ else:
70
+ self._init_session_without_login()
71
+
72
+ if email:
73
+ self._login(email)
74
+
75
+ self.email: str = email
76
+ self.t: str = self._get_t()
77
+ self.sid: str = self._get_sid()
78
+
79
+ self.n: int = 1
80
+ self.base: int = 420
81
+ self.queue: list = []
82
+ self.finished: bool = True
83
+ self.last_uuid: str = None
84
+ self.backend_uuid: str = (
85
+ None # unused because we can't yet follow-up questions
86
+ )
87
+ self.frontend_session_id: str = str(uuid4())
88
+
89
+ assert self._ask_anonymous_user(), "failed to ask anonymous user"
90
+ self.ws: WebSocketApp = self._init_websocket()
91
+ self.ws_thread: Thread = Thread(target=self.ws.run_forever).start()
92
+ self._auth_session()
93
+
94
+ while not (self.ws.sock and self.ws.sock.connected):
95
+ sleep(0.01)
96
+
66
97
  self.__available_optimizers = (
67
98
  method
68
99
  for method in dir(Optimizers)
@@ -79,6 +110,347 @@ class PERPLEXITY(Provider):
79
110
  is_conversation, self.max_tokens_to_sample, filepath, update_file
80
111
  )
81
112
  self.conversation.history_offset = history_offset
113
+ self.session.proxies = proxies
114
+
115
+ def _recover_session(self, email: str) -> None:
116
+ with open(".perplexity_session", "r") as f:
117
+ perplexity_session: dict = loads(f.read())
118
+
119
+ if email in perplexity_session:
120
+ self.session.cookies.update(perplexity_session[email])
121
+ else:
122
+ self._login(email, perplexity_session)
123
+
124
+ def _login(self, email: str, ps: dict = None) -> None:
125
+ self.session.post(
126
+ url="https://www.perplexity.ai/api/auth/signin-email",
127
+ data={"email": email},
128
+ )
129
+
130
+ email_link: str = str(input("paste the link you received by email: "))
131
+ self.session.get(email_link)
132
+
133
+ if ps:
134
+ ps[email] = self.session.cookies.get_dict()
135
+ else:
136
+ ps = {email: self.session.cookies.get_dict()}
137
+
138
+ with open(".perplexity_session", "w") as f:
139
+ f.write(dumps(ps))
140
+
141
+ def _init_session_without_login(self) -> None:
142
+ self.session.get(url=f"https://www.perplexity.ai/search/{str(uuid4())}")
143
+ self.session.headers.update(self.user_agent)
144
+
145
+ def _auth_session(self) -> None:
146
+ self.session.get(url="https://www.perplexity.ai/api/auth/session")
147
+
148
+ def _get_t(self) -> str:
149
+ return format(getrandbits(32), "08x")
150
+
151
+ def _get_sid(self) -> str:
152
+ return loads(
153
+ self.session.get(
154
+ url=f"https://www.perplexity.ai/socket.io/?EIO=4&transport=polling&t={self.t}"
155
+ ).text[1:]
156
+ )["sid"]
157
+
158
+ def _ask_anonymous_user(self) -> bool:
159
+ response = self.session.post(
160
+ url=f"https://www.perplexity.ai/socket.io/?EIO=4&transport=polling&t={self.t}&sid={self.sid}",
161
+ data='40{"jwt":"anonymous-ask-user"}',
162
+ ).text
163
+
164
+ return response == "OK"
165
+
166
+ def _start_interaction(self) -> None:
167
+ self.finished = False
168
+
169
+ if self.n == 9:
170
+ self.n = 0
171
+ self.base *= 10
172
+ else:
173
+ self.n += 1
174
+
175
+ self.queue = []
176
+
177
+ def _get_cookies_str(self) -> str:
178
+ cookies = ""
179
+ for key, value in self.session.cookies.get_dict().items():
180
+ cookies += f"{key}={value}; "
181
+ return cookies[:-2]
182
+
183
+ def _write_file_url(self, filename: str, file_url: str) -> None:
184
+ if ".perplexity_files_url" in listdir():
185
+ with open(".perplexity_files_url", "r") as f:
186
+ perplexity_files_url: dict = loads(f.read())
187
+ else:
188
+ perplexity_files_url: dict = {}
189
+
190
+ perplexity_files_url[filename] = file_url
191
+
192
+ with open(".perplexity_files_url", "w") as f:
193
+ f.write(dumps(perplexity_files_url))
194
+
195
+ def _init_websocket(self) -> WebSocketApp:
196
+ def on_open(ws: WebSocketApp) -> None:
197
+ ws.send("2probe")
198
+ ws.send("5")
199
+
200
+ def on_message(ws: WebSocketApp, message: str) -> None:
201
+ if message == "2":
202
+ ws.send("3")
203
+ elif not self.finished:
204
+ if message.startswith("42"):
205
+ message: list = loads(message[2:])
206
+ content: dict = message[1]
207
+ if "mode" in content and content["mode"] == "copilot":
208
+ content["copilot_answer"] = loads(content["text"])
209
+ elif "mode" in content:
210
+ content.update(loads(content["text"]))
211
+ content.pop("text")
212
+ if (
213
+ not ("final" in content and content["final"])
214
+ ) or ("status" in content and content["status"] == "completed"):
215
+ self.queue.append(content)
216
+ if message[0] == "query_answered":
217
+ self.last_uuid = content["uuid"]
218
+ self.finished = True
219
+ elif message.startswith("43"):
220
+ message: dict = loads(message[3:])[0]
221
+ if (
222
+ "uuid" in message and message["uuid"] != self.last_uuid
223
+ ) or "uuid" not in message:
224
+ self.queue.append(message)
225
+ self.finished = True
226
+
227
+ return WebSocketApp(
228
+ url=f"wss://www.perplexity.ai/socket.io/?EIO=4&transport=websocket&sid={self.sid}",
229
+ header=self.user_agent,
230
+ cookie=self._get_cookies_str(),
231
+ on_open=on_open,
232
+ on_message=on_message,
233
+ on_error=lambda ws, err: print(f"websocket error: {err}"),
234
+ )
235
+
236
+ def _s(
237
+ self,
238
+ query: str,
239
+ mode: str = "concise",
240
+ search_focus: str = "internet",
241
+ attachments: list[str] = [],
242
+ language: str = "en-GB",
243
+ in_page: str = None,
244
+ in_domain: str = None,
245
+ ) -> None:
246
+ assert self.finished, "already searching"
247
+ assert mode in ["concise", "copilot"], "invalid mode"
248
+ assert len(attachments) <= 4, "too many attachments: max 4"
249
+ assert (
250
+ search_focus
251
+ in [
252
+ "internet",
253
+ "scholar",
254
+ "writing",
255
+ "wolfram",
256
+ "youtube",
257
+ "reddit",
258
+ ]
259
+ ), "invalid search focus"
260
+
261
+ if in_page:
262
+ search_focus = "in_page"
263
+ if in_domain:
264
+ search_focus = "in_domain"
265
+
266
+ self._start_interaction()
267
+ ws_message: str = (
268
+ f"{self.base + self.n}"
269
+ + dumps(
270
+ [
271
+ "perplexity_ask",
272
+ query,
273
+ {
274
+ "version": "2.1",
275
+ "source": "default", # "ios"
276
+ "frontend_session_id": self.frontend_session_id,
277
+ "language": language,
278
+ "timezone": "CET",
279
+ "attachments": attachments,
280
+ "search_focus": search_focus,
281
+ "frontend_uuid": str(uuid4()),
282
+ "mode": mode,
283
+ # "use_inhouse_model": True
284
+ "in_page": in_page,
285
+ "in_domain": in_domain,
286
+ },
287
+ ]
288
+ )
289
+ )
290
+
291
+ self.ws.send(ws_message)
292
+
293
+ def search(
294
+ self,
295
+ query: str,
296
+ mode: str = "concise",
297
+ search_focus: str = "internet",
298
+ attachments: list[str] = [],
299
+ language: str = "en-GB",
300
+ timeout: float = 30,
301
+ in_page: str = None,
302
+ in_domain: str = None,
303
+ ) -> Iterable[Dict]:
304
+ self._s(query, mode, search_focus, attachments, language, in_page, in_domain)
305
+
306
+ start_time: float = time()
307
+ while (not self.finished) or len(self.queue) != 0:
308
+ if timeout and time() - start_time > timeout:
309
+ self.finished = True
310
+ return {"error": "timeout"}
311
+ if len(self.queue) != 0:
312
+ yield self.queue.pop(0)
313
+
314
+ def search_sync(
315
+ self,
316
+ query: str,
317
+ mode: str = "concise",
318
+ search_focus: str = "internet",
319
+ attachments: list[str] = [],
320
+ language: str = "en-GB",
321
+ timeout: float = 30,
322
+ in_page: str = None,
323
+ in_domain: str = None,
324
+ ) -> dict:
325
+ self._s(query, mode, search_focus, attachments, language, in_page, in_domain)
326
+
327
+ start_time: float = time()
328
+ while not self.finished:
329
+ if timeout and time() - start_time > timeout:
330
+ self.finished = True
331
+ return {"error": "timeout"}
332
+
333
+ return self.queue.pop(-1)
334
+
335
+ def upload(self, filename: str) -> str:
336
+ assert self.finished, "already searching"
337
+ assert filename.split(".")[-1] in [
338
+ "txt",
339
+ "pdf",
340
+ ], "invalid file format"
341
+
342
+ if filename.startswith("http"):
343
+ file = get(filename).content
344
+ else:
345
+ with open(filename, "rb") as f:
346
+ file = f.read()
347
+
348
+ self._start_interaction()
349
+ ws_message: str = (
350
+ f"{self.base + self.n}"
351
+ + dumps(
352
+ [
353
+ "get_upload_url",
354
+ {
355
+ "version": "2.1",
356
+ "source": "default",
357
+ "content_type": "text/plain"
358
+ if filename.split(".")[-1] == "txt"
359
+ else "application/pdf",
360
+ },
361
+ ]
362
+ )
363
+ )
364
+
365
+ self.ws.send(ws_message)
366
+
367
+ while not self.finished or len(self.queue) != 0:
368
+ if len(self.queue) != 0:
369
+ upload_data = self.queue.pop(0)
370
+
371
+ assert not upload_data["rate_limited"], "rate limited"
372
+
373
+ post(
374
+ url=upload_data["url"],
375
+ files={
376
+ "acl": (None, upload_data["fields"]["acl"]),
377
+ "Content-Type": (None, upload_data["fields"]["Content-Type"]),
378
+ "key": (None, upload_data["fields"]["key"]),
379
+ "AWSAccessKeyId": (None, upload_data["fields"]["AWSAccessKeyId"]),
380
+ "x-amz-security-token": (
381
+ None,
382
+ upload_data["fields"]["x-amz-security-token"],
383
+ ),
384
+ "policy": (None, upload_data["fields"]["policy"]),
385
+ "signature": (None, upload_data["fields"]["signature"]),
386
+ "file": (filename, file),
387
+ },
388
+ )
389
+
390
+ file_url: str = (
391
+ upload_data["url"] + upload_data["fields"]["key"].split("$")[0] + filename
392
+ )
393
+
394
+ self._write_file_url(filename, file_url)
395
+
396
+ return file_url
397
+
398
+ def threads(self, query: str = None, limit: int = None) -> list[dict]:
399
+ assert self.email, "not logged in"
400
+ assert self.finished, "already searching"
401
+
402
+ if not limit:
403
+ limit = 20
404
+ data: dict = {"version": "2.1", "source": "default", "limit": limit, "offset": 0}
405
+ if query:
406
+ data["search_term"] = query
407
+
408
+ self._start_interaction()
409
+ ws_message: str = f"{self.base + self.n}" + dumps(["list_ask_threads", data])
410
+
411
+ self.ws.send(ws_message)
412
+
413
+ while not self.finished or len(self.queue) != 0:
414
+ if len(self.queue) != 0:
415
+ return self.queue.pop(0)
416
+
417
+ def list_autosuggest(self, query: str = "", search_focus: str = "internet") -> list[dict]:
418
+ assert self.finished, "already searching"
419
+
420
+ self._start_interaction()
421
+ ws_message: str = (
422
+ f"{self.base + self.n}"
423
+ + dumps(
424
+ [
425
+ "list_autosuggest",
426
+ query,
427
+ {
428
+ "has_attachment": False,
429
+ "search_focus": search_focus,
430
+ "source": "default",
431
+ "version": "2.1",
432
+ },
433
+ ]
434
+ )
435
+ )
436
+
437
+ self.ws.send(ws_message)
438
+
439
+ while not self.finished or len(self.queue) != 0:
440
+ if len(self.queue) != 0:
441
+ return self.queue.pop(0)
442
+
443
+ def close(self) -> None:
444
+ self.ws.close()
445
+
446
+ if self.email:
447
+ with open(".perplexity_session", "r") as f:
448
+ perplexity_session: dict = loads(f.read())
449
+
450
+ perplexity_session[self.email] = self.session.cookies.get_dict()
451
+
452
+ with open(".perplexity_session", "w") as f:
453
+ f.write(dumps(perplexity_session))
82
454
 
83
455
  def ask(
84
456
  self,
@@ -87,7 +459,7 @@ class PERPLEXITY(Provider):
87
459
  raw: bool = False,
88
460
  optimizer: str = None,
89
461
  conversationally: bool = False,
90
- ) -> dict:
462
+ ) -> dict | Generator:
91
463
  """Chat with AI
92
464
 
93
465
  Args:
@@ -134,18 +506,19 @@ class PERPLEXITY(Provider):
134
506
  )
135
507
 
136
508
  def for_stream():
137
- for response in Perplexity().generate_answer(conversation_prompt):
138
- yield json.dumps(response) if raw else response
509
+ for response in self.search(conversation_prompt):
510
+ yield dumps(response) if raw else response
139
511
  self.last_response.update(response)
140
512
 
141
513
  self.conversation.update_chat_history(
142
- prompt,
143
- self.get_message(self.last_response),
514
+ prompt, self.get_message(self.last_response)
144
515
  )
145
516
 
146
517
  def for_non_stream():
147
- for _ in for_stream():
148
- pass
518
+ self.last_response.update(self.search_sync(conversation_prompt))
519
+ self.conversation.update_chat_history(
520
+ prompt, self.get_message(self.last_response)
521
+ )
149
522
  return self.last_response
150
523
 
151
524
  return for_stream() if stream else for_non_stream()
@@ -156,7 +529,7 @@ class PERPLEXITY(Provider):
156
529
  stream: bool = False,
157
530
  optimizer: str = None,
158
531
  conversationally: bool = False,
159
- ) -> str:
532
+ ) -> str | Generator:
160
533
  """Generate response `str`
161
534
  Args:
162
535
  prompt (str): Prompt to be send.
@@ -211,20 +584,17 @@ class PERPLEXITY(Provider):
211
584
  self.web_results.clear()
212
585
  update_web_results(web_results)
213
586
 
214
- return (
215
- text_str
216
- if self.quiet or not self.web_results
217
- else text_str + "\n\n# WEB-RESULTS\n\n" + yaml.dump(self.web_results)
218
- )
587
+ return text_str
219
588
 
220
589
  else:
221
- if str(response.get("expect_search_results")).lower() == "true":
222
- return (
223
- text_str
224
- if self.quiet
225
- else text_str
226
- + "\n\n# WEB-RESULTS\n\n"
227
- + yaml.dump(response.get("web_results"))
228
- )
229
- else:
230
- return text_str
590
+ return text_str
591
+
592
+
593
+ if __name__ == "__main__":
594
+ perplexity = Perplexity()
595
+ # Stream the response
596
+ response = perplexity.chat("What is the meaning of life?")
597
+ for chunk in response:
598
+ print(chunk, end="", flush=True)
599
+
600
+ perplexity.close()
@@ -19,10 +19,10 @@ import io
19
19
  import re
20
20
  import json
21
21
  import yaml
22
- from ..AIutel import Optimizers
23
- from ..AIutel import Conversation
24
- from ..AIutel import AwesomePrompts, sanitize_stream
25
- from ..AIbase import Provider, AsyncProvider
22
+ from webscout.AIutel import Optimizers
23
+ from webscout.AIutel import Conversation
24
+ from webscout.AIutel import AwesomePrompts, sanitize_stream
25
+ from webscout.AIbase import Provider, AsyncProvider
26
26
  from Helpingai_T2 import Perplexity
27
27
  from webscout import exceptions
28
28
  from typing import Any, AsyncGenerator, Dict
@@ -1004,4 +1004,5 @@ class AsyncPhindv2(AsyncProvider):
1004
1004
  response["choices"][0]["delta"].get("content")
1005
1005
  if response["choices"][0].get("finish_reason") is None
1006
1006
  else ""
1007
- )
1007
+ )
1008
+
@@ -175,4 +175,10 @@ class PIZZAGPT(Provider):
175
175
  assert isinstance(response, dict), "Response should be of dict data-type only"
176
176
  return response["text"]
177
177
  if __name__ == "__main__":
178
- print(PIZZAGPT().chat("hello"))
178
+ from rich import print
179
+
180
+ ai = PIZZAGPT()
181
+ # Stream the response
182
+ response = ai.chat(input(">>> "))
183
+ for chunk in response:
184
+ print(chunk, end="", flush=True)