webscout 6.0__py3-none-any.whl → 6.2__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 (63) hide show
  1. webscout/AIauto.py +77 -259
  2. webscout/Agents/Onlinesearcher.py +22 -10
  3. webscout/Agents/functioncall.py +2 -2
  4. webscout/Bard.py +21 -21
  5. webscout/Extra/autollama.py +37 -20
  6. webscout/Local/__init__.py +6 -7
  7. webscout/Local/formats.py +406 -194
  8. webscout/Local/model.py +1074 -477
  9. webscout/Local/samplers.py +108 -144
  10. webscout/Local/thread.py +251 -410
  11. webscout/Local/ui.py +401 -0
  12. webscout/Local/utils.py +338 -136
  13. webscout/Provider/Amigo.py +51 -38
  14. webscout/Provider/Deepseek.py +7 -6
  15. webscout/Provider/EDITEE.py +2 -2
  16. webscout/Provider/GPTWeb.py +1 -1
  17. webscout/Provider/Llama3.py +1 -1
  18. webscout/Provider/NinjaChat.py +200 -0
  19. webscout/Provider/OLLAMA.py +1 -1
  20. webscout/Provider/Perplexity.py +1 -1
  21. webscout/Provider/Reka.py +12 -5
  22. webscout/Provider/TTI/AIuncensored.py +103 -0
  23. webscout/Provider/TTI/Nexra.py +3 -3
  24. webscout/Provider/TTI/__init__.py +4 -2
  25. webscout/Provider/TTI/aiforce.py +2 -2
  26. webscout/Provider/TTI/imgninza.py +136 -0
  27. webscout/Provider/TTI/talkai.py +116 -0
  28. webscout/Provider/TeachAnything.py +0 -3
  29. webscout/Provider/Youchat.py +1 -1
  30. webscout/Provider/__init__.py +16 -12
  31. webscout/Provider/{ChatHub.py → aimathgpt.py} +72 -88
  32. webscout/Provider/cerebras.py +143 -123
  33. webscout/Provider/cleeai.py +1 -1
  34. webscout/Provider/felo_search.py +1 -1
  35. webscout/Provider/gaurish.py +207 -0
  36. webscout/Provider/geminiprorealtime.py +160 -0
  37. webscout/Provider/genspark.py +1 -1
  38. webscout/Provider/julius.py +8 -3
  39. webscout/Provider/learnfastai.py +1 -1
  40. webscout/Provider/{aigames.py → llmchat.py} +74 -84
  41. webscout/Provider/promptrefine.py +3 -1
  42. webscout/Provider/talkai.py +196 -0
  43. webscout/Provider/turboseek.py +3 -8
  44. webscout/Provider/tutorai.py +1 -1
  45. webscout/__init__.py +2 -43
  46. webscout/exceptions.py +5 -1
  47. webscout/tempid.py +4 -73
  48. webscout/utils.py +3 -0
  49. webscout/version.py +1 -1
  50. webscout/webai.py +1 -1
  51. webscout/webscout_search.py +154 -123
  52. {webscout-6.0.dist-info → webscout-6.2.dist-info}/METADATA +164 -245
  53. {webscout-6.0.dist-info → webscout-6.2.dist-info}/RECORD +57 -55
  54. webscout/Local/rawdog.py +0 -946
  55. webscout/Provider/BasedGPT.py +0 -214
  56. webscout/Provider/TTI/amigo.py +0 -148
  57. webscout/Provider/bixin.py +0 -264
  58. webscout/Provider/xdash.py +0 -182
  59. webscout/websx_search.py +0 -19
  60. {webscout-6.0.dist-info → webscout-6.2.dist-info}/LICENSE.md +0 -0
  61. {webscout-6.0.dist-info → webscout-6.2.dist-info}/WHEEL +0 -0
  62. {webscout-6.0.dist-info → webscout-6.2.dist-info}/entry_points.txt +0 -0
  63. {webscout-6.0.dist-info → webscout-6.2.dist-info}/top_level.txt +0 -0
webscout/Local/ui.py ADDED
@@ -0,0 +1,401 @@
1
+ import os
2
+ import sys
3
+ import json
4
+ import base64
5
+
6
+ from httpx import stream
7
+
8
+ from .model import Model
9
+
10
+ from .utils import (
11
+ InferenceLock,
12
+ download_model,
13
+ print_verbose,
14
+ SPECIAL_STYLE,
15
+ assert_type,
16
+ ERROR_STYLE,
17
+ USER_STYLE,
18
+ BOT_STYLE,
19
+ RESET_ALL
20
+ )
21
+
22
+ from .thread import Thread
23
+ from cryptography import x509
24
+ from cryptography.x509.oid import NameOID
25
+ from datetime import datetime, timedelta, UTC
26
+ from cryptography.hazmat.primitives import hashes
27
+ from cryptography.hazmat.primitives import serialization
28
+ from cryptography.hazmat.primitives.asymmetric import rsa
29
+ from flask import Flask, logging, render_template, request, Response
30
+ from cryptography.hazmat.primitives.serialization import Encoding
31
+
32
+ # Color codes for console output
33
+ YELLOW = SPECIAL_STYLE
34
+ GREEN = USER_STYLE
35
+ RED = ERROR_STYLE
36
+ RESET = RESET_ALL
37
+ BLUE = BOT_STYLE
38
+
39
+ # Warning message for WebUI security
40
+ WARNING = f"""{RED}
41
+ ################################################################################
42
+ {RESET}
43
+
44
+ PLEASE KEEP IN MIND
45
+
46
+ The webscout.Local WebUI is not guaranteed to be secure.
47
+
48
+ It is not intended to be exposed to the internet.
49
+
50
+ If you expose the WebUI to the internet, you do so at your own risk.
51
+
52
+ YOU HAVE BEEN WARNED!
53
+
54
+ {RED}
55
+ ################################################################################
56
+ {RESET}"""
57
+
58
+ # SSL certificate generation warning
59
+ SSL_CERT_FIRST_TIME_WARNING = (
60
+ f"{YELLOW}You have just generated a new self-signed SSL certificate and "
61
+ f"key. Your browser will probably warn you about an untrusted "
62
+ f"certificate. This is expected, and you may safely proceed to the WebUI. "
63
+ f"Subsequent WebUI sessions will reuse this SSL certificate.{RESET}"
64
+ )
65
+
66
+ # Constants
67
+ ASSETS_FOLDER = os.path.join(os.path.dirname(__file__), "assets")
68
+ MAX_LENGTH_INPUT = 100_000 # Maximum input length (characters)
69
+
70
+ def generate_self_signed_ssl_cert() -> None:
71
+ """Generates a self-signed SSL certificate and key."""
72
+ private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
73
+ public_key = private_key.public_key()
74
+
75
+ # Certificate details
76
+ name = x509.Name([
77
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "XY"),
78
+ x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "DUMMY_STATE"),
79
+ x509.NameAttribute(NameOID.LOCALITY_NAME, "DUMMY_LOCALITY"),
80
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "EZLLAMA LLC"),
81
+ x509.NameAttribute(NameOID.COMMON_NAME, "localhost"),
82
+ ])
83
+
84
+ # Certificate builder
85
+ builder = x509.CertificateBuilder(
86
+ subject_name=name,
87
+ issuer_name=name,
88
+ public_key=public_key,
89
+ serial_number=x509.random_serial_number(),
90
+ not_valid_before=datetime.now(tz=UTC),
91
+ not_valid_after=datetime.now(tz=UTC) + timedelta(days=36500),
92
+ )
93
+ builder = builder.add_extension(
94
+ x509.SubjectAlternativeName([x509.DNSName("localhost")]),
95
+ critical=False,
96
+ )
97
+
98
+ # Sign and save the certificate and key
99
+ certificate = builder.sign(private_key=private_key, algorithm=hashes.SHA256())
100
+ with open(f"{ASSETS_FOLDER}/key.pem", "wb") as f:
101
+ f.write(private_key.private_bytes(
102
+ encoding=serialization.Encoding.PEM,
103
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
104
+ encryption_algorithm=serialization.NoEncryption(),
105
+ ))
106
+ with open(f"{ASSETS_FOLDER}/cert.pem", "wb") as f:
107
+ f.write(certificate.public_bytes(Encoding.PEM))
108
+
109
+ def check_for_ssl_cert() -> bool:
110
+ """Checks if SSL certificate and key exist."""
111
+ return all(os.path.exists(f"{ASSETS_FOLDER}/{file}.pem") for file in ["cert", "key"])
112
+
113
+ def newline() -> None:
114
+ """Prints a newline to stderr."""
115
+ print("", end="\n", file=sys.stderr, flush=True)
116
+
117
+ def encode(text: str) -> str:
118
+ """Encodes a string to base64."""
119
+ return base64.b64encode(text.encode("utf-8")).decode("utf-8")
120
+
121
+ def decode(text: str) -> str:
122
+ """Decodes a base64 encoded string."""
123
+ return base64.b64decode(text).decode("utf-8")
124
+
125
+ def assert_max_length(text: str) -> None:
126
+ """Asserts that the length of the text is within the allowed limit."""
127
+ if len(text) > MAX_LENGTH_INPUT:
128
+ raise AssertionError(
129
+ f"Input text exceeds maximum length of {MAX_LENGTH_INPUT} characters."
130
+ )
131
+
132
+ def _print_inference_string(text: str) -> None:
133
+ """Prints the inference string to stderr."""
134
+ print(
135
+ f"{'#' * 80}\n"
136
+ f"{YELLOW}'''{RESET}{text}{YELLOW}'''{RESET}\n"
137
+ f"{'#' * 80}",
138
+ file=sys.stderr,
139
+ flush=True
140
+ )
141
+
142
+
143
+ class WebUI:
144
+ """
145
+ Represents the webscout.Local WebUI server.
146
+ """
147
+
148
+ def __init__(self, thread: Thread):
149
+ """
150
+ Initializes the WebUI instance.
151
+
152
+ Args:
153
+ thread (Thread): The Thread instance to use for the WebUI.
154
+ """
155
+ assert_type(thread, Thread, "thread", "WebUI")
156
+ self.thread = thread
157
+ self.lock = InferenceLock()
158
+ self._cancel_flag = False
159
+
160
+ self.app = Flask(
161
+ __name__,
162
+ static_folder=ASSETS_FOLDER,
163
+ template_folder=ASSETS_FOLDER,
164
+ static_url_path="",
165
+ )
166
+
167
+ self._log_host = None
168
+ self._log_port = None
169
+
170
+ def log(self, text: str) -> None:
171
+ """Logs a message to the console."""
172
+ if self._log_host is None or self._log_port is None:
173
+ print_verbose(text)
174
+ else:
175
+ print(
176
+ f"webscout.Local: WebUI @ "
177
+ f"{YELLOW}{self._log_host}{RESET}:"
178
+ f"{YELLOW}{self._log_port}{RESET}: {text}",
179
+ file=sys.stderr,
180
+ flush=True
181
+ )
182
+
183
+ def _get_context_string(self) -> str:
184
+ """Returns a string representing the current context usage."""
185
+ thread_len_tokens = self.thread.len_messages()
186
+ max_ctx_len = self.thread.model.context_length
187
+ return f"{thread_len_tokens} / {max_ctx_len} tokens used"
188
+
189
+ def start(self, host: str, port: int = 8080, ssl: bool = False) -> None:
190
+ """
191
+ Starts the WebUI server.
192
+
193
+ Args:
194
+ host (str): The hostname or IP address to bind the server to.
195
+ port (int, optional): The port to listen on. Defaults to 8080.
196
+ ssl (bool, optional): Whether to use SSL. Defaults to False.
197
+ """
198
+ print(WARNING, file=sys.stderr, flush=True)
199
+
200
+ assert_type(host, str, "host", "WebUI.start")
201
+ assert_type(port, int, "port", "WebUI.start")
202
+
203
+ self._log_host = host
204
+ self._log_port = port
205
+
206
+ self.log(f"Starting WebUI instance:")
207
+ self.log(f" thread.uuid == {self.thread.uuid}")
208
+ self.log(f" host == {host}")
209
+ self.log(f" port == {port}")
210
+ self.log(f" ssl (HTTPS) == {ssl}")
211
+
212
+ if ssl:
213
+ if not check_for_ssl_cert():
214
+ self.log("Generating self-signed SSL certificate...")
215
+ generate_self_signed_ssl_cert()
216
+ print_verbose(SSL_CERT_FIRST_TIME_WARNING)
217
+ else:
218
+ self.log("Reusing previously generated SSL certificate.")
219
+
220
+ # Flask route definitions
221
+ @self.app.route("/")
222
+ def home():
223
+ return render_template("index.html")
224
+
225
+ @self.app.route("/convo", methods=["GET"])
226
+ def convo():
227
+ msgs_dict = {i: {encode(msg["role"]): encode(msg["content"])}
228
+ for i, msg in enumerate(self.thread.messages)}
229
+ json_convo = json.dumps(msgs_dict)
230
+ return json_convo, 200, {"ContentType": "application/json"}
231
+
232
+ @self.app.route("/cancel", methods=["POST"])
233
+ def cancel():
234
+ newline()
235
+ self.log("Hit cancel endpoint - flag is set.")
236
+ self._cancel_flag = True
237
+ return "", 200
238
+
239
+ @self.app.route("/submit", methods=["POST"])
240
+ def submit():
241
+ self.log("Hit submit endpoint.")
242
+ prompt = decode(request.data)
243
+ assert_max_length(prompt)
244
+
245
+ if not prompt:
246
+ self.log("Empty prompt submitted. Ignoring.")
247
+ return "", 200
248
+
249
+ # Pass the stream variable to the generate function
250
+ def generate(stream=stream):
251
+ with self.lock:
252
+ self.thread.add_message("user", prompt)
253
+ print(f"{GREEN}{prompt}{RESET}", file=sys.stderr)
254
+ inf_str = self.thread.inference_str_from_messages()
255
+ _print_inference_string(inf_str)
256
+
257
+ if stream:
258
+ token_generator = self.thread.model.stream(
259
+ inf_str,
260
+ stops=self.thread.format["stops"],
261
+ sampler=self.thread.sampler
262
+ )
263
+ response = ""
264
+ for token in token_generator:
265
+ if self._cancel_flag:
266
+ print(file=sys.stderr)
267
+ self.log("Canceling generation from /submit.")
268
+ self._cancel_flag = False
269
+ return "", 418 # I'm a teapot
270
+
271
+ tok_text = token['choices'][0]['text']
272
+ response += tok_text
273
+ print(f'{BLUE}{tok_text}{RESET}', end='', flush=True, file=sys.stderr)
274
+ yield encode(tok_text) + "\n"
275
+ else:
276
+ response = self.thread.model.generate(
277
+ inf_str,
278
+ stops=self.thread.format["stops"],
279
+ sampler=self.thread.sampler
280
+ )
281
+ # Simulate streaming by yielding chunks of the content
282
+ chunk_size = self.stream_chunk_size
283
+ for i in range(0, len(response), chunk_size):
284
+ chunk = response[i:i + chunk_size]
285
+ yield encode(chunk) + "\n"
286
+
287
+ self._cancel_flag = False
288
+ newline()
289
+ self.thread.add_message("bot", response)
290
+
291
+ return Response(generate(), mimetype="text/plain")
292
+
293
+ @self.app.route("/reset", methods=["POST"])
294
+ def reset():
295
+ self.thread.reset()
296
+ self.log("Thread reset.")
297
+ return "", 200
298
+
299
+ @self.app.route("/get_context_string", methods=["GET"])
300
+ def get_context_string():
301
+ return json.dumps({"text": encode(self._get_context_string())}), 200, {"ContentType": "application/json"}
302
+
303
+ @self.app.route("/remove", methods=["POST"])
304
+ def remove():
305
+ if len(self.thread.messages) > 1: # Prevent deleting the system message
306
+ self.thread.messages.pop(-1)
307
+ self.log("Removed last message.")
308
+ return "", 200
309
+ else:
310
+ self.log("No previous message to remove.")
311
+ return "", 418 # I'm a teapot
312
+
313
+ @self.app.route("/trigger", methods=["POST"])
314
+ def trigger():
315
+ self.log("Hit trigger endpoint.")
316
+ prompt = decode(request.data)
317
+ assert_max_length(prompt)
318
+
319
+ if prompt:
320
+ self.log(f"Trigger with prompt: {prompt!r}")
321
+ else:
322
+ self.log("Trigger without prompt.")
323
+
324
+ # Pass stream to the generate function
325
+ def generate(stream=stream):
326
+ with self.lock:
327
+ inf_str = self.thread.inference_str_from_messages() + prompt
328
+ _print_inference_string(inf_str)
329
+
330
+ if stream:
331
+ token_generator = self.thread.model.stream(
332
+ inf_str,
333
+ stops=self.thread.format["stops"],
334
+ sampler=self.thread.sampler
335
+ )
336
+ response = ""
337
+ for token in token_generator:
338
+ if self._cancel_flag:
339
+ print(file=sys.stderr)
340
+ self.log("Canceling generation from /trigger.")
341
+ self._cancel_flag = False
342
+ return "", 418 # I'm a teapot
343
+
344
+ tok_text = token["choices"][0]["text"]
345
+ response += tok_text
346
+ print(f"{BLUE}{tok_text}{RESET}", end='', flush=True)
347
+ yield encode(tok_text) + "\n"
348
+ else:
349
+ response = self.thread.model.generate(
350
+ inf_str,
351
+ stops=self.thread.format["stops"],
352
+ sampler=self.thread.sampler
353
+ )
354
+ # Simulate streaming by yielding chunks of the content
355
+ chunk_size = self.stream_chunk_size
356
+ for i in range(0, len(response), chunk_size):
357
+ chunk = response[i:i + chunk_size]
358
+ yield encode(chunk) + "\n"
359
+
360
+ self._cancel_flag = False
361
+ print("", file=sys.stderr)
362
+ self.thread.add_message("bot", prompt + response)
363
+
364
+ return Response(generate(), mimetype="text/plain")
365
+
366
+ @self.app.route("/summarize", methods=["GET"])
367
+ def summarize():
368
+ with self.lock:
369
+ summary = self.thread.summarize()
370
+ self.log(f"Generated summary: {BLUE}{summary!r}{RESET}")
371
+ return encode(summary), 200, {"ContentType": "text/plain"}
372
+
373
+ if not self.thread.model.is_loaded():
374
+ self.log("Loading model...")
375
+ self.thread.model.load()
376
+ else:
377
+ self.log("Model is already loaded.")
378
+
379
+ self.log("Warming up thread...")
380
+ self.thread.warmup()
381
+
382
+ self.log("Running Flask app...")
383
+ try:
384
+ self.app.run(
385
+ host=host,
386
+ port=port,
387
+ ssl_context=(
388
+ (f"{ASSETS_FOLDER}/cert.pem", f"{ASSETS_FOLDER}/key.pem")
389
+ if ssl
390
+ else None
391
+ ),
392
+ )
393
+ except Exception as exc:
394
+ newline()
395
+ self.log(f"{RED}Exception in Flask: {exc}{RESET}")
396
+ raise exc
397
+ else:
398
+ newline()
399
+ self._log_host = None
400
+ self._log_port = None
401
+