llms-py 3.0.0b7__py3-none-any.whl → 3.0.0b9__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.
- llms/__pycache__/main.cpython-314.pyc +0 -0
- llms/extensions/analytics/ui/index.mjs +51 -162
- llms/extensions/app/__init__.py +519 -0
- llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
- llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
- llms/extensions/app/db.py +643 -0
- llms/extensions/app/db_manager.py +195 -0
- llms/extensions/app/requests.json +9073 -0
- llms/extensions/app/threads.json +15290 -0
- llms/{ui/modules/threads → extensions/app/ui}/Recents.mjs +82 -55
- llms/{ui/modules/threads → extensions/app/ui}/index.mjs +78 -9
- llms/extensions/app/ui/threadStore.mjs +407 -0
- llms/extensions/core_tools/__init__.py +272 -32
- llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
- llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
- llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +344 -0
- llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +9884 -0
- llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
- llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
- llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
- llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
- llms/extensions/core_tools/ui/index.mjs +650 -0
- llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
- llms/extensions/gallery/db.py +4 -4
- llms/extensions/gallery/ui/index.mjs +2 -1
- llms/extensions/katex/__init__.py +6 -0
- llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/katex/ui/README.md +125 -0
- llms/extensions/katex/ui/contrib/auto-render.js +338 -0
- llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
- llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
- llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
- llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
- llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
- llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
- llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
- llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- llms/extensions/katex/ui/index.mjs +92 -0
- llms/extensions/katex/ui/katex-swap.css +1230 -0
- llms/extensions/katex/ui/katex-swap.min.css +1 -0
- llms/extensions/katex/ui/katex.css +1230 -0
- llms/extensions/katex/ui/katex.js +19080 -0
- llms/extensions/katex/ui/katex.min.css +1 -0
- llms/extensions/katex/ui/katex.min.js +1 -0
- llms/extensions/katex/ui/katex.min.mjs +1 -0
- llms/extensions/katex/ui/katex.mjs +18547 -0
- llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
- llms/extensions/providers/anthropic.py +44 -1
- llms/extensions/system_prompts/ui/index.mjs +2 -1
- llms/extensions/tools/__init__.py +5 -0
- llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/tools/ui/index.mjs +8 -8
- llms/index.html +26 -38
- llms/llms.json +4 -1
- llms/main.py +492 -103
- llms/ui/App.mjs +2 -3
- llms/ui/ai.mjs +29 -13
- llms/ui/app.css +255 -289
- llms/ui/ctx.mjs +84 -6
- llms/ui/index.mjs +4 -6
- llms/ui/lib/vue.min.mjs +10 -9
- llms/ui/lib/vue.mjs +1796 -1635
- llms/ui/markdown.mjs +4 -2
- llms/ui/modules/chat/ChatBody.mjs +90 -86
- llms/ui/modules/chat/HomeTools.mjs +0 -242
- llms/ui/modules/chat/index.mjs +103 -170
- llms/ui/modules/model-selector.mjs +2 -2
- llms/ui/tailwind.input.css +35 -1
- llms/ui/utils.mjs +12 -0
- {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b9.dist-info}/METADATA +1 -1
- llms_py-3.0.0b9.dist-info/RECORD +198 -0
- llms/ui/modules/threads/threadStore.mjs +0 -640
- llms_py-3.0.0b7.dist-info/RECORD +0 -80
- {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b9.dist-info}/WHEEL +0 -0
- {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b9.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b9.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b9.dist-info}/top_level.txt +0 -0
llms/main.py
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import argparse
|
|
10
10
|
import asyncio
|
|
11
11
|
import base64
|
|
12
|
+
import contextlib
|
|
12
13
|
import hashlib
|
|
13
14
|
import importlib.util
|
|
14
15
|
import inspect
|
|
@@ -40,7 +41,7 @@ try:
|
|
|
40
41
|
except ImportError:
|
|
41
42
|
HAS_PIL = False
|
|
42
43
|
|
|
43
|
-
VERSION = "3.0.
|
|
44
|
+
VERSION = "3.0.0b9"
|
|
44
45
|
_ROOT = None
|
|
45
46
|
DEBUG = os.getenv("DEBUG") == "1"
|
|
46
47
|
MOCK = os.getenv("MOCK") == "1"
|
|
@@ -324,6 +325,15 @@ def convert_image_if_needed(image_bytes, mimetype="image/png"):
|
|
|
324
325
|
return image_bytes, mimetype
|
|
325
326
|
|
|
326
327
|
|
|
328
|
+
def to_content(result):
|
|
329
|
+
if isinstance(result, (str, int, float, bool)):
|
|
330
|
+
return str(result)
|
|
331
|
+
elif isinstance(result, (list, set, tuple, dict)):
|
|
332
|
+
return json.dumps(result)
|
|
333
|
+
else:
|
|
334
|
+
return str(result)
|
|
335
|
+
|
|
336
|
+
|
|
327
337
|
def function_to_tool_definition(func):
|
|
328
338
|
type_hints = get_type_hints(func)
|
|
329
339
|
signature = inspect.signature(func)
|
|
@@ -332,11 +342,11 @@ def function_to_tool_definition(func):
|
|
|
332
342
|
for name, param in signature.parameters.items():
|
|
333
343
|
param_type = type_hints.get(name, str)
|
|
334
344
|
param_type_name = "string"
|
|
335
|
-
if param_type
|
|
345
|
+
if param_type is int:
|
|
336
346
|
param_type_name = "integer"
|
|
337
|
-
elif param_type
|
|
347
|
+
elif param_type is float:
|
|
338
348
|
param_type_name = "number"
|
|
339
|
-
elif param_type
|
|
349
|
+
elif param_type is bool:
|
|
340
350
|
param_type_name = "boolean"
|
|
341
351
|
|
|
342
352
|
parameters["properties"][name] = {"type": param_type_name}
|
|
@@ -484,6 +494,92 @@ async def process_chat(chat, provider_id=None):
|
|
|
484
494
|
return chat
|
|
485
495
|
|
|
486
496
|
|
|
497
|
+
def image_ext_from_mimetype(mimetype, default="png"):
|
|
498
|
+
if "/" in mimetype:
|
|
499
|
+
_ext = mimetypes.guess_extension(mimetype)
|
|
500
|
+
if _ext:
|
|
501
|
+
return _ext.lstrip(".")
|
|
502
|
+
return default
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def audio_ext_from_format(format, default="mp3"):
|
|
506
|
+
if format == "mpeg":
|
|
507
|
+
return "mp3"
|
|
508
|
+
return format or default
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def file_ext_from_mimetype(mimetype, default="pdf"):
|
|
512
|
+
if "/" in mimetype:
|
|
513
|
+
_ext = mimetypes.guess_extension(mimetype)
|
|
514
|
+
if _ext:
|
|
515
|
+
return _ext.lstrip(".")
|
|
516
|
+
return default
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def cache_message_inline_data(m):
|
|
520
|
+
"""
|
|
521
|
+
Replaces and caches any inline data URIs in the message content.
|
|
522
|
+
"""
|
|
523
|
+
if "content" not in m:
|
|
524
|
+
return
|
|
525
|
+
|
|
526
|
+
content = m["content"]
|
|
527
|
+
if isinstance(content, list):
|
|
528
|
+
for item in content:
|
|
529
|
+
if item.get("type") == "image_url":
|
|
530
|
+
image_url = item.get("image_url", {})
|
|
531
|
+
url = image_url.get("url")
|
|
532
|
+
if url and url.startswith("data:"):
|
|
533
|
+
# Extract base64 and mimetype
|
|
534
|
+
try:
|
|
535
|
+
header, base64_data = url.split(";base64,")
|
|
536
|
+
# header is like "data:image/png"
|
|
537
|
+
ext = image_ext_from_mimetype(header.split(":")[1])
|
|
538
|
+
filename = f"image.{ext}" # Hash will handle uniqueness
|
|
539
|
+
|
|
540
|
+
cache_url, _ = save_image_to_cache(base64_data, filename, {}, ignore_info=True)
|
|
541
|
+
image_url["url"] = cache_url
|
|
542
|
+
except Exception as e:
|
|
543
|
+
_log(f"Error caching inline image: {e}")
|
|
544
|
+
|
|
545
|
+
elif item.get("type") == "input_audio":
|
|
546
|
+
input_audio = item.get("input_audio", {})
|
|
547
|
+
data = input_audio.get("data")
|
|
548
|
+
if data:
|
|
549
|
+
# Handle data URI or raw base64
|
|
550
|
+
base64_data = data
|
|
551
|
+
if data.startswith("data:"):
|
|
552
|
+
with contextlib.suppress(ValueError):
|
|
553
|
+
header, base64_data = data.split(";base64,")
|
|
554
|
+
|
|
555
|
+
fmt = audio_ext_from_format(input_audio.get("format"))
|
|
556
|
+
filename = f"audio.{fmt}"
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
cache_url, _ = save_bytes_to_cache(base64_data, filename, {}, ignore_info=True)
|
|
560
|
+
input_audio["data"] = cache_url
|
|
561
|
+
except Exception as e:
|
|
562
|
+
_log(f"Error caching inline audio: {e}")
|
|
563
|
+
|
|
564
|
+
elif item.get("type") == "file":
|
|
565
|
+
file_info = item.get("file", {})
|
|
566
|
+
file_data = file_info.get("file_data")
|
|
567
|
+
if file_data and file_data.startswith("data:"):
|
|
568
|
+
try:
|
|
569
|
+
header, base64_data = file_data.split(";base64,")
|
|
570
|
+
mimetype = header.split(":")[1]
|
|
571
|
+
# Try to get extension from filename if available, else mimetype
|
|
572
|
+
filename = file_info.get("filename", "file")
|
|
573
|
+
if "." not in filename:
|
|
574
|
+
ext = file_ext_from_mimetype(mimetype)
|
|
575
|
+
filename = f"{filename}.{ext}"
|
|
576
|
+
|
|
577
|
+
cache_url, _ = save_bytes_to_cache(base64_data, filename, {}, ignore_info=True)
|
|
578
|
+
file_info["file_data"] = cache_url
|
|
579
|
+
except Exception as e:
|
|
580
|
+
_log(f"Error caching inline file: {e}")
|
|
581
|
+
|
|
582
|
+
|
|
487
583
|
class HTTPError(Exception):
|
|
488
584
|
def __init__(self, status, reason, body, headers=None):
|
|
489
585
|
self.status = status
|
|
@@ -493,7 +589,7 @@ class HTTPError(Exception):
|
|
|
493
589
|
super().__init__(f"HTTP {status} {reason}")
|
|
494
590
|
|
|
495
591
|
|
|
496
|
-
def save_bytes_to_cache(base64_data, filename, file_info):
|
|
592
|
+
def save_bytes_to_cache(base64_data, filename, file_info, ignore_info=False):
|
|
497
593
|
ext = filename.split(".")[-1]
|
|
498
594
|
mimetype = get_file_mime_type(filename)
|
|
499
595
|
content = base64.b64decode(base64_data) if isinstance(base64_data, str) else base64_data
|
|
@@ -507,6 +603,14 @@ def save_bytes_to_cache(base64_data, filename, file_info):
|
|
|
507
603
|
full_path = get_cache_path(relative_path)
|
|
508
604
|
url = f"/~cache/{relative_path}"
|
|
509
605
|
|
|
606
|
+
# if file and its .info.json already exists, return it
|
|
607
|
+
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
608
|
+
if os.path.exists(full_path) and os.path.exists(info_path):
|
|
609
|
+
_dbg(f"Cached bytes exists: {relative_path}")
|
|
610
|
+
if ignore_info:
|
|
611
|
+
return url, None
|
|
612
|
+
return url, json.load(open(info_path))
|
|
613
|
+
|
|
510
614
|
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
511
615
|
|
|
512
616
|
with open(full_path, "wb") as f:
|
|
@@ -520,12 +624,19 @@ def save_bytes_to_cache(base64_data, filename, file_info):
|
|
|
520
624
|
}
|
|
521
625
|
info.update(file_info)
|
|
522
626
|
|
|
627
|
+
# Save metadata
|
|
628
|
+
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
629
|
+
with open(info_path, "w") as f:
|
|
630
|
+
json.dump(info, f)
|
|
631
|
+
|
|
632
|
+
_dbg(f"Saved cached bytes and info: {relative_path}")
|
|
633
|
+
|
|
523
634
|
g_app.on_cache_saved_filters({"url": url, "info": info})
|
|
524
635
|
|
|
525
636
|
return url, info
|
|
526
637
|
|
|
527
638
|
|
|
528
|
-
def save_image_to_cache(base64_data, filename, image_info):
|
|
639
|
+
def save_image_to_cache(base64_data, filename, image_info, ignore_info=False):
|
|
529
640
|
ext = filename.split(".")[-1]
|
|
530
641
|
mimetype = get_file_mime_type(filename)
|
|
531
642
|
content = base64.b64decode(base64_data) if isinstance(base64_data, str) else base64_data
|
|
@@ -542,6 +653,9 @@ def save_image_to_cache(base64_data, filename, image_info):
|
|
|
542
653
|
# if file and its .info.json already exists, return it
|
|
543
654
|
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
544
655
|
if os.path.exists(full_path) and os.path.exists(info_path):
|
|
656
|
+
_dbg(f"Saved image exists: {relative_path}")
|
|
657
|
+
if ignore_info:
|
|
658
|
+
return url, None
|
|
545
659
|
return url, json.load(open(info_path))
|
|
546
660
|
|
|
547
661
|
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
@@ -576,6 +690,8 @@ def save_image_to_cache(base64_data, filename, image_info):
|
|
|
576
690
|
with open(info_path, "w") as f:
|
|
577
691
|
json.dump(info, f)
|
|
578
692
|
|
|
693
|
+
_dbg(f"Saved image and info: {relative_path}")
|
|
694
|
+
|
|
579
695
|
g_app.on_cache_saved_filters({"url": url, "info": info})
|
|
580
696
|
|
|
581
697
|
return url, info
|
|
@@ -610,6 +726,21 @@ def chat_to_prompt(chat):
|
|
|
610
726
|
return prompt
|
|
611
727
|
|
|
612
728
|
|
|
729
|
+
def chat_to_system_prompt(chat):
|
|
730
|
+
if "messages" in chat:
|
|
731
|
+
for message in chat["messages"]:
|
|
732
|
+
if message["role"] == "system":
|
|
733
|
+
# if content is string
|
|
734
|
+
if isinstance(message["content"], str):
|
|
735
|
+
return message["content"]
|
|
736
|
+
elif isinstance(message["content"], list):
|
|
737
|
+
# if content is array of objects
|
|
738
|
+
for part in message["content"]:
|
|
739
|
+
if part["type"] == "text":
|
|
740
|
+
return part["text"]
|
|
741
|
+
return None
|
|
742
|
+
|
|
743
|
+
|
|
613
744
|
def chat_to_username(chat):
|
|
614
745
|
if "metadata" in chat and "user" in chat["metadata"]:
|
|
615
746
|
return chat["metadata"]["user"]
|
|
@@ -632,6 +763,34 @@ def last_user_prompt(chat):
|
|
|
632
763
|
return prompt
|
|
633
764
|
|
|
634
765
|
|
|
766
|
+
def chat_response_to_message(openai_response):
|
|
767
|
+
"""
|
|
768
|
+
Returns an assistant message from the OpenAI Response.
|
|
769
|
+
Handles normalizing text, image, and audio responses into the message content.
|
|
770
|
+
"""
|
|
771
|
+
timestamp = int(time.time() * 1000) # openai_response.get("created")
|
|
772
|
+
choices = openai_response
|
|
773
|
+
if isinstance(openai_response, dict) and "choices" in openai_response:
|
|
774
|
+
choices = openai_response["choices"]
|
|
775
|
+
|
|
776
|
+
choice = choices[0] if isinstance(choices, list) and choices else choices
|
|
777
|
+
|
|
778
|
+
if isinstance(choice, str):
|
|
779
|
+
return {"role": "assistant", "content": choice, "timestamp": timestamp}
|
|
780
|
+
|
|
781
|
+
if isinstance(choice, dict):
|
|
782
|
+
message = choice.get("message", choice)
|
|
783
|
+
else:
|
|
784
|
+
return {"role": "assistant", "content": str(choice), "timestamp": timestamp}
|
|
785
|
+
|
|
786
|
+
# Ensure message is a dict
|
|
787
|
+
if not isinstance(message, dict):
|
|
788
|
+
return {"role": "assistant", "content": message, "timestamp": timestamp}
|
|
789
|
+
|
|
790
|
+
message.update({"timestamp": timestamp})
|
|
791
|
+
return message
|
|
792
|
+
|
|
793
|
+
|
|
635
794
|
def to_file_info(chat, info=None, response=None):
|
|
636
795
|
prompt = last_user_prompt(chat)
|
|
637
796
|
ret = info or {}
|
|
@@ -809,13 +968,17 @@ class OpenAiCompatible:
|
|
|
809
968
|
if not self.models:
|
|
810
969
|
await self.load_models()
|
|
811
970
|
|
|
812
|
-
def
|
|
971
|
+
def model_info(self, model):
|
|
813
972
|
provider_model = self.provider_model(model) or model
|
|
814
973
|
for model_id, model_info in self.models.items():
|
|
815
974
|
if model_id.lower() == provider_model.lower():
|
|
816
|
-
return model_info
|
|
975
|
+
return model_info
|
|
817
976
|
return None
|
|
818
977
|
|
|
978
|
+
def model_cost(self, model):
|
|
979
|
+
model_info = self.model_info(model)
|
|
980
|
+
return model_info.get("cost") if model_info else None
|
|
981
|
+
|
|
819
982
|
def provider_model(self, model):
|
|
820
983
|
# convert model to lowercase for case-insensitive comparison
|
|
821
984
|
model_lower = model.lower()
|
|
@@ -877,7 +1040,7 @@ class OpenAiCompatible:
|
|
|
877
1040
|
chat["model"] = self.provider_model(chat["model"]) or chat["model"]
|
|
878
1041
|
|
|
879
1042
|
if "modalities" in chat:
|
|
880
|
-
for modality in chat
|
|
1043
|
+
for modality in chat.get("modalities", []):
|
|
881
1044
|
# use default implementation for text modalities
|
|
882
1045
|
if modality == "text":
|
|
883
1046
|
continue
|
|
@@ -1108,8 +1271,12 @@ def api_providers():
|
|
|
1108
1271
|
return ret
|
|
1109
1272
|
|
|
1110
1273
|
|
|
1274
|
+
def to_error_message(e):
|
|
1275
|
+
return str(e)
|
|
1276
|
+
|
|
1277
|
+
|
|
1111
1278
|
def to_error_response(e, stacktrace=False):
|
|
1112
|
-
status = {"errorCode": "Error", "message":
|
|
1279
|
+
status = {"errorCode": "Error", "message": to_error_message(e)}
|
|
1113
1280
|
if stacktrace:
|
|
1114
1281
|
status["stackTrace"] = traceback.format_exc()
|
|
1115
1282
|
return {"responseStatus": status}
|
|
@@ -1122,6 +1289,14 @@ def create_error_response(message, error_code="Error", stack_trace=None):
|
|
|
1122
1289
|
return ret
|
|
1123
1290
|
|
|
1124
1291
|
|
|
1292
|
+
def should_cancel_thread(context):
|
|
1293
|
+
ret = context.get("cancelled", False)
|
|
1294
|
+
if ret:
|
|
1295
|
+
thread_id = context.get("threadId")
|
|
1296
|
+
_dbg(f"Thread cancelled {thread_id}")
|
|
1297
|
+
return ret
|
|
1298
|
+
|
|
1299
|
+
|
|
1125
1300
|
def g_chat_request(template=None, text=None, model=None, system_prompt=None):
|
|
1126
1301
|
chat_template = g_config["defaults"].get(template or "text")
|
|
1127
1302
|
if not chat_template:
|
|
@@ -1150,28 +1325,51 @@ def g_chat_request(template=None, text=None, model=None, system_prompt=None):
|
|
|
1150
1325
|
|
|
1151
1326
|
|
|
1152
1327
|
async def g_chat_completion(chat, context=None):
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1328
|
+
try:
|
|
1329
|
+
model = chat.get("model")
|
|
1330
|
+
if not model:
|
|
1331
|
+
raise Exception("Model not specified")
|
|
1332
|
+
|
|
1333
|
+
if context is None:
|
|
1334
|
+
context = {"chat": chat, "tools": "all"}
|
|
1158
1335
|
|
|
1336
|
+
# get first provider that has the model
|
|
1337
|
+
candidate_providers = [name for name, provider in g_handlers.items() if provider.provider_model(model)]
|
|
1338
|
+
if len(candidate_providers) == 0:
|
|
1339
|
+
raise (Exception(f"Model {model} not found"))
|
|
1340
|
+
except Exception as e:
|
|
1341
|
+
await g_app.on_chat_error(e, context or {"chat": chat})
|
|
1342
|
+
raise e
|
|
1343
|
+
|
|
1344
|
+
started_at = time.time()
|
|
1159
1345
|
first_exception = None
|
|
1346
|
+
provider_name = "Unknown"
|
|
1160
1347
|
for name in candidate_providers:
|
|
1161
|
-
provider = g_handlers[name]
|
|
1162
|
-
_log(f"provider: {name} {type(provider).__name__}")
|
|
1163
1348
|
try:
|
|
1164
|
-
|
|
1165
|
-
|
|
1349
|
+
provider_name = name
|
|
1350
|
+
provider = g_handlers[name]
|
|
1351
|
+
_log(f"provider: {name} {type(provider).__name__}")
|
|
1352
|
+
started_at = time.time()
|
|
1353
|
+
context["startedAt"] = datetime.now()
|
|
1354
|
+
context["provider"] = name
|
|
1355
|
+
model_info = provider.model_info(model)
|
|
1356
|
+
context["modelCost"] = model_info.get("cost", provider.model_cost(model)) or {"input": 0, "output": 0}
|
|
1357
|
+
context["modelInfo"] = model_info
|
|
1358
|
+
|
|
1359
|
+
# Accumulate usage across tool calls
|
|
1360
|
+
total_usage = {
|
|
1361
|
+
"prompt_tokens": 0,
|
|
1362
|
+
"completion_tokens": 0,
|
|
1363
|
+
"total_tokens": 0,
|
|
1364
|
+
}
|
|
1365
|
+
accumulated_cost = 0.0
|
|
1366
|
+
|
|
1166
1367
|
# Inject global tools if present
|
|
1167
1368
|
current_chat = chat.copy()
|
|
1168
1369
|
if g_app.tool_definitions:
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
only_tools_str = chat["metadata"].get("only_tools", "")
|
|
1173
|
-
include_all_tools = only_tools_str == "all"
|
|
1174
|
-
only_tools = only_tools_str.split(",")
|
|
1370
|
+
only_tools_str = context.get("tools", "all")
|
|
1371
|
+
include_all_tools = only_tools_str == "all"
|
|
1372
|
+
only_tools = only_tools_str.split(",")
|
|
1175
1373
|
|
|
1176
1374
|
if include_all_tools or len(only_tools) > 0:
|
|
1177
1375
|
if "tools" not in current_chat:
|
|
@@ -1183,12 +1381,38 @@ async def g_chat_completion(chat, context=None):
|
|
|
1183
1381
|
if name not in existing_tools and (include_all_tools or name in only_tools):
|
|
1184
1382
|
current_chat["tools"].append(tool_def)
|
|
1185
1383
|
|
|
1384
|
+
# Apply pre-chat filters ONCE
|
|
1385
|
+
context["chat"] = current_chat
|
|
1386
|
+
for filter_func in g_app.chat_request_filters:
|
|
1387
|
+
await filter_func(current_chat, context)
|
|
1388
|
+
|
|
1186
1389
|
# Tool execution loop
|
|
1187
|
-
max_iterations =
|
|
1390
|
+
max_iterations = 10
|
|
1188
1391
|
tool_history = []
|
|
1392
|
+
final_response = None
|
|
1393
|
+
|
|
1189
1394
|
for _ in range(max_iterations):
|
|
1395
|
+
if should_cancel_thread(context):
|
|
1396
|
+
return
|
|
1397
|
+
|
|
1190
1398
|
response = await provider.chat(current_chat)
|
|
1191
1399
|
|
|
1400
|
+
if should_cancel_thread(context):
|
|
1401
|
+
return
|
|
1402
|
+
|
|
1403
|
+
# Aggregate usage
|
|
1404
|
+
if "usage" in response:
|
|
1405
|
+
usage = response["usage"]
|
|
1406
|
+
total_usage["prompt_tokens"] += usage.get("prompt_tokens", 0)
|
|
1407
|
+
total_usage["completion_tokens"] += usage.get("completion_tokens", 0)
|
|
1408
|
+
total_usage["total_tokens"] += usage.get("total_tokens", 0)
|
|
1409
|
+
|
|
1410
|
+
# Calculate cost for this step if available
|
|
1411
|
+
if "cost" in response and isinstance(response["cost"], (int, float)):
|
|
1412
|
+
accumulated_cost += response["cost"]
|
|
1413
|
+
elif "cost" in usage and isinstance(usage["cost"], (int, float)):
|
|
1414
|
+
accumulated_cost += usage["cost"]
|
|
1415
|
+
|
|
1192
1416
|
# Check for tool_calls in the response
|
|
1193
1417
|
choice = response.get("choices", [])[0] if response.get("choices") else {}
|
|
1194
1418
|
message = choice.get("message", {})
|
|
@@ -1198,48 +1422,85 @@ async def g_chat_completion(chat, context=None):
|
|
|
1198
1422
|
# Append the assistant's message with tool calls to history
|
|
1199
1423
|
if "messages" not in current_chat:
|
|
1200
1424
|
current_chat["messages"] = []
|
|
1425
|
+
if "timestamp" not in message:
|
|
1426
|
+
message["timestamp"] = int(time.time() * 1000)
|
|
1201
1427
|
current_chat["messages"].append(message)
|
|
1202
1428
|
tool_history.append(message)
|
|
1203
1429
|
|
|
1204
1430
|
for tool_call in tool_calls:
|
|
1205
1431
|
function_name = tool_call["function"]["name"]
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1432
|
+
try:
|
|
1433
|
+
function_args = json.loads(tool_call["function"]["arguments"])
|
|
1434
|
+
except Exception as e:
|
|
1435
|
+
tool_result = f"Error parsing JSON arguments for tool {function_name}: {e}"
|
|
1436
|
+
else:
|
|
1437
|
+
tool_result = f"Error: Tool {function_name} not found"
|
|
1438
|
+
if function_name in g_app.tools:
|
|
1439
|
+
try:
|
|
1440
|
+
func = g_app.tools[function_name]
|
|
1441
|
+
if inspect.iscoroutinefunction(func):
|
|
1442
|
+
tool_result = await func(**function_args)
|
|
1443
|
+
else:
|
|
1444
|
+
tool_result = func(**function_args)
|
|
1445
|
+
except Exception as e:
|
|
1446
|
+
tool_result = f"Error executing tool {function_name}: {e}"
|
|
1218
1447
|
|
|
1219
1448
|
# Append tool result to history
|
|
1220
|
-
tool_msg = {"role": "tool", "tool_call_id": tool_call["id"], "content":
|
|
1449
|
+
tool_msg = {"role": "tool", "tool_call_id": tool_call["id"], "content": to_content(tool_result)}
|
|
1221
1450
|
current_chat["messages"].append(tool_msg)
|
|
1222
1451
|
tool_history.append(tool_msg)
|
|
1223
1452
|
|
|
1453
|
+
for filter_func in g_app.chat_tool_filters:
|
|
1454
|
+
await filter_func(current_chat, context)
|
|
1455
|
+
|
|
1456
|
+
if should_cancel_thread(context):
|
|
1457
|
+
return
|
|
1458
|
+
|
|
1224
1459
|
# Continue loop to send tool results back to LLM
|
|
1225
1460
|
continue
|
|
1226
1461
|
|
|
1227
|
-
# If no tool calls,
|
|
1462
|
+
# If no tool calls, this is the final response
|
|
1228
1463
|
if tool_history:
|
|
1229
1464
|
response["tool_history"] = tool_history
|
|
1230
|
-
|
|
1465
|
+
|
|
1466
|
+
# Update final response with aggregated usage
|
|
1467
|
+
if "usage" not in response:
|
|
1468
|
+
response["usage"] = {}
|
|
1469
|
+
# convert to int seconds
|
|
1470
|
+
context["duration"] = duration = int(time.time() - started_at)
|
|
1471
|
+
total_usage.update({"duration": duration})
|
|
1472
|
+
response["usage"].update(total_usage)
|
|
1473
|
+
# If we accumulated cost, set it on the response
|
|
1474
|
+
if accumulated_cost > 0:
|
|
1475
|
+
response["cost"] = accumulated_cost
|
|
1476
|
+
|
|
1477
|
+
final_response = response
|
|
1478
|
+
break # Exit tool loop
|
|
1479
|
+
|
|
1480
|
+
if final_response:
|
|
1481
|
+
# Apply post-chat filters ONCE on final response
|
|
1482
|
+
for filter_func in g_app.chat_response_filters:
|
|
1483
|
+
await filter_func(final_response, context)
|
|
1484
|
+
|
|
1485
|
+
if DEBUG:
|
|
1486
|
+
_dbg(json.dumps(final_response, indent=2))
|
|
1487
|
+
|
|
1488
|
+
return final_response
|
|
1231
1489
|
|
|
1232
1490
|
except Exception as e:
|
|
1233
1491
|
if first_exception is None:
|
|
1234
1492
|
first_exception = e
|
|
1235
|
-
|
|
1493
|
+
context["stackTrace"] = traceback.format_exc()
|
|
1494
|
+
_err(f"Provider {provider_name} failed", first_exception)
|
|
1495
|
+
await g_app.on_chat_error(e, context)
|
|
1496
|
+
|
|
1236
1497
|
continue
|
|
1237
1498
|
|
|
1238
1499
|
# If we get here, all providers failed
|
|
1239
1500
|
raise first_exception
|
|
1240
1501
|
|
|
1241
1502
|
|
|
1242
|
-
async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False):
|
|
1503
|
+
async def cli_chat(chat, tools=None, image=None, audio=None, file=None, args=None, raw=False):
|
|
1243
1504
|
if g_default_model:
|
|
1244
1505
|
chat["model"] = g_default_model
|
|
1245
1506
|
|
|
@@ -1314,7 +1575,10 @@ async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False
|
|
|
1314
1575
|
printdump(chat)
|
|
1315
1576
|
|
|
1316
1577
|
try:
|
|
1317
|
-
|
|
1578
|
+
context = {
|
|
1579
|
+
"tools": tools or "all",
|
|
1580
|
+
}
|
|
1581
|
+
response = await g_app.chat_completion(chat, context=context)
|
|
1318
1582
|
|
|
1319
1583
|
if raw:
|
|
1320
1584
|
print(json.dumps(response, indent=2))
|
|
@@ -1349,15 +1613,15 @@ async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False
|
|
|
1349
1613
|
except HTTPError as e:
|
|
1350
1614
|
# HTTP error (4xx, 5xx)
|
|
1351
1615
|
print(f"{e}:\n{e.body}")
|
|
1352
|
-
exit(1)
|
|
1616
|
+
g_app.exit(1)
|
|
1353
1617
|
except aiohttp.ClientConnectionError as e:
|
|
1354
1618
|
# Connection issues
|
|
1355
1619
|
print(f"Connection error: {e}")
|
|
1356
|
-
exit(1)
|
|
1620
|
+
g_app.exit(1)
|
|
1357
1621
|
except asyncio.TimeoutError as e:
|
|
1358
1622
|
# Timeout
|
|
1359
1623
|
print(f"Timeout error: {e}")
|
|
1360
|
-
exit(1)
|
|
1624
|
+
g_app.exit(1)
|
|
1361
1625
|
|
|
1362
1626
|
|
|
1363
1627
|
def config_str(key):
|
|
@@ -2043,17 +2307,41 @@ class AppExtensions:
|
|
|
2043
2307
|
def __init__(self, cli_args, extra_args):
|
|
2044
2308
|
self.cli_args = cli_args
|
|
2045
2309
|
self.extra_args = extra_args
|
|
2310
|
+
self.config = None
|
|
2311
|
+
self.error_auth_required = create_error_response("Authentication required", "Unauthorized")
|
|
2046
2312
|
self.ui_extensions = []
|
|
2047
2313
|
self.chat_request_filters = []
|
|
2314
|
+
self.chat_tool_filters = []
|
|
2048
2315
|
self.chat_response_filters = []
|
|
2316
|
+
self.chat_error_filters = []
|
|
2049
2317
|
self.server_add_get = []
|
|
2050
2318
|
self.server_add_post = []
|
|
2051
2319
|
self.server_add_put = []
|
|
2052
2320
|
self.server_add_delete = []
|
|
2053
2321
|
self.server_add_patch = []
|
|
2054
2322
|
self.cache_saved_filters = []
|
|
2323
|
+
self.shutdown_handlers = []
|
|
2055
2324
|
self.tools = {}
|
|
2056
2325
|
self.tool_definitions = []
|
|
2326
|
+
self.index_headers = []
|
|
2327
|
+
self.index_footers = []
|
|
2328
|
+
self.request_args = {
|
|
2329
|
+
"image_config": dict, # e.g. { "aspect_ratio": "1:1" }
|
|
2330
|
+
"temperature": float, # e.g: 0.7
|
|
2331
|
+
"max_completion_tokens": int, # e.g: 2048
|
|
2332
|
+
"seed": int, # e.g: 42
|
|
2333
|
+
"top_p": float, # e.g: 0.9
|
|
2334
|
+
"frequency_penalty": float, # e.g: 0.5
|
|
2335
|
+
"presence_penalty": float, # e.g: 0.5
|
|
2336
|
+
"stop": list, # e.g: ["Stop"]
|
|
2337
|
+
"reasoning_effort": str, # e.g: minimal, low, medium, high
|
|
2338
|
+
"verbosity": str, # e.g: low, medium, high
|
|
2339
|
+
"service_tier": str, # e.g: auto, default
|
|
2340
|
+
"top_logprobs": int,
|
|
2341
|
+
"safety_identifier": str,
|
|
2342
|
+
"store": bool,
|
|
2343
|
+
"enable_thinking": bool,
|
|
2344
|
+
}
|
|
2057
2345
|
self.all_providers = [
|
|
2058
2346
|
OpenAiCompatible,
|
|
2059
2347
|
MistralProvider,
|
|
@@ -2075,6 +2363,43 @@ class AppExtensions:
|
|
|
2075
2363
|
"16:9": "1344×768",
|
|
2076
2364
|
"21:9": "1536×672",
|
|
2077
2365
|
}
|
|
2366
|
+
self.import_maps = {
|
|
2367
|
+
"vue-prod": "/ui/lib/vue.min.mjs",
|
|
2368
|
+
"vue": "/ui/lib/vue.mjs",
|
|
2369
|
+
"vue-router": "/ui/lib/vue-router.min.mjs",
|
|
2370
|
+
"@servicestack/client": "/ui/lib/servicestack-client.mjs",
|
|
2371
|
+
"@servicestack/vue": "/ui/lib/servicestack-vue.mjs",
|
|
2372
|
+
"idb": "/ui/lib/idb.min.mjs",
|
|
2373
|
+
"marked": "/ui/lib/marked.min.mjs",
|
|
2374
|
+
"highlight.js": "/ui/lib/highlight.min.mjs",
|
|
2375
|
+
"chart.js": "/ui/lib/chart.js",
|
|
2376
|
+
"color.js": "/ui/lib/color.js",
|
|
2377
|
+
"ctx.mjs": "/ui/ctx.mjs",
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
def set_config(self, config):
|
|
2381
|
+
self.config = config
|
|
2382
|
+
self.auth_enabled = self.config.get("auth", {}).get("enabled", False)
|
|
2383
|
+
|
|
2384
|
+
# Authentication middleware helper
|
|
2385
|
+
def check_auth(self, request):
|
|
2386
|
+
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
2387
|
+
if not self.auth_enabled:
|
|
2388
|
+
return True, None
|
|
2389
|
+
|
|
2390
|
+
# Check for OAuth session token
|
|
2391
|
+
session_token = get_session_token(request)
|
|
2392
|
+
if session_token and session_token in g_sessions:
|
|
2393
|
+
return True, g_sessions[session_token]
|
|
2394
|
+
|
|
2395
|
+
# Check for API key
|
|
2396
|
+
auth_header = request.headers.get("Authorization", "")
|
|
2397
|
+
if auth_header.startswith("Bearer "):
|
|
2398
|
+
api_key = auth_header[7:]
|
|
2399
|
+
if api_key:
|
|
2400
|
+
return True, {"authProvider": "apikey"}
|
|
2401
|
+
|
|
2402
|
+
return False, None
|
|
2078
2403
|
|
|
2079
2404
|
def get_session(self, request):
|
|
2080
2405
|
session_token = get_session_token(request)
|
|
@@ -2100,25 +2425,7 @@ class AppExtensions:
|
|
|
2100
2425
|
return g_chat_request(template=template, text=text, model=model, system_prompt=system_prompt)
|
|
2101
2426
|
|
|
2102
2427
|
async def chat_completion(self, chat, context=None):
|
|
2103
|
-
# Apply pre-chat filters
|
|
2104
|
-
if context is None:
|
|
2105
|
-
context = {"chat": chat}
|
|
2106
|
-
elif "request" in context:
|
|
2107
|
-
username = self.get_username(context["request"])
|
|
2108
|
-
if username:
|
|
2109
|
-
if "metadata" not in chat:
|
|
2110
|
-
chat["metadata"] = {}
|
|
2111
|
-
chat["metadata"]["user"] = username
|
|
2112
|
-
|
|
2113
|
-
for filter_func in self.chat_request_filters:
|
|
2114
|
-
chat = await filter_func(chat, context)
|
|
2115
|
-
|
|
2116
2428
|
response = await g_chat_completion(chat, context)
|
|
2117
|
-
|
|
2118
|
-
# Apply post-chat filters
|
|
2119
|
-
for filter_func in self.chat_response_filters:
|
|
2120
|
-
response = await filter_func(response, context)
|
|
2121
|
-
|
|
2122
2429
|
return response
|
|
2123
2430
|
|
|
2124
2431
|
def on_cache_saved_filters(self, context):
|
|
@@ -2126,6 +2433,25 @@ class AppExtensions:
|
|
|
2126
2433
|
for filter_func in self.cache_saved_filters:
|
|
2127
2434
|
filter_func(context)
|
|
2128
2435
|
|
|
2436
|
+
async def on_chat_error(self, e, context):
|
|
2437
|
+
# Apply chat error filters
|
|
2438
|
+
if "stackTrace" not in context:
|
|
2439
|
+
context["stackTrace"] = traceback.format_exc()
|
|
2440
|
+
for filter_func in self.chat_error_filters:
|
|
2441
|
+
try:
|
|
2442
|
+
await filter_func(e, context)
|
|
2443
|
+
except Exception as e:
|
|
2444
|
+
_err("chat error filter failed", e)
|
|
2445
|
+
|
|
2446
|
+
def exit(self, exit_code=0):
|
|
2447
|
+
if len(self.shutdown_handlers) > 0:
|
|
2448
|
+
_dbg(f"running {len(self.shutdown_handlers)} shutdown handlers...")
|
|
2449
|
+
for handler in self.shutdown_handlers:
|
|
2450
|
+
handler()
|
|
2451
|
+
|
|
2452
|
+
_dbg(f"exit({exit_code})")
|
|
2453
|
+
sys.exit(exit_code)
|
|
2454
|
+
|
|
2129
2455
|
|
|
2130
2456
|
def handler_name(handler):
|
|
2131
2457
|
if hasattr(handler, "__name__"):
|
|
@@ -2136,6 +2462,9 @@ def handler_name(handler):
|
|
|
2136
2462
|
class ExtensionContext:
|
|
2137
2463
|
def __init__(self, app, path):
|
|
2138
2464
|
self.app = app
|
|
2465
|
+
self.cli_args = app.cli_args
|
|
2466
|
+
self.extra_args = app.extra_args
|
|
2467
|
+
self.error_auth_required = app.error_auth_required
|
|
2139
2468
|
self.path = path
|
|
2140
2469
|
self.name = os.path.basename(path)
|
|
2141
2470
|
if self.name.endswith(".py"):
|
|
@@ -2146,10 +2475,17 @@ class ExtensionContext:
|
|
|
2146
2475
|
self.debug = DEBUG
|
|
2147
2476
|
self.verbose = g_verbose
|
|
2148
2477
|
self.aspect_ratios = app.aspect_ratios
|
|
2478
|
+
self.request_args = app.request_args
|
|
2149
2479
|
|
|
2150
2480
|
def chat_to_prompt(self, chat):
|
|
2151
2481
|
return chat_to_prompt(chat)
|
|
2152
2482
|
|
|
2483
|
+
def chat_to_system_prompt(self, chat):
|
|
2484
|
+
return chat_to_system_prompt(chat)
|
|
2485
|
+
|
|
2486
|
+
def chat_response_to_message(self, response):
|
|
2487
|
+
return chat_response_to_message(response)
|
|
2488
|
+
|
|
2153
2489
|
def last_user_prompt(self, chat):
|
|
2154
2490
|
return last_user_prompt(chat)
|
|
2155
2491
|
|
|
@@ -2184,6 +2520,12 @@ class ExtensionContext:
|
|
|
2184
2520
|
if self.verbose:
|
|
2185
2521
|
print(traceback.format_exc(), flush=True)
|
|
2186
2522
|
|
|
2523
|
+
def error_message(self, e):
|
|
2524
|
+
return to_error_message(e)
|
|
2525
|
+
|
|
2526
|
+
def error_response(self, e, stacktrace=False):
|
|
2527
|
+
return to_error_response(e, stacktrace=stacktrace)
|
|
2528
|
+
|
|
2187
2529
|
def add_provider(self, provider):
|
|
2188
2530
|
self.log(f"Registered provider: {provider.__name__}")
|
|
2189
2531
|
self.app.all_providers.append(provider)
|
|
@@ -2197,14 +2539,26 @@ class ExtensionContext:
|
|
|
2197
2539
|
self.log(f"Registered chat request filter: {handler_name(handler)}")
|
|
2198
2540
|
self.app.chat_request_filters.append(handler)
|
|
2199
2541
|
|
|
2542
|
+
def register_chat_tool_filter(self, handler):
|
|
2543
|
+
self.log(f"Registered chat tool filter: {handler_name(handler)}")
|
|
2544
|
+
self.app.chat_tool_filters.append(handler)
|
|
2545
|
+
|
|
2200
2546
|
def register_chat_response_filter(self, handler):
|
|
2201
2547
|
self.log(f"Registered chat response filter: {handler_name(handler)}")
|
|
2202
2548
|
self.app.chat_response_filters.append(handler)
|
|
2203
2549
|
|
|
2550
|
+
def register_chat_error_filter(self, handler):
|
|
2551
|
+
self.log(f"Registered chat error filter: {handler_name(handler)}")
|
|
2552
|
+
self.app.chat_error_filters.append(handler)
|
|
2553
|
+
|
|
2204
2554
|
def register_cache_saved_filter(self, handler):
|
|
2205
2555
|
self.log(f"Registered cache saved filter: {handler_name(handler)}")
|
|
2206
2556
|
self.app.cache_saved_filters.append(handler)
|
|
2207
2557
|
|
|
2558
|
+
def register_shutdown_handler(self, handler):
|
|
2559
|
+
self.log(f"Registered shutdown handler: {handler_name(handler)}")
|
|
2560
|
+
self.app.shutdown_handlers.append(handler)
|
|
2561
|
+
|
|
2208
2562
|
def add_static_files(self, ext_dir):
|
|
2209
2563
|
self.log(f"Registered static files: {ext_dir}")
|
|
2210
2564
|
|
|
@@ -2237,6 +2591,15 @@ class ExtensionContext:
|
|
|
2237
2591
|
self.dbg(f"Registered PATCH: {os.path.join(self.ext_prefix, path)}")
|
|
2238
2592
|
self.app.server_add_patch.append((os.path.join(self.ext_prefix, path), handler, kwargs))
|
|
2239
2593
|
|
|
2594
|
+
def add_importmaps(self, dict):
|
|
2595
|
+
self.app.import_maps.update(dict)
|
|
2596
|
+
|
|
2597
|
+
def add_index_header(self, html):
|
|
2598
|
+
self.app.index_headers.append(html)
|
|
2599
|
+
|
|
2600
|
+
def add_index_footer(self, html):
|
|
2601
|
+
self.app.index_footers.append(html)
|
|
2602
|
+
|
|
2240
2603
|
def get_config(self):
|
|
2241
2604
|
return g_config
|
|
2242
2605
|
|
|
@@ -2264,6 +2627,9 @@ class ExtensionContext:
|
|
|
2264
2627
|
self.app.tools[name] = func
|
|
2265
2628
|
self.app.tool_definitions.append(tool_def)
|
|
2266
2629
|
|
|
2630
|
+
def check_auth(self, request):
|
|
2631
|
+
return self.app.check_auth(request)
|
|
2632
|
+
|
|
2267
2633
|
def get_session(self, request):
|
|
2268
2634
|
return self.app.get_session(request)
|
|
2269
2635
|
|
|
@@ -2273,6 +2639,15 @@ class ExtensionContext:
|
|
|
2273
2639
|
def get_user_path(self, username=None):
|
|
2274
2640
|
return self.app.get_user_path(username)
|
|
2275
2641
|
|
|
2642
|
+
def should_cancel_thread(self, context):
|
|
2643
|
+
return should_cancel_thread(context)
|
|
2644
|
+
|
|
2645
|
+
def cache_message_inline_data(self, message):
|
|
2646
|
+
return cache_message_inline_data(message)
|
|
2647
|
+
|
|
2648
|
+
def to_content(self, result):
|
|
2649
|
+
return to_content(result)
|
|
2650
|
+
|
|
2276
2651
|
|
|
2277
2652
|
def get_extensions_path():
|
|
2278
2653
|
return os.getenv("LLMS_EXTENSIONS_DIR", os.path.join(Path.home(), ".llms", "extensions"))
|
|
@@ -2454,6 +2829,9 @@ def main():
|
|
|
2454
2829
|
parser.add_argument(
|
|
2455
2830
|
"-s", "--system", default=None, help="System prompt to use for chat completion", metavar="PROMPT"
|
|
2456
2831
|
)
|
|
2832
|
+
parser.add_argument(
|
|
2833
|
+
"--tools", default=None, help="Tools to use for chat completion (all|none|<tool>,<tool>...)", metavar="TOOLS"
|
|
2834
|
+
)
|
|
2457
2835
|
parser.add_argument("--image", default=None, help="Image input to use in chat completion")
|
|
2458
2836
|
parser.add_argument("--audio", default=None, help="Audio input to use in chat completion")
|
|
2459
2837
|
parser.add_argument("--file", default=None, help="File input to use in chat completion")
|
|
@@ -2576,6 +2954,8 @@ def main():
|
|
|
2576
2954
|
g_config_path = home_config_path
|
|
2577
2955
|
g_config = load_config_json(text_from_file(g_config_path))
|
|
2578
2956
|
|
|
2957
|
+
g_app.set_config(g_config)
|
|
2958
|
+
|
|
2579
2959
|
if not g_providers:
|
|
2580
2960
|
g_providers = json.loads(text_from_file(home_providers_path))
|
|
2581
2961
|
|
|
@@ -2794,17 +3174,16 @@ def main():
|
|
|
2794
3174
|
print(f"\n{model_count} models available from {provider_count} providers")
|
|
2795
3175
|
|
|
2796
3176
|
print_status()
|
|
2797
|
-
exit(0)
|
|
3177
|
+
g_app.exit(0)
|
|
2798
3178
|
|
|
2799
3179
|
if cli_args.check is not None:
|
|
2800
3180
|
# Check validity of models for a provider
|
|
2801
3181
|
provider_name = cli_args.check
|
|
2802
3182
|
model_names = extra_args if len(extra_args) > 0 else None
|
|
2803
3183
|
asyncio.run(check_models(provider_name, model_names))
|
|
2804
|
-
exit(0)
|
|
3184
|
+
g_app.exit(0)
|
|
2805
3185
|
|
|
2806
3186
|
if cli_args.serve is not None:
|
|
2807
|
-
error_auth_required = create_error_response("Authentication required", "Unauthorized")
|
|
2808
3187
|
# Disable inactive providers and save to config before starting server
|
|
2809
3188
|
all_providers = g_config["providers"].keys()
|
|
2810
3189
|
enabled_providers = list(g_handlers.keys())
|
|
@@ -2857,35 +3236,18 @@ def main():
|
|
|
2857
3236
|
_log(f"client_max_size set to {client_max_size} bytes ({client_max_size / 1024 / 1024:.1f}MB)")
|
|
2858
3237
|
app = web.Application(client_max_size=client_max_size)
|
|
2859
3238
|
|
|
2860
|
-
# Authentication middleware helper
|
|
2861
|
-
def check_auth(request):
|
|
2862
|
-
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
2863
|
-
if not auth_enabled:
|
|
2864
|
-
return True, None
|
|
2865
|
-
|
|
2866
|
-
# Check for OAuth session token
|
|
2867
|
-
session_token = get_session_token(request)
|
|
2868
|
-
if session_token and session_token in g_sessions:
|
|
2869
|
-
return True, g_sessions[session_token]
|
|
2870
|
-
|
|
2871
|
-
# Check for API key
|
|
2872
|
-
auth_header = request.headers.get("Authorization", "")
|
|
2873
|
-
if auth_header.startswith("Bearer "):
|
|
2874
|
-
api_key = auth_header[7:]
|
|
2875
|
-
if api_key:
|
|
2876
|
-
return True, {"authProvider": "apikey"}
|
|
2877
|
-
|
|
2878
|
-
return False, None
|
|
2879
|
-
|
|
2880
3239
|
async def chat_handler(request):
|
|
2881
3240
|
# Check authentication if enabled
|
|
2882
|
-
is_authenticated, user_data = check_auth(request)
|
|
3241
|
+
is_authenticated, user_data = g_app.check_auth(request)
|
|
2883
3242
|
if not is_authenticated:
|
|
2884
|
-
return web.json_response(error_auth_required, status=401)
|
|
3243
|
+
return web.json_response(g_app.error_auth_required, status=401)
|
|
2885
3244
|
|
|
2886
3245
|
try:
|
|
2887
3246
|
chat = await request.json()
|
|
2888
|
-
context = {"request": request, "
|
|
3247
|
+
context = {"chat": chat, "request": request, "user": g_app.get_username(request)}
|
|
3248
|
+
metadata = chat.get("metadata", {})
|
|
3249
|
+
context["threadId"] = metadata.get("threadId", None)
|
|
3250
|
+
context["tools"] = metadata.get("tools", "all")
|
|
2889
3251
|
response = await g_app.chat_completion(chat, context)
|
|
2890
3252
|
return web.json_response(response)
|
|
2891
3253
|
except Exception as e:
|
|
@@ -2941,9 +3303,9 @@ def main():
|
|
|
2941
3303
|
|
|
2942
3304
|
async def upload_handler(request):
|
|
2943
3305
|
# Check authentication if enabled
|
|
2944
|
-
is_authenticated, user_data = check_auth(request)
|
|
3306
|
+
is_authenticated, user_data = g_app.check_auth(request)
|
|
2945
3307
|
if not is_authenticated:
|
|
2946
|
-
return web.json_response(error_auth_required, status=401)
|
|
3308
|
+
return web.json_response(g_app.error_auth_required, status=401)
|
|
2947
3309
|
|
|
2948
3310
|
reader = await request.multipart()
|
|
2949
3311
|
|
|
@@ -3293,7 +3655,7 @@ def main():
|
|
|
3293
3655
|
# })
|
|
3294
3656
|
|
|
3295
3657
|
# Not authenticated - return error in expected format
|
|
3296
|
-
return web.json_response(error_auth_required, status=401)
|
|
3658
|
+
return web.json_response(g_app.error_auth_required, status=401)
|
|
3297
3659
|
|
|
3298
3660
|
app.router.add_get("/auth", auth_handler)
|
|
3299
3661
|
app.router.add_get("/auth/github", github_auth_handler)
|
|
@@ -3406,8 +3768,28 @@ def main():
|
|
|
3406
3768
|
# Serve index.html from root
|
|
3407
3769
|
async def index_handler(request):
|
|
3408
3770
|
index_content = read_resource_file_bytes("index.html")
|
|
3409
|
-
|
|
3410
|
-
|
|
3771
|
+
|
|
3772
|
+
importmaps = {"imports": g_app.import_maps}
|
|
3773
|
+
importmaps_script = '<script type="importmap">\n' + json.dumps(importmaps, indent=4) + "\n</script>"
|
|
3774
|
+
index_content = index_content.replace(
|
|
3775
|
+
b'<script type="importmap"></script>',
|
|
3776
|
+
importmaps_script.encode("utf-8"),
|
|
3777
|
+
)
|
|
3778
|
+
|
|
3779
|
+
if len(g_app.index_headers) > 0:
|
|
3780
|
+
html_header = ""
|
|
3781
|
+
for header in g_app.index_headers:
|
|
3782
|
+
html_header += header
|
|
3783
|
+
# replace </head> with html_header
|
|
3784
|
+
index_content = index_content.replace(b"</head>", html_header.encode("utf-8") + b"\n</head>")
|
|
3785
|
+
|
|
3786
|
+
if len(g_app.index_footers) > 0:
|
|
3787
|
+
html_footer = ""
|
|
3788
|
+
for footer in g_app.index_footers:
|
|
3789
|
+
html_footer += footer
|
|
3790
|
+
# replace </body> with html_footer
|
|
3791
|
+
index_content = index_content.replace(b"</body>", html_footer.encode("utf-8") + b"\n</body>")
|
|
3792
|
+
|
|
3411
3793
|
return web.Response(body=index_content, content_type="text/html")
|
|
3412
3794
|
|
|
3413
3795
|
app.router.add_get("/", index_handler)
|
|
@@ -3427,7 +3809,7 @@ def main():
|
|
|
3427
3809
|
|
|
3428
3810
|
print(f"Starting server on port {port}...")
|
|
3429
3811
|
web.run_app(app, host="0.0.0.0", port=port, print=_log)
|
|
3430
|
-
exit(0)
|
|
3812
|
+
g_app.exit(0)
|
|
3431
3813
|
|
|
3432
3814
|
if cli_args.enable is not None:
|
|
3433
3815
|
if cli_args.enable.endswith(","):
|
|
@@ -3457,7 +3839,7 @@ def main():
|
|
|
3457
3839
|
print_status()
|
|
3458
3840
|
if len(msgs) > 0:
|
|
3459
3841
|
print("\n" + "\n".join(msgs))
|
|
3460
|
-
exit(0)
|
|
3842
|
+
g_app.exit(0)
|
|
3461
3843
|
|
|
3462
3844
|
if cli_args.disable is not None:
|
|
3463
3845
|
if cli_args.disable.endswith(","):
|
|
@@ -3480,7 +3862,7 @@ def main():
|
|
|
3480
3862
|
print(f"\nDisabled provider {provider}")
|
|
3481
3863
|
|
|
3482
3864
|
print_status()
|
|
3483
|
-
exit(0)
|
|
3865
|
+
g_app.exit(0)
|
|
3484
3866
|
|
|
3485
3867
|
if cli_args.default is not None:
|
|
3486
3868
|
default_model = cli_args.default
|
|
@@ -3492,7 +3874,7 @@ def main():
|
|
|
3492
3874
|
default_text["model"] = default_model
|
|
3493
3875
|
save_config(g_config)
|
|
3494
3876
|
print(f"\nDefault model set to: {default_model}")
|
|
3495
|
-
exit(0)
|
|
3877
|
+
g_app.exit(0)
|
|
3496
3878
|
|
|
3497
3879
|
if (
|
|
3498
3880
|
cli_args.chat is not None
|
|
@@ -3552,21 +3934,28 @@ def main():
|
|
|
3552
3934
|
|
|
3553
3935
|
asyncio.run(
|
|
3554
3936
|
cli_chat(
|
|
3555
|
-
chat,
|
|
3937
|
+
chat,
|
|
3938
|
+
tools=cli_args.tools,
|
|
3939
|
+
image=cli_args.image,
|
|
3940
|
+
audio=cli_args.audio,
|
|
3941
|
+
file=cli_args.file,
|
|
3942
|
+
args=args,
|
|
3943
|
+
raw=cli_args.raw,
|
|
3556
3944
|
)
|
|
3557
3945
|
)
|
|
3558
|
-
exit(0)
|
|
3946
|
+
g_app.exit(0)
|
|
3559
3947
|
except Exception as e:
|
|
3560
3948
|
print(f"{cli_args.logprefix}Error: {e}")
|
|
3561
3949
|
if cli_args.verbose:
|
|
3562
3950
|
traceback.print_exc()
|
|
3563
|
-
exit(1)
|
|
3951
|
+
g_app.exit(1)
|
|
3564
3952
|
|
|
3565
3953
|
handled = run_extension_cli()
|
|
3566
3954
|
|
|
3567
3955
|
if not handled:
|
|
3568
3956
|
# show usage from ArgumentParser
|
|
3569
3957
|
parser.print_help()
|
|
3958
|
+
g_app.exit(0)
|
|
3570
3959
|
|
|
3571
3960
|
|
|
3572
3961
|
if __name__ == "__main__":
|