llms-py 3.0.10__py3-none-any.whl → 3.0.18__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/extensions/app/__init__.py +0 -1
- llms/extensions/app/db.py +7 -3
- llms/extensions/app/ui/threadStore.mjs +10 -3
- llms/extensions/computer/README.md +96 -0
- llms/extensions/computer/__init__.py +59 -0
- llms/extensions/computer/base.py +80 -0
- llms/extensions/computer/bash.py +185 -0
- llms/extensions/computer/computer.py +523 -0
- llms/extensions/computer/edit.py +299 -0
- llms/extensions/computer/filesystem.py +542 -0
- llms/extensions/computer/platform.py +461 -0
- llms/extensions/computer/run.py +37 -0
- llms/extensions/core_tools/__init__.py +0 -38
- llms/extensions/providers/anthropic.py +28 -1
- llms/extensions/providers/cerebras.py +0 -1
- llms/extensions/providers/google.py +112 -34
- llms/extensions/skills/LICENSE +202 -0
- llms/extensions/skills/__init__.py +130 -0
- llms/extensions/skills/errors.py +25 -0
- llms/extensions/skills/models.py +39 -0
- llms/extensions/skills/parser.py +178 -0
- llms/extensions/skills/ui/index.mjs +376 -0
- llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
- llms/extensions/skills/validator.py +177 -0
- llms/extensions/system_prompts/ui/index.mjs +6 -10
- llms/extensions/tools/__init__.py +5 -82
- llms/extensions/tools/ui/index.mjs +194 -63
- llms/main.py +502 -146
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +530 -0
- llms/ui/ctx.mjs +53 -6
- llms/ui/modules/chat/ChatBody.mjs +200 -20
- llms/ui/modules/chat/index.mjs +108 -104
- llms/ui/tailwind.input.css +10 -0
- llms/ui/utils.mjs +25 -1
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/METADATA +2 -2
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/RECORD +41 -24
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/top_level.txt +0 -0
llms/main.py
CHANGED
|
@@ -18,17 +18,33 @@ import mimetypes
|
|
|
18
18
|
import os
|
|
19
19
|
import re
|
|
20
20
|
import secrets
|
|
21
|
+
import shlex
|
|
21
22
|
import shutil
|
|
22
23
|
import site
|
|
23
24
|
import subprocess
|
|
24
25
|
import sys
|
|
26
|
+
import tempfile
|
|
25
27
|
import time
|
|
26
28
|
import traceback
|
|
27
29
|
from datetime import datetime
|
|
30
|
+
from enum import Enum, IntEnum
|
|
28
31
|
from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
|
|
29
32
|
from io import BytesIO
|
|
30
33
|
from pathlib import Path
|
|
31
|
-
from typing import
|
|
34
|
+
from typing import (
|
|
35
|
+
Annotated,
|
|
36
|
+
Any,
|
|
37
|
+
Callable,
|
|
38
|
+
Dict,
|
|
39
|
+
List,
|
|
40
|
+
Literal,
|
|
41
|
+
Optional,
|
|
42
|
+
Tuple,
|
|
43
|
+
Union,
|
|
44
|
+
get_args,
|
|
45
|
+
get_origin,
|
|
46
|
+
get_type_hints,
|
|
47
|
+
)
|
|
32
48
|
from urllib.parse import parse_qs, urlencode, urljoin
|
|
33
49
|
|
|
34
50
|
import aiohttp
|
|
@@ -41,7 +57,7 @@ try:
|
|
|
41
57
|
except ImportError:
|
|
42
58
|
HAS_PIL = False
|
|
43
59
|
|
|
44
|
-
VERSION = "3.0.
|
|
60
|
+
VERSION = "3.0.18"
|
|
45
61
|
_ROOT = None
|
|
46
62
|
DEBUG = os.getenv("DEBUG") == "1"
|
|
47
63
|
MOCK = os.getenv("MOCK") == "1"
|
|
@@ -59,6 +75,12 @@ g_oauth_states = {} # CSRF protection: {state: {created, redirect_uri}}
|
|
|
59
75
|
g_app = None # ExtensionsContext Singleton
|
|
60
76
|
|
|
61
77
|
|
|
78
|
+
class ExitCode(IntEnum):
|
|
79
|
+
SUCCESS = 0
|
|
80
|
+
FAILED = 1
|
|
81
|
+
UNHANDLED = 9
|
|
82
|
+
|
|
83
|
+
|
|
62
84
|
def _log(message):
|
|
63
85
|
if g_verbose:
|
|
64
86
|
print(f"{g_logprefix}{message}", flush=True)
|
|
@@ -340,22 +362,102 @@ def to_content(result):
|
|
|
340
362
|
return str(result)
|
|
341
363
|
|
|
342
364
|
|
|
365
|
+
def get_literal_values(typ):
|
|
366
|
+
"""Recursively extract values from Literal and Union types."""
|
|
367
|
+
origin = get_origin(typ)
|
|
368
|
+
if origin is Literal:
|
|
369
|
+
return list(get_args(typ))
|
|
370
|
+
elif origin is Union:
|
|
371
|
+
values = []
|
|
372
|
+
for arg in get_args(typ):
|
|
373
|
+
# Recurse for nested Unions or Literals
|
|
374
|
+
nested_values = get_literal_values(arg)
|
|
375
|
+
if nested_values:
|
|
376
|
+
for v in nested_values:
|
|
377
|
+
if v not in values:
|
|
378
|
+
values.append(v)
|
|
379
|
+
return values
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _py_type_to_json_type(param_type):
|
|
384
|
+
param_type_name = "string"
|
|
385
|
+
enum_values = None
|
|
386
|
+
items = None
|
|
387
|
+
|
|
388
|
+
# Check for Enum
|
|
389
|
+
if inspect.isclass(param_type) and issubclass(param_type, Enum):
|
|
390
|
+
enum_values = [e.value for e in param_type]
|
|
391
|
+
elif get_origin(param_type) is list or get_origin(param_type) is list:
|
|
392
|
+
param_type_name = "array"
|
|
393
|
+
args = get_args(param_type)
|
|
394
|
+
if args:
|
|
395
|
+
items_type, _, _ = _py_type_to_json_type(args[0])
|
|
396
|
+
items = {"type": items_type}
|
|
397
|
+
elif get_origin(param_type) is dict:
|
|
398
|
+
param_type_name = "object"
|
|
399
|
+
else:
|
|
400
|
+
# Check for Literal / Union[Literal]
|
|
401
|
+
enum_values = get_literal_values(param_type)
|
|
402
|
+
|
|
403
|
+
if enum_values:
|
|
404
|
+
# Infer type from the first value
|
|
405
|
+
value_type = type(enum_values[0])
|
|
406
|
+
if value_type is int:
|
|
407
|
+
param_type_name = "integer"
|
|
408
|
+
elif value_type is float:
|
|
409
|
+
param_type_name = "number"
|
|
410
|
+
elif value_type is bool:
|
|
411
|
+
param_type_name = "boolean"
|
|
412
|
+
|
|
413
|
+
elif param_type is int:
|
|
414
|
+
param_type_name = "integer"
|
|
415
|
+
elif param_type is float:
|
|
416
|
+
param_type_name = "number"
|
|
417
|
+
elif param_type is bool:
|
|
418
|
+
param_type_name = "boolean"
|
|
419
|
+
|
|
420
|
+
return param_type_name, enum_values, items
|
|
421
|
+
|
|
422
|
+
|
|
343
423
|
def function_to_tool_definition(func):
|
|
344
|
-
type_hints = get_type_hints(func)
|
|
424
|
+
type_hints = get_type_hints(func, include_extras=True)
|
|
345
425
|
signature = inspect.signature(func)
|
|
346
426
|
parameters = {"type": "object", "properties": {}, "required": []}
|
|
347
427
|
|
|
348
428
|
for name, param in signature.parameters.items():
|
|
349
429
|
param_type = type_hints.get(name, str)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
430
|
+
description = None
|
|
431
|
+
|
|
432
|
+
# Check for Annotated (for description)
|
|
433
|
+
if get_origin(param_type) is Annotated:
|
|
434
|
+
args = get_args(param_type)
|
|
435
|
+
param_type = args[0]
|
|
436
|
+
for arg in args[1:]:
|
|
437
|
+
if isinstance(arg, str):
|
|
438
|
+
description = arg
|
|
439
|
+
break
|
|
440
|
+
|
|
441
|
+
# Unwrap Optional / Union[T, None]
|
|
442
|
+
origin = get_origin(param_type)
|
|
443
|
+
if origin is Union:
|
|
444
|
+
args = get_args(param_type)
|
|
445
|
+
# Filter out NoneType
|
|
446
|
+
non_none_args = [arg for arg in args if arg is not type(None)]
|
|
447
|
+
if len(non_none_args) == 1:
|
|
448
|
+
param_type = non_none_args[0]
|
|
449
|
+
|
|
450
|
+
param_type_name, enum_values, items = _py_type_to_json_type(param_type)
|
|
451
|
+
|
|
452
|
+
prop = {"type": param_type_name}
|
|
453
|
+
if description:
|
|
454
|
+
prop["description"] = description
|
|
455
|
+
if enum_values:
|
|
456
|
+
prop["enum"] = enum_values
|
|
457
|
+
if items:
|
|
458
|
+
prop["items"] = items
|
|
459
|
+
parameters["properties"][name] = prop
|
|
357
460
|
|
|
358
|
-
parameters["properties"][name] = {"type": param_type_name}
|
|
359
461
|
if param.default == inspect.Parameter.empty:
|
|
360
462
|
parameters["required"].append(name)
|
|
361
463
|
|
|
@@ -363,7 +465,7 @@ def function_to_tool_definition(func):
|
|
|
363
465
|
"type": "function",
|
|
364
466
|
"function": {
|
|
365
467
|
"name": func.__name__,
|
|
366
|
-
"description": func.__doc__ or "",
|
|
468
|
+
"description": (func.__doc__ or "").strip(),
|
|
367
469
|
"parameters": parameters,
|
|
368
470
|
},
|
|
369
471
|
}
|
|
@@ -783,7 +885,7 @@ def chat_to_prompt(chat):
|
|
|
783
885
|
prompt = ""
|
|
784
886
|
if "messages" in chat:
|
|
785
887
|
for message in chat["messages"]:
|
|
786
|
-
if message
|
|
888
|
+
if message.get("role") == "user":
|
|
787
889
|
# if content is string
|
|
788
890
|
if isinstance(message["content"], str):
|
|
789
891
|
if prompt:
|
|
@@ -802,7 +904,7 @@ def chat_to_prompt(chat):
|
|
|
802
904
|
def chat_to_system_prompt(chat):
|
|
803
905
|
if "messages" in chat:
|
|
804
906
|
for message in chat["messages"]:
|
|
805
|
-
if message
|
|
907
|
+
if message.get("role") == "system":
|
|
806
908
|
# if content is string
|
|
807
909
|
if isinstance(message["content"], str):
|
|
808
910
|
return message["content"]
|
|
@@ -830,7 +932,7 @@ def last_user_prompt(chat):
|
|
|
830
932
|
prompt = ""
|
|
831
933
|
if "messages" in chat:
|
|
832
934
|
for message in chat["messages"]:
|
|
833
|
-
if message
|
|
935
|
+
if message.get("role") == "user":
|
|
834
936
|
# if content is string
|
|
835
937
|
if isinstance(message["content"], str):
|
|
836
938
|
prompt = message["content"]
|
|
@@ -1114,8 +1216,8 @@ class OpenAiCompatible:
|
|
|
1114
1216
|
def chat_summary(self, chat):
|
|
1115
1217
|
return chat_summary(chat)
|
|
1116
1218
|
|
|
1117
|
-
def process_chat(self, chat, provider_id=None):
|
|
1118
|
-
return process_chat(chat, provider_id)
|
|
1219
|
+
async def process_chat(self, chat, provider_id=None):
|
|
1220
|
+
return await process_chat(chat, provider_id)
|
|
1119
1221
|
|
|
1120
1222
|
async def chat(self, chat, context=None):
|
|
1121
1223
|
chat["model"] = self.provider_model(chat["model"]) or chat["model"]
|
|
@@ -1170,7 +1272,7 @@ class OpenAiCompatible:
|
|
|
1170
1272
|
if self.enable_thinking is not None:
|
|
1171
1273
|
chat["enable_thinking"] = self.enable_thinking
|
|
1172
1274
|
|
|
1173
|
-
chat = await process_chat(chat, provider_id=self.id)
|
|
1275
|
+
chat = await self.process_chat(chat, provider_id=self.id)
|
|
1174
1276
|
_log(f"POST {self.chat_url}")
|
|
1175
1277
|
_log(chat_summary(chat))
|
|
1176
1278
|
# remove metadata if any (conflicts with some providers, e.g. Z.ai)
|
|
@@ -1202,6 +1304,15 @@ class GroqProvider(OpenAiCompatible):
|
|
|
1202
1304
|
kwargs["api"] = "https://api.groq.com/openai/v1"
|
|
1203
1305
|
super().__init__(**kwargs)
|
|
1204
1306
|
|
|
1307
|
+
async def process_chat(self, chat, provider_id=None):
|
|
1308
|
+
ret = await process_chat(chat, provider_id)
|
|
1309
|
+
chat.pop("modalities", None) # groq doesn't support modalities
|
|
1310
|
+
messages = chat.get("messages", []).copy()
|
|
1311
|
+
for message in messages:
|
|
1312
|
+
message.pop("timestamp", None) # groq doesn't support timestamp
|
|
1313
|
+
ret["messages"] = messages
|
|
1314
|
+
return ret
|
|
1315
|
+
|
|
1205
1316
|
|
|
1206
1317
|
class XaiProvider(OpenAiCompatible):
|
|
1207
1318
|
sdk = "@ai-sdk/xai"
|
|
@@ -1505,6 +1616,7 @@ def g_tool_result(result, function_name: Optional[str] = None, function_args: Op
|
|
|
1505
1616
|
content = []
|
|
1506
1617
|
resources = []
|
|
1507
1618
|
args = function_args or {}
|
|
1619
|
+
_dbg(f"{function_name} tool result type: {type(result)}")
|
|
1508
1620
|
if isinstance(result, dict):
|
|
1509
1621
|
text, res = tool_result_part(result, function_name, args)
|
|
1510
1622
|
if text:
|
|
@@ -1525,9 +1637,118 @@ def g_tool_result(result, function_name: Optional[str] = None, function_args: Op
|
|
|
1525
1637
|
return text, resources
|
|
1526
1638
|
|
|
1527
1639
|
|
|
1640
|
+
def convert_tool_args(function_name, function_args):
|
|
1641
|
+
"""
|
|
1642
|
+
Convert tool arg values to their specified types.
|
|
1643
|
+
types: string, number, integer, boolean, object, array, null
|
|
1644
|
+
example prop_def = [
|
|
1645
|
+
{
|
|
1646
|
+
"type": "string"
|
|
1647
|
+
},
|
|
1648
|
+
{
|
|
1649
|
+
"default": "name",
|
|
1650
|
+
"type": "string",
|
|
1651
|
+
"enum": ["name", "size"]
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
"default": [],
|
|
1655
|
+
"type": "array",
|
|
1656
|
+
"items": {
|
|
1657
|
+
"type": "string"
|
|
1658
|
+
}
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
"anyOf": [
|
|
1662
|
+
{
|
|
1663
|
+
"type": "string"
|
|
1664
|
+
},
|
|
1665
|
+
{
|
|
1666
|
+
"type": "null"
|
|
1667
|
+
}
|
|
1668
|
+
],
|
|
1669
|
+
"default": null,
|
|
1670
|
+
},
|
|
1671
|
+
]
|
|
1672
|
+
"""
|
|
1673
|
+
tool_def = g_app.get_tool_definition(function_name)
|
|
1674
|
+
if not tool_def:
|
|
1675
|
+
return function_args
|
|
1676
|
+
|
|
1677
|
+
if "function" in tool_def and "parameters" in tool_def["function"]:
|
|
1678
|
+
parameters = tool_def.get("function", {}).get("parameters")
|
|
1679
|
+
properties = parameters.get("properties", {})
|
|
1680
|
+
required = parameters.get("required", [])
|
|
1681
|
+
new_args = function_args.copy()
|
|
1682
|
+
|
|
1683
|
+
for key, value in function_args.items():
|
|
1684
|
+
if key in properties and isinstance(value, str):
|
|
1685
|
+
prop_type = properties[key].get("type")
|
|
1686
|
+
str_val = value.strip()
|
|
1687
|
+
|
|
1688
|
+
if str_val == "":
|
|
1689
|
+
if prop_type in ("integer", "number"):
|
|
1690
|
+
new_args[key] = None
|
|
1691
|
+
else:
|
|
1692
|
+
new_args.pop(key)
|
|
1693
|
+
continue
|
|
1694
|
+
|
|
1695
|
+
if prop_type == "integer":
|
|
1696
|
+
with contextlib.suppress(ValueError, TypeError):
|
|
1697
|
+
new_args[key] = int(str_val)
|
|
1698
|
+
|
|
1699
|
+
elif prop_type == "number":
|
|
1700
|
+
with contextlib.suppress(ValueError, TypeError):
|
|
1701
|
+
new_args[key] = float(str_val)
|
|
1702
|
+
|
|
1703
|
+
elif prop_type == "boolean":
|
|
1704
|
+
lower_val = str_val.lower()
|
|
1705
|
+
if lower_val in ("true", "1", "yes"):
|
|
1706
|
+
new_args[key] = True
|
|
1707
|
+
elif lower_val in ("false", "0", "no"):
|
|
1708
|
+
new_args[key] = False
|
|
1709
|
+
|
|
1710
|
+
elif prop_type == "object":
|
|
1711
|
+
if str_val == "":
|
|
1712
|
+
new_args[key] = None
|
|
1713
|
+
else:
|
|
1714
|
+
with contextlib.suppress(json.JSONDecodeError, TypeError):
|
|
1715
|
+
new_args[key] = json.loads(str_val)
|
|
1716
|
+
|
|
1717
|
+
elif prop_type == "array":
|
|
1718
|
+
if str_val == "":
|
|
1719
|
+
new_args[key] = []
|
|
1720
|
+
else:
|
|
1721
|
+
# Simple CSV split for arrays; could be more robust with JSON parsing if wrapped in brackets
|
|
1722
|
+
# Check if it looks like a JSON array
|
|
1723
|
+
if str_val.startswith("[") and str_val.endswith("]"):
|
|
1724
|
+
with contextlib.suppress(json.JSONDecodeError):
|
|
1725
|
+
items = json.loads(str_val)
|
|
1726
|
+
else:
|
|
1727
|
+
items = [s.strip() for s in str_val.split(",")]
|
|
1728
|
+
item_type = properties[key].get("items", {}).get("type")
|
|
1729
|
+
if item_type == "integer":
|
|
1730
|
+
items = [int(i) for i in items]
|
|
1731
|
+
elif item_type == "number":
|
|
1732
|
+
items = [float(i) for i in items]
|
|
1733
|
+
new_args[key] = items
|
|
1734
|
+
|
|
1735
|
+
# Validate required parameters
|
|
1736
|
+
missing = [key for key in required if key not in new_args]
|
|
1737
|
+
if missing:
|
|
1738
|
+
raise ValueError(f"Missing required arguments: {', '.join(missing)}")
|
|
1739
|
+
|
|
1740
|
+
return new_args
|
|
1741
|
+
|
|
1742
|
+
return function_args
|
|
1743
|
+
|
|
1744
|
+
|
|
1528
1745
|
async def g_exec_tool(function_name, function_args):
|
|
1746
|
+
_log(f"g_exec_tool: {function_name}")
|
|
1529
1747
|
if function_name in g_app.tools:
|
|
1530
1748
|
try:
|
|
1749
|
+
# Type conversion based on tool definition
|
|
1750
|
+
function_args = convert_tool_args(function_name, function_args)
|
|
1751
|
+
|
|
1531
1752
|
func = g_app.tools[function_name]
|
|
1532
1753
|
is_async = inspect.iscoroutinefunction(func)
|
|
1533
1754
|
_dbg(f"Executing {'async' if is_async else 'sync'} tool '{function_name}' with args: {function_args}")
|
|
@@ -1536,7 +1757,7 @@ async def g_exec_tool(function_name, function_args):
|
|
|
1536
1757
|
else:
|
|
1537
1758
|
return g_tool_result(func(**function_args), function_name, function_args)
|
|
1538
1759
|
except Exception as e:
|
|
1539
|
-
return f"Error executing tool '{function_name}'
|
|
1760
|
+
return f"Error executing tool '{function_name}':\n{to_error_message(e)}", None
|
|
1540
1761
|
return f"Error: Tool '{function_name}' not found", None
|
|
1541
1762
|
|
|
1542
1763
|
|
|
@@ -1547,7 +1768,7 @@ def group_resources(resources: list):
|
|
|
1547
1768
|
{"images": [{"type": "image_url", "image_url": {"url": "/image.jpg"}}] }
|
|
1548
1769
|
"""
|
|
1549
1770
|
grouped = {}
|
|
1550
|
-
for res in resources:
|
|
1771
|
+
for res in resources or []:
|
|
1551
1772
|
type = res.get("type")
|
|
1552
1773
|
if not type:
|
|
1553
1774
|
continue
|
|
@@ -1576,6 +1797,9 @@ async def g_chat_completion(chat, context=None):
|
|
|
1576
1797
|
if context is None:
|
|
1577
1798
|
context = {"chat": chat, "tools": "all"}
|
|
1578
1799
|
|
|
1800
|
+
if "request_id" not in context:
|
|
1801
|
+
context["request_id"] = str(int(time.time() * 1000))
|
|
1802
|
+
|
|
1579
1803
|
# get first provider that has the model
|
|
1580
1804
|
candidate_providers = [name for name, provider in g_handlers.items() if provider.provider_model(model)]
|
|
1581
1805
|
if len(candidate_providers) == 0:
|
|
@@ -1620,10 +1844,15 @@ async def g_chat_completion(chat, context=None):
|
|
|
1620
1844
|
tool_history = []
|
|
1621
1845
|
final_response = None
|
|
1622
1846
|
|
|
1623
|
-
for
|
|
1847
|
+
for request_count in range(max_iterations):
|
|
1624
1848
|
if should_cancel_thread(context):
|
|
1625
1849
|
return
|
|
1626
1850
|
|
|
1851
|
+
if DEBUG:
|
|
1852
|
+
messages = current_chat.get("messages", [])
|
|
1853
|
+
last_message = messages[-1] if messages else None
|
|
1854
|
+
_dbg(f"Provider {provider_name}, request {request_count}:\n{json.dumps(last_message, indent=2)}")
|
|
1855
|
+
|
|
1627
1856
|
response = await provider.chat(current_chat, context=context)
|
|
1628
1857
|
|
|
1629
1858
|
if should_cancel_thread(context):
|
|
@@ -1722,7 +1951,12 @@ async def g_chat_completion(chat, context=None):
|
|
|
1722
1951
|
continue
|
|
1723
1952
|
|
|
1724
1953
|
# If we get here, all providers failed
|
|
1725
|
-
|
|
1954
|
+
if first_exception:
|
|
1955
|
+
raise first_exception
|
|
1956
|
+
|
|
1957
|
+
e = Exception("All providers failed")
|
|
1958
|
+
await g_app.on_chat_error(e, context or {"chat": chat})
|
|
1959
|
+
raise e
|
|
1726
1960
|
|
|
1727
1961
|
|
|
1728
1962
|
async def cli_chat(chat, tools=None, image=None, audio=None, file=None, args=None, raw=False):
|
|
@@ -1853,12 +2087,18 @@ def config_str(key):
|
|
|
1853
2087
|
return key in g_config and g_config[key] or None
|
|
1854
2088
|
|
|
1855
2089
|
|
|
1856
|
-
def load_config(config, providers, verbose=None):
|
|
2090
|
+
def load_config(config, providers, verbose=None, debug=None, disable_extensions: List[str] = None):
|
|
1857
2091
|
global g_config, g_providers, g_verbose
|
|
1858
2092
|
g_config = config
|
|
1859
2093
|
g_providers = providers
|
|
1860
|
-
if verbose:
|
|
2094
|
+
if verbose is not None:
|
|
1861
2095
|
g_verbose = verbose
|
|
2096
|
+
if debug is not None:
|
|
2097
|
+
global DEBUG
|
|
2098
|
+
DEBUG = debug
|
|
2099
|
+
if disable_extensions:
|
|
2100
|
+
global DISABLE_EXTENSIONS
|
|
2101
|
+
DISABLE_EXTENSIONS = disable_extensions
|
|
1862
2102
|
|
|
1863
2103
|
|
|
1864
2104
|
def init_llms(config, providers):
|
|
@@ -2539,7 +2779,7 @@ class AppExtensions:
|
|
|
2539
2779
|
APIs extensions can use to extend the app
|
|
2540
2780
|
"""
|
|
2541
2781
|
|
|
2542
|
-
def __init__(self, cli_args, extra_args):
|
|
2782
|
+
def __init__(self, cli_args: argparse.Namespace, extra_args: Dict[str, Any]):
|
|
2543
2783
|
self.cli_args = cli_args
|
|
2544
2784
|
self.extra_args = extra_args
|
|
2545
2785
|
self.config = None
|
|
@@ -2563,6 +2803,7 @@ class AppExtensions:
|
|
|
2563
2803
|
self.tool_groups = {}
|
|
2564
2804
|
self.index_headers = []
|
|
2565
2805
|
self.index_footers = []
|
|
2806
|
+
self.allowed_directories = []
|
|
2566
2807
|
self.request_args = {
|
|
2567
2808
|
"image_config": dict, # e.g. { "aspect_ratio": "1:1" }
|
|
2568
2809
|
"temperature": float, # e.g: 0.7
|
|
@@ -2615,12 +2856,28 @@ class AppExtensions:
|
|
|
2615
2856
|
"ctx.mjs": "/ui/ctx.mjs",
|
|
2616
2857
|
}
|
|
2617
2858
|
|
|
2618
|
-
def set_config(self, config):
|
|
2859
|
+
def set_config(self, config: Dict[str, Any]):
|
|
2619
2860
|
self.config = config
|
|
2620
2861
|
self.auth_enabled = self.config.get("auth", {}).get("enabled", False)
|
|
2621
2862
|
|
|
2863
|
+
def set_allowed_directories(
|
|
2864
|
+
self, directories: List[Annotated[str, "List of absolute paths that are allowed to be accessed."]]
|
|
2865
|
+
) -> None:
|
|
2866
|
+
"""Set the list of allowed directories."""
|
|
2867
|
+
self.allowed_directories = [os.path.abspath(d) for d in directories]
|
|
2868
|
+
|
|
2869
|
+
def add_allowed_directory(self, path: str) -> None:
|
|
2870
|
+
"""Add an allowed directory."""
|
|
2871
|
+
abs_path = os.path.abspath(path)
|
|
2872
|
+
if abs_path not in self.allowed_directories:
|
|
2873
|
+
self.allowed_directories.append(abs_path)
|
|
2874
|
+
|
|
2875
|
+
def get_allowed_directories(self) -> List[str]:
|
|
2876
|
+
"""Get the list of allowed directories."""
|
|
2877
|
+
return self.allowed_directories
|
|
2878
|
+
|
|
2622
2879
|
# Authentication middleware helper
|
|
2623
|
-
def check_auth(self, request):
|
|
2880
|
+
def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
|
2624
2881
|
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
2625
2882
|
if not self.auth_enabled:
|
|
2626
2883
|
return True, None
|
|
@@ -2639,7 +2896,7 @@ class AppExtensions:
|
|
|
2639
2896
|
|
|
2640
2897
|
return False, None
|
|
2641
2898
|
|
|
2642
|
-
def get_session(self, request):
|
|
2899
|
+
def get_session(self, request: web.Request) -> Optional[Dict[str, Any]]:
|
|
2643
2900
|
session_token = get_session_token(request)
|
|
2644
2901
|
|
|
2645
2902
|
if not session_token or session_token not in g_sessions:
|
|
@@ -2648,58 +2905,71 @@ class AppExtensions:
|
|
|
2648
2905
|
session_data = g_sessions[session_token]
|
|
2649
2906
|
return session_data
|
|
2650
2907
|
|
|
2651
|
-
def get_username(self, request):
|
|
2908
|
+
def get_username(self, request: web.Request) -> Optional[str]:
|
|
2652
2909
|
session = self.get_session(request)
|
|
2653
2910
|
if session:
|
|
2654
2911
|
return session.get("userName")
|
|
2655
2912
|
return None
|
|
2656
2913
|
|
|
2657
|
-
def get_user_path(self, username=None):
|
|
2914
|
+
def get_user_path(self, username: Optional[str] = None) -> str:
|
|
2658
2915
|
if username:
|
|
2659
2916
|
return home_llms_path(os.path.join("user", username))
|
|
2660
2917
|
return home_llms_path(os.path.join("user", "default"))
|
|
2661
2918
|
|
|
2662
|
-
def
|
|
2919
|
+
def get_providers(self) -> Dict[str, Any]:
|
|
2920
|
+
return g_handlers
|
|
2921
|
+
|
|
2922
|
+
def chat_request(
|
|
2923
|
+
self,
|
|
2924
|
+
template: Optional[str] = None,
|
|
2925
|
+
text: Optional[str] = None,
|
|
2926
|
+
model: Optional[str] = None,
|
|
2927
|
+
system_prompt: Optional[str] = None,
|
|
2928
|
+
) -> Dict[str, Any]:
|
|
2663
2929
|
return g_chat_request(template=template, text=text, model=model, system_prompt=system_prompt)
|
|
2664
2930
|
|
|
2665
|
-
async def chat_completion(self, chat, context=None):
|
|
2931
|
+
async def chat_completion(self, chat: Dict[str, Any], context: Optional[Dict[str, Any]] = None) -> Any:
|
|
2666
2932
|
response = await g_chat_completion(chat, context)
|
|
2667
2933
|
return response
|
|
2668
2934
|
|
|
2669
|
-
def on_cache_saved_filters(self, context):
|
|
2935
|
+
def on_cache_saved_filters(self, context: Dict[str, Any]):
|
|
2670
2936
|
# _log(f"on_cache_saved_filters {len(self.cache_saved_filters)}: {context['url']}")
|
|
2671
2937
|
for filter_func in self.cache_saved_filters:
|
|
2672
2938
|
filter_func(context)
|
|
2673
2939
|
|
|
2674
|
-
async def on_chat_error(self, e, context):
|
|
2940
|
+
async def on_chat_error(self, e: Exception, context: Dict[str, Any]):
|
|
2675
2941
|
# Apply chat error filters
|
|
2676
2942
|
if "stackTrace" not in context:
|
|
2677
2943
|
context["stackTrace"] = traceback.format_exc()
|
|
2678
2944
|
for filter_func in self.chat_error_filters:
|
|
2679
2945
|
try:
|
|
2680
|
-
|
|
2946
|
+
task = filter_func(e, context)
|
|
2947
|
+
if asyncio.isfuture(task):
|
|
2948
|
+
await task
|
|
2681
2949
|
except Exception as e:
|
|
2682
2950
|
_err("chat error filter failed", e)
|
|
2683
2951
|
|
|
2684
|
-
async def on_chat_tool(self, chat, context):
|
|
2952
|
+
async def on_chat_tool(self, chat: Dict[str, Any], context: Dict[str, Any]):
|
|
2685
2953
|
m_len = len(chat.get("messages", []))
|
|
2686
2954
|
t_len = len(self.chat_tool_filters)
|
|
2687
2955
|
_dbg(
|
|
2688
|
-
f"on_tool_call for thread {context.get('threadId'
|
|
2956
|
+
f"on_tool_call for thread {context.get('threadId')} with {m_len} {pluralize('message', m_len)}, invoking {t_len} {pluralize('filter', t_len)}:"
|
|
2689
2957
|
)
|
|
2690
2958
|
for filter_func in self.chat_tool_filters:
|
|
2691
2959
|
await filter_func(chat, context)
|
|
2692
2960
|
|
|
2693
|
-
def
|
|
2961
|
+
def shutdown(self):
|
|
2694
2962
|
if len(self.shutdown_handlers) > 0:
|
|
2695
2963
|
_dbg(f"running {len(self.shutdown_handlers)} shutdown handlers...")
|
|
2696
2964
|
for handler in self.shutdown_handlers:
|
|
2697
2965
|
handler()
|
|
2698
2966
|
|
|
2967
|
+
def exit(self, exit_code: int = 0):
|
|
2968
|
+
self.shutdown()
|
|
2699
2969
|
_dbg(f"exit({exit_code})")
|
|
2700
2970
|
sys.exit(exit_code)
|
|
2701
2971
|
|
|
2702
|
-
def create_chat_with_tools(self, chat, use_tools="all"):
|
|
2972
|
+
def create_chat_with_tools(self, chat: Dict[str, Any], use_tools: str = "all") -> Dict[str, Any]:
|
|
2703
2973
|
# Inject global tools if present
|
|
2704
2974
|
current_chat = chat.copy()
|
|
2705
2975
|
tools = current_chat.get("tools")
|
|
@@ -2713,6 +2983,11 @@ class AppExtensions:
|
|
|
2713
2983
|
if "tools" not in current_chat:
|
|
2714
2984
|
current_chat["tools"] = []
|
|
2715
2985
|
|
|
2986
|
+
_dbg(
|
|
2987
|
+
f"create_chat_with_tools: all_tools:{include_all_tools}, only_tools:{only_tools_list}, chat tools: "
|
|
2988
|
+
+ str(len(current_chat["tools"]))
|
|
2989
|
+
)
|
|
2990
|
+
|
|
2716
2991
|
existing_tools = {t["function"]["name"] for t in current_chat["tools"]}
|
|
2717
2992
|
for tool_def in self.tool_definitions:
|
|
2718
2993
|
name = tool_def["function"]["name"]
|
|
@@ -2720,6 +2995,12 @@ class AppExtensions:
|
|
|
2720
2995
|
current_chat["tools"].append(tool_def)
|
|
2721
2996
|
return current_chat
|
|
2722
2997
|
|
|
2998
|
+
def get_tool_definition(self, name: str) -> Optional[Dict[str, Any]]:
|
|
2999
|
+
for tool_def in self.tool_definitions:
|
|
3000
|
+
if tool_def["function"]["name"] == name:
|
|
3001
|
+
return tool_def
|
|
3002
|
+
return None
|
|
3003
|
+
|
|
2723
3004
|
|
|
2724
3005
|
def handler_name(handler):
|
|
2725
3006
|
if hasattr(handler, "__name__"):
|
|
@@ -2728,7 +3009,7 @@ def handler_name(handler):
|
|
|
2728
3009
|
|
|
2729
3010
|
|
|
2730
3011
|
class ExtensionContext:
|
|
2731
|
-
def __init__(self, app, path):
|
|
3012
|
+
def __init__(self, app: AppExtensions, path: str):
|
|
2732
3013
|
self.app = app
|
|
2733
3014
|
self.cli_args = app.cli_args
|
|
2734
3015
|
self.extra_args = app.extra_args
|
|
@@ -2746,101 +3027,121 @@ class ExtensionContext:
|
|
|
2746
3027
|
self.request_args = app.request_args
|
|
2747
3028
|
self.disabled = False
|
|
2748
3029
|
|
|
2749
|
-
def
|
|
3030
|
+
def set_allowed_directories(
|
|
3031
|
+
self, directories: List[Annotated[str, "List of absolute paths that are allowed to be accessed."]]
|
|
3032
|
+
) -> None:
|
|
3033
|
+
"""Set the list of allowed directories."""
|
|
3034
|
+
self.app.set_allowed_directories(directories)
|
|
3035
|
+
|
|
3036
|
+
def add_allowed_directory(self, path: str) -> None:
|
|
3037
|
+
"""Add an allowed directory."""
|
|
3038
|
+
self.app.add_allowed_directory(path)
|
|
3039
|
+
|
|
3040
|
+
def get_allowed_directories(self) -> List[str]:
|
|
3041
|
+
"""Get the list of allowed directories."""
|
|
3042
|
+
return self.app.get_allowed_directories()
|
|
3043
|
+
|
|
3044
|
+
def chat_to_prompt(self, chat: Dict[str, Any]) -> str:
|
|
2750
3045
|
return chat_to_prompt(chat)
|
|
2751
3046
|
|
|
2752
|
-
def chat_to_system_prompt(self, chat):
|
|
3047
|
+
def chat_to_system_prompt(self, chat: Dict[str, Any]) -> str:
|
|
2753
3048
|
return chat_to_system_prompt(chat)
|
|
2754
3049
|
|
|
2755
|
-
def chat_response_to_message(self, response):
|
|
3050
|
+
def chat_response_to_message(self, response: Dict[str, Any]) -> Dict[str, Any]:
|
|
2756
3051
|
return chat_response_to_message(response)
|
|
2757
3052
|
|
|
2758
|
-
def last_user_prompt(self, chat):
|
|
3053
|
+
def last_user_prompt(self, chat: Dict[str, Any]) -> str:
|
|
2759
3054
|
return last_user_prompt(chat)
|
|
2760
3055
|
|
|
2761
|
-
def to_file_info(
|
|
3056
|
+
def to_file_info(
|
|
3057
|
+
self, chat: Dict[str, Any], info: Optional[Dict[str, Any]] = None, response: Optional[Dict[str, Any]] = None
|
|
3058
|
+
) -> Dict[str, Any]:
|
|
2762
3059
|
return to_file_info(chat, info=info, response=response)
|
|
2763
3060
|
|
|
2764
|
-
def save_image_to_cache(
|
|
3061
|
+
def save_image_to_cache(
|
|
3062
|
+
self, base64_data: Union[str, bytes], filename: str, image_info: Dict[str, Any], ignore_info: bool = False
|
|
3063
|
+
) -> Tuple[str, Optional[Dict[str, Any]]]:
|
|
2765
3064
|
return save_image_to_cache(base64_data, filename, image_info, ignore_info=ignore_info)
|
|
2766
3065
|
|
|
2767
|
-
def save_bytes_to_cache(
|
|
3066
|
+
def save_bytes_to_cache(
|
|
3067
|
+
self, bytes_data: Union[str, bytes], filename: str, file_info: Optional[Dict[str, Any]]
|
|
3068
|
+
) -> Tuple[str, Optional[Dict[str, Any]]]:
|
|
2768
3069
|
return save_bytes_to_cache(bytes_data, filename, file_info)
|
|
2769
3070
|
|
|
2770
|
-
def text_from_file(self, path):
|
|
3071
|
+
def text_from_file(self, path: str) -> str:
|
|
2771
3072
|
return text_from_file(path)
|
|
2772
3073
|
|
|
2773
|
-
def json_from_file(self, path):
|
|
3074
|
+
def json_from_file(self, path: str) -> Any:
|
|
2774
3075
|
return json_from_file(path)
|
|
2775
3076
|
|
|
2776
|
-
def download_file(self, url):
|
|
3077
|
+
def download_file(self, url: str) -> Tuple[bytes, Dict[str, Any]]:
|
|
2777
3078
|
return download_file(url)
|
|
2778
3079
|
|
|
2779
|
-
def session_download_file(self, session, url):
|
|
3080
|
+
def session_download_file(self, session: aiohttp.ClientSession, url: str) -> Tuple[bytes, Dict[str, Any]]:
|
|
2780
3081
|
return session_download_file(session, url)
|
|
2781
3082
|
|
|
2782
|
-
def read_binary_file(self, url):
|
|
3083
|
+
def read_binary_file(self, url: str) -> Tuple[bytes, Dict[str, Any]]:
|
|
2783
3084
|
return read_binary_file(url)
|
|
2784
3085
|
|
|
2785
|
-
def log(self, message):
|
|
3086
|
+
def log(self, message: Any):
|
|
2786
3087
|
if self.verbose:
|
|
2787
3088
|
print(f"[{self.name}] {message}", flush=True)
|
|
2788
3089
|
return message
|
|
2789
3090
|
|
|
2790
|
-
def log_json(self, obj):
|
|
3091
|
+
def log_json(self, obj: Any):
|
|
2791
3092
|
if self.verbose:
|
|
2792
3093
|
print(f"[{self.name}] {json.dumps(obj, indent=2)}", flush=True)
|
|
2793
3094
|
return obj
|
|
2794
3095
|
|
|
2795
|
-
def dbg(self, message):
|
|
3096
|
+
def dbg(self, message: Any):
|
|
2796
3097
|
if self.debug:
|
|
2797
3098
|
print(f"DEBUG [{self.name}]: {message}", flush=True)
|
|
2798
3099
|
|
|
2799
|
-
def err(self, message, e):
|
|
3100
|
+
def err(self, message: str, e: Exception):
|
|
2800
3101
|
print(f"ERROR [{self.name}]: {message}", e)
|
|
2801
3102
|
if self.verbose:
|
|
2802
3103
|
print(traceback.format_exc(), flush=True)
|
|
2803
3104
|
|
|
2804
|
-
def error_message(self, e):
|
|
3105
|
+
def error_message(self, e: Exception) -> str:
|
|
2805
3106
|
return to_error_message(e)
|
|
2806
3107
|
|
|
2807
|
-
def error_response(self, e, stacktrace=False):
|
|
3108
|
+
def error_response(self, e: Exception, stacktrace: bool = False) -> Dict[str, Any]:
|
|
2808
3109
|
return to_error_response(e, stacktrace=stacktrace)
|
|
2809
3110
|
|
|
2810
|
-
def add_provider(self, provider):
|
|
3111
|
+
def add_provider(self, provider: Any):
|
|
2811
3112
|
self.log(f"Registered provider: {provider.__name__}")
|
|
2812
3113
|
self.app.all_providers.append(provider)
|
|
2813
3114
|
|
|
2814
|
-
def register_ui_extension(self, index):
|
|
3115
|
+
def register_ui_extension(self, index: str):
|
|
2815
3116
|
path = os.path.join(self.ext_prefix, index)
|
|
2816
3117
|
self.log(f"Registered UI extension: {path}")
|
|
2817
3118
|
self.app.ui_extensions.append({"id": self.name, "path": path})
|
|
2818
3119
|
|
|
2819
|
-
def register_chat_request_filter(self, handler):
|
|
3120
|
+
def register_chat_request_filter(self, handler: Callable):
|
|
2820
3121
|
self.log(f"Registered chat request filter: {handler_name(handler)}")
|
|
2821
3122
|
self.app.chat_request_filters.append(handler)
|
|
2822
3123
|
|
|
2823
|
-
def register_chat_tool_filter(self, handler):
|
|
3124
|
+
def register_chat_tool_filter(self, handler: Callable):
|
|
2824
3125
|
self.log(f"Registered chat tool filter: {handler_name(handler)}")
|
|
2825
3126
|
self.app.chat_tool_filters.append(handler)
|
|
2826
3127
|
|
|
2827
|
-
def register_chat_response_filter(self, handler):
|
|
3128
|
+
def register_chat_response_filter(self, handler: Callable):
|
|
2828
3129
|
self.log(f"Registered chat response filter: {handler_name(handler)}")
|
|
2829
3130
|
self.app.chat_response_filters.append(handler)
|
|
2830
3131
|
|
|
2831
|
-
def register_chat_error_filter(self, handler):
|
|
3132
|
+
def register_chat_error_filter(self, handler: Callable):
|
|
2832
3133
|
self.log(f"Registered chat error filter: {handler_name(handler)}")
|
|
2833
3134
|
self.app.chat_error_filters.append(handler)
|
|
2834
3135
|
|
|
2835
|
-
def register_cache_saved_filter(self, handler):
|
|
3136
|
+
def register_cache_saved_filter(self, handler: Callable):
|
|
2836
3137
|
self.log(f"Registered cache saved filter: {handler_name(handler)}")
|
|
2837
3138
|
self.app.cache_saved_filters.append(handler)
|
|
2838
3139
|
|
|
2839
|
-
def register_shutdown_handler(self, handler):
|
|
3140
|
+
def register_shutdown_handler(self, handler: Callable):
|
|
2840
3141
|
self.log(f"Registered shutdown handler: {handler_name(handler)}")
|
|
2841
3142
|
self.app.shutdown_handlers.append(handler)
|
|
2842
3143
|
|
|
2843
|
-
def add_static_files(self, ext_dir):
|
|
3144
|
+
def add_static_files(self, ext_dir: str):
|
|
2844
3145
|
self.log(f"Registered static files: {ext_dir}")
|
|
2845
3146
|
|
|
2846
3147
|
async def serve_static(request):
|
|
@@ -2852,57 +3153,66 @@ class ExtensionContext:
|
|
|
2852
3153
|
|
|
2853
3154
|
self.app.server_add_get.append((os.path.join(self.ext_prefix, "{path:.*}"), serve_static, {}))
|
|
2854
3155
|
|
|
2855
|
-
def web_path(self, method, path):
|
|
3156
|
+
def web_path(self, method: str, path: str) -> str:
|
|
2856
3157
|
full_path = os.path.join(self.ext_prefix, path) if path else self.ext_prefix
|
|
2857
3158
|
self.dbg(f"Registered {method:<6} {full_path}")
|
|
2858
3159
|
return full_path
|
|
2859
3160
|
|
|
2860
|
-
def add_get(self, path, handler, **kwargs):
|
|
3161
|
+
def add_get(self, path: str, handler: Callable, **kwargs: Any):
|
|
2861
3162
|
self.app.server_add_get.append((self.web_path("GET", path), handler, kwargs))
|
|
2862
3163
|
|
|
2863
|
-
def add_post(self, path, handler, **kwargs):
|
|
3164
|
+
def add_post(self, path: str, handler: Callable, **kwargs: Any):
|
|
2864
3165
|
self.app.server_add_post.append((self.web_path("POST", path), handler, kwargs))
|
|
2865
3166
|
|
|
2866
|
-
def add_put(self, path, handler, **kwargs):
|
|
3167
|
+
def add_put(self, path: str, handler: Callable, **kwargs: Any):
|
|
2867
3168
|
self.app.server_add_put.append((self.web_path("PUT", path), handler, kwargs))
|
|
2868
3169
|
|
|
2869
|
-
def add_delete(self, path, handler, **kwargs):
|
|
3170
|
+
def add_delete(self, path: str, handler: Callable, **kwargs: Any):
|
|
2870
3171
|
self.app.server_add_delete.append((self.web_path("DELETE", path), handler, kwargs))
|
|
2871
3172
|
|
|
2872
|
-
def add_patch(self, path, handler, **kwargs):
|
|
3173
|
+
def add_patch(self, path: str, handler: Callable, **kwargs: Any):
|
|
2873
3174
|
self.app.server_add_patch.append((self.web_path("PATCH", path), handler, kwargs))
|
|
2874
3175
|
|
|
2875
|
-
def add_importmaps(self, dict):
|
|
3176
|
+
def add_importmaps(self, dict: Dict[str, str]):
|
|
2876
3177
|
self.app.import_maps.update(dict)
|
|
2877
3178
|
|
|
2878
|
-
def add_index_header(self, html):
|
|
3179
|
+
def add_index_header(self, html: str):
|
|
2879
3180
|
self.app.index_headers.append(html)
|
|
2880
3181
|
|
|
2881
|
-
def add_index_footer(self, html):
|
|
3182
|
+
def add_index_footer(self, html: str):
|
|
2882
3183
|
self.app.index_footers.append(html)
|
|
2883
3184
|
|
|
2884
|
-
def
|
|
3185
|
+
def get_home_path(self, name: str = "") -> str:
|
|
3186
|
+
return home_llms_path(name)
|
|
3187
|
+
|
|
3188
|
+
def get_config(self) -> Optional[Dict[str, Any]]:
|
|
2885
3189
|
return g_config
|
|
2886
3190
|
|
|
2887
|
-
def get_cache_path(self, path=""):
|
|
3191
|
+
def get_cache_path(self, path: str = "") -> str:
|
|
2888
3192
|
return get_cache_path(path)
|
|
2889
3193
|
|
|
2890
|
-
def get_file_mime_type(self, filename):
|
|
3194
|
+
def get_file_mime_type(self, filename: str) -> str:
|
|
2891
3195
|
return get_file_mime_type(filename)
|
|
2892
3196
|
|
|
2893
|
-
def chat_request(
|
|
3197
|
+
def chat_request(
|
|
3198
|
+
self,
|
|
3199
|
+
template: Optional[str] = None,
|
|
3200
|
+
text: Optional[str] = None,
|
|
3201
|
+
model: Optional[str] = None,
|
|
3202
|
+
system_prompt: Optional[str] = None,
|
|
3203
|
+
) -> Dict[str, Any]:
|
|
2894
3204
|
return self.app.chat_request(template=template, text=text, model=model, system_prompt=system_prompt)
|
|
2895
3205
|
|
|
2896
|
-
def chat_completion(self, chat, context=None):
|
|
2897
|
-
return self.app.chat_completion(chat, context=context)
|
|
3206
|
+
async def chat_completion(self, chat: Dict[str, Any], context: Optional[Dict[str, Any]] = None) -> Any:
|
|
3207
|
+
return await self.app.chat_completion(chat, context=context)
|
|
2898
3208
|
|
|
2899
|
-
def get_providers(self):
|
|
3209
|
+
def get_providers(self) -> Dict[str, Any]:
|
|
2900
3210
|
return g_handlers
|
|
2901
3211
|
|
|
2902
|
-
def get_provider(self, name):
|
|
3212
|
+
def get_provider(self, name: str) -> Optional[Any]:
|
|
2903
3213
|
return g_handlers.get(name)
|
|
2904
3214
|
|
|
2905
|
-
def sanitize_tool_def(self, tool_def):
|
|
3215
|
+
def sanitize_tool_def(self, tool_def: Dict[str, Any]) -> Dict[str, Any]:
|
|
2906
3216
|
"""
|
|
2907
3217
|
Merge $defs parameter into tool_def property to reduce client/server complexity
|
|
2908
3218
|
"""
|
|
@@ -2942,7 +3252,7 @@ class ExtensionContext:
|
|
|
2942
3252
|
parameters = func_def.get("parameters", {})
|
|
2943
3253
|
defs = parameters.get("$defs", {})
|
|
2944
3254
|
properties = parameters.get("properties", {})
|
|
2945
|
-
for
|
|
3255
|
+
for _, prop_def in properties.items():
|
|
2946
3256
|
if "$ref" in prop_def:
|
|
2947
3257
|
ref = prop_def["$ref"]
|
|
2948
3258
|
if ref.startswith("#/$defs/"):
|
|
@@ -2954,7 +3264,7 @@ class ExtensionContext:
|
|
|
2954
3264
|
del parameters["$defs"]
|
|
2955
3265
|
return tool_def
|
|
2956
3266
|
|
|
2957
|
-
def register_tool(self, func, tool_def=None, group=None):
|
|
3267
|
+
def register_tool(self, func: Callable, tool_def: Optional[Dict[str, Any]] = None, group: Optional[str] = None):
|
|
2958
3268
|
if tool_def is None:
|
|
2959
3269
|
tool_def = function_to_tool_definition(func)
|
|
2960
3270
|
|
|
@@ -2976,54 +3286,58 @@ class ExtensionContext:
|
|
|
2976
3286
|
self.app.tool_groups[group] = []
|
|
2977
3287
|
self.app.tool_groups[group].append(name)
|
|
2978
3288
|
|
|
2979
|
-
def get_tool_definition(self, name):
|
|
2980
|
-
|
|
2981
|
-
if tool_def["function"]["name"] == name:
|
|
2982
|
-
return tool_def
|
|
2983
|
-
return None
|
|
3289
|
+
def get_tool_definition(self, name: str) -> Optional[Dict[str, Any]]:
|
|
3290
|
+
return self.app.get_tool_definition(name)
|
|
2984
3291
|
|
|
2985
|
-
def group_resources(self, resources:
|
|
3292
|
+
def group_resources(self, resources: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
|
2986
3293
|
return group_resources(resources)
|
|
2987
3294
|
|
|
2988
|
-
def check_auth(self, request):
|
|
3295
|
+
def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
|
2989
3296
|
return self.app.check_auth(request)
|
|
2990
3297
|
|
|
2991
|
-
def get_session(self, request):
|
|
3298
|
+
def get_session(self, request: web.Request) -> Optional[Dict[str, Any]]:
|
|
2992
3299
|
return self.app.get_session(request)
|
|
2993
3300
|
|
|
2994
|
-
def get_username(self, request):
|
|
3301
|
+
def get_username(self, request: web.Request) -> Optional[str]:
|
|
2995
3302
|
return self.app.get_username(request)
|
|
2996
3303
|
|
|
2997
|
-
def get_user_path(self, username=None):
|
|
3304
|
+
def get_user_path(self, username: Optional[str] = None) -> str:
|
|
2998
3305
|
return self.app.get_user_path(username)
|
|
2999
3306
|
|
|
3000
|
-
def context_to_username(self, context):
|
|
3307
|
+
def context_to_username(self, context: Optional[Dict[str, Any]]) -> Optional[str]:
|
|
3001
3308
|
if context and "request" in context:
|
|
3002
3309
|
return self.get_username(context["request"])
|
|
3003
3310
|
return None
|
|
3004
3311
|
|
|
3005
|
-
def should_cancel_thread(self, context):
|
|
3312
|
+
def should_cancel_thread(self, context: Dict[str, Any]) -> bool:
|
|
3006
3313
|
return should_cancel_thread(context)
|
|
3007
3314
|
|
|
3008
|
-
def cache_message_inline_data(self, message):
|
|
3315
|
+
def cache_message_inline_data(self, message: Dict[str, Any]):
|
|
3009
3316
|
return cache_message_inline_data(message)
|
|
3010
3317
|
|
|
3011
|
-
async def exec_tool(self, name, args):
|
|
3318
|
+
async def exec_tool(self, name: str, args: Dict[str, Any]) -> Tuple[Optional[str], List[Dict[str, Any]]]:
|
|
3012
3319
|
return await g_exec_tool(name, args)
|
|
3013
3320
|
|
|
3014
|
-
def tool_result(
|
|
3321
|
+
def tool_result(
|
|
3322
|
+
self, result: Any, function_name: Optional[str] = None, function_args: Optional[Dict[str, Any]] = None
|
|
3323
|
+
) -> Dict[str, Any]:
|
|
3015
3324
|
return g_tool_result(result, function_name, function_args)
|
|
3016
3325
|
|
|
3017
|
-
def tool_result_part(
|
|
3326
|
+
def tool_result_part(
|
|
3327
|
+
self,
|
|
3328
|
+
result: Dict[str, Any],
|
|
3329
|
+
function_name: Optional[str] = None,
|
|
3330
|
+
function_args: Optional[Dict[str, Any]] = None,
|
|
3331
|
+
) -> Dict[str, Any]:
|
|
3018
3332
|
return tool_result_part(result, function_name, function_args)
|
|
3019
3333
|
|
|
3020
|
-
def to_content(self, result):
|
|
3334
|
+
def to_content(self, result: Any) -> str:
|
|
3021
3335
|
return to_content(result)
|
|
3022
3336
|
|
|
3023
|
-
def create_chat_with_tools(self, chat, use_tools="all"):
|
|
3337
|
+
def create_chat_with_tools(self, chat: Dict[str, Any], use_tools: str = "all") -> Dict[str, Any]:
|
|
3024
3338
|
return self.app.create_chat_with_tools(chat, use_tools)
|
|
3025
3339
|
|
|
3026
|
-
def chat_to_aspect_ratio(self, chat):
|
|
3340
|
+
def chat_to_aspect_ratio(self, chat: Dict[str, Any]) -> str:
|
|
3027
3341
|
return chat_to_aspect_ratio(chat)
|
|
3028
3342
|
|
|
3029
3343
|
|
|
@@ -3080,7 +3394,21 @@ def get_extensions_dirs():
|
|
|
3080
3394
|
return ret
|
|
3081
3395
|
|
|
3082
3396
|
|
|
3397
|
+
def verify_root_path():
|
|
3398
|
+
global _ROOT
|
|
3399
|
+
_ROOT = os.getenv("LLMS_ROOT", resolve_root())
|
|
3400
|
+
if not _ROOT:
|
|
3401
|
+
print("Resource root not found")
|
|
3402
|
+
exit(1)
|
|
3403
|
+
|
|
3404
|
+
|
|
3083
3405
|
def init_extensions(parser):
|
|
3406
|
+
"""
|
|
3407
|
+
Programmatic entry point for the CLI.
|
|
3408
|
+
Example: cli("ls minimax")
|
|
3409
|
+
"""
|
|
3410
|
+
verify_root_path()
|
|
3411
|
+
|
|
3084
3412
|
"""
|
|
3085
3413
|
Initializes extensions by loading their __init__.py files and calling the __parser__ function if it exists.
|
|
3086
3414
|
"""
|
|
@@ -3235,14 +3563,7 @@ def run_extension_cli():
|
|
|
3235
3563
|
return False
|
|
3236
3564
|
|
|
3237
3565
|
|
|
3238
|
-
def
|
|
3239
|
-
global _ROOT, g_verbose, g_default_model, g_logprefix, g_providers, g_config, g_config_path, g_app
|
|
3240
|
-
|
|
3241
|
-
_ROOT = os.getenv("LLMS_ROOT", resolve_root())
|
|
3242
|
-
if not _ROOT:
|
|
3243
|
-
print("Resource root not found")
|
|
3244
|
-
exit(1)
|
|
3245
|
-
|
|
3566
|
+
def create_arg_parser():
|
|
3246
3567
|
parser = argparse.ArgumentParser(description=f"llms v{VERSION}")
|
|
3247
3568
|
parser.add_argument("--config", default=None, help="Path to config file", metavar="FILE")
|
|
3248
3569
|
parser.add_argument("--providers", default=None, help="Path to models.dev providers file", metavar="FILE")
|
|
@@ -3311,11 +3632,13 @@ def main():
|
|
|
3311
3632
|
help="Update an extension (use 'all' to update all extensions)",
|
|
3312
3633
|
metavar="EXTENSION",
|
|
3313
3634
|
)
|
|
3635
|
+
return parser
|
|
3314
3636
|
|
|
3315
|
-
# Load parser extensions, go through all extensions and load their parser arguments
|
|
3316
|
-
init_extensions(parser)
|
|
3317
3637
|
|
|
3318
|
-
|
|
3638
|
+
def cli_exec(cli_args, extra_args):
|
|
3639
|
+
global _ROOT, g_verbose, g_default_model, g_logprefix, g_providers, g_config, g_config_path, g_app
|
|
3640
|
+
|
|
3641
|
+
verify_root_path()
|
|
3319
3642
|
|
|
3320
3643
|
g_app = AppExtensions(cli_args, extra_args)
|
|
3321
3644
|
|
|
@@ -3351,12 +3674,12 @@ def main():
|
|
|
3351
3674
|
else:
|
|
3352
3675
|
asyncio.run(save_text_url(github_url("providers-extra.json"), home_providers_extra_path))
|
|
3353
3676
|
print(f"Created default extra providers config at {home_providers_extra_path}")
|
|
3354
|
-
|
|
3677
|
+
return ExitCode.SUCCESS
|
|
3355
3678
|
|
|
3356
3679
|
if cli_args.providers:
|
|
3357
3680
|
if not os.path.exists(cli_args.providers):
|
|
3358
3681
|
print(f"providers.json not found at {cli_args.providers}")
|
|
3359
|
-
|
|
3682
|
+
return ExitCode.FAILED
|
|
3360
3683
|
g_providers = json.loads(text_from_file(cli_args.providers))
|
|
3361
3684
|
|
|
3362
3685
|
if cli_args.config:
|
|
@@ -3385,7 +3708,7 @@ def main():
|
|
|
3385
3708
|
if cli_args.update_providers:
|
|
3386
3709
|
asyncio.run(update_providers(home_providers_path))
|
|
3387
3710
|
print(f"Updated {home_providers_path}")
|
|
3388
|
-
|
|
3711
|
+
return ExitCode.SUCCESS
|
|
3389
3712
|
|
|
3390
3713
|
# if home_providers_path is older than 1 day, update providers list
|
|
3391
3714
|
if (
|
|
@@ -3418,7 +3741,7 @@ def main():
|
|
|
3418
3741
|
print(" llms --add <github-user>/<repo>")
|
|
3419
3742
|
|
|
3420
3743
|
asyncio.run(list_extensions())
|
|
3421
|
-
|
|
3744
|
+
return ExitCode.SUCCESS
|
|
3422
3745
|
|
|
3423
3746
|
async def install_extension(name):
|
|
3424
3747
|
# Determine git URL and target directory name
|
|
@@ -3481,7 +3804,7 @@ def main():
|
|
|
3481
3804
|
os.rmdir(target_path)
|
|
3482
3805
|
|
|
3483
3806
|
asyncio.run(install_extension(cli_args.add))
|
|
3484
|
-
|
|
3807
|
+
return ExitCode.SUCCESS
|
|
3485
3808
|
|
|
3486
3809
|
if cli_args.remove is not None:
|
|
3487
3810
|
if cli_args.remove == "ls":
|
|
@@ -3490,11 +3813,11 @@ def main():
|
|
|
3490
3813
|
extensions = os.listdir(extensions_path)
|
|
3491
3814
|
if len(extensions) == 0:
|
|
3492
3815
|
print("No extensions installed.")
|
|
3493
|
-
|
|
3816
|
+
return ExitCode.SUCCESS
|
|
3494
3817
|
print("Installed extensions:")
|
|
3495
3818
|
for extension in extensions:
|
|
3496
3819
|
print(f" {extension}")
|
|
3497
|
-
|
|
3820
|
+
return ExitCode.SUCCESS
|
|
3498
3821
|
# Remove an extension
|
|
3499
3822
|
extension_name = cli_args.remove
|
|
3500
3823
|
extensions_path = get_extensions_path()
|
|
@@ -3502,7 +3825,7 @@ def main():
|
|
|
3502
3825
|
|
|
3503
3826
|
if not os.path.exists(target_path):
|
|
3504
3827
|
print(f"Extension {extension_name} not found at {target_path}")
|
|
3505
|
-
|
|
3828
|
+
return ExitCode.FAILED
|
|
3506
3829
|
|
|
3507
3830
|
print(f"Removing extension: {extension_name}...")
|
|
3508
3831
|
try:
|
|
@@ -3510,9 +3833,9 @@ def main():
|
|
|
3510
3833
|
print(f"Extension {extension_name} removed successfully.")
|
|
3511
3834
|
except Exception as e:
|
|
3512
3835
|
print(f"Failed to remove extension: {e}")
|
|
3513
|
-
|
|
3836
|
+
return ExitCode.FAILED
|
|
3514
3837
|
|
|
3515
|
-
|
|
3838
|
+
return ExitCode.SUCCESS
|
|
3516
3839
|
|
|
3517
3840
|
if cli_args.update:
|
|
3518
3841
|
if cli_args.update == "ls":
|
|
@@ -3521,7 +3844,7 @@ def main():
|
|
|
3521
3844
|
extensions = os.listdir(extensions_path)
|
|
3522
3845
|
if len(extensions) == 0:
|
|
3523
3846
|
print("No extensions installed.")
|
|
3524
|
-
|
|
3847
|
+
return ExitCode.SUCCESS
|
|
3525
3848
|
print("Installed extensions:")
|
|
3526
3849
|
for extension in extensions:
|
|
3527
3850
|
print(f" {extension}")
|
|
@@ -3529,7 +3852,7 @@ def main():
|
|
|
3529
3852
|
print("\nUsage:")
|
|
3530
3853
|
print(" llms --update <extension>")
|
|
3531
3854
|
print(" llms --update all")
|
|
3532
|
-
|
|
3855
|
+
return ExitCode.SUCCESS
|
|
3533
3856
|
|
|
3534
3857
|
async def update_extensions(extension_name):
|
|
3535
3858
|
extensions_path = get_extensions_path()
|
|
@@ -3546,7 +3869,11 @@ def main():
|
|
|
3546
3869
|
_log(result.stdout.decode("utf-8"))
|
|
3547
3870
|
|
|
3548
3871
|
asyncio.run(update_extensions(cli_args.update))
|
|
3549
|
-
|
|
3872
|
+
return ExitCode.SUCCESS
|
|
3873
|
+
|
|
3874
|
+
g_app.add_allowed_directory(home_llms_path(".agent")) # info for agents, e.g: skills
|
|
3875
|
+
g_app.add_allowed_directory(os.getcwd()) # add current directory
|
|
3876
|
+
g_app.add_allowed_directory(tempfile.gettempdir()) # add temp directory
|
|
3550
3877
|
|
|
3551
3878
|
g_app.extensions = install_extensions()
|
|
3552
3879
|
|
|
@@ -3604,7 +3931,7 @@ def main():
|
|
|
3604
3931
|
print(f"\n{model_count} models available from {provider_count} providers")
|
|
3605
3932
|
|
|
3606
3933
|
print_status()
|
|
3607
|
-
|
|
3934
|
+
return ExitCode.SUCCESS
|
|
3608
3935
|
|
|
3609
3936
|
if cli_args.check is not None:
|
|
3610
3937
|
# Check validity of models for a provider
|
|
@@ -3613,7 +3940,7 @@ def main():
|
|
|
3613
3940
|
provider_name = cli_args.check
|
|
3614
3941
|
model_names = extra_args if len(extra_args) > 0 else None
|
|
3615
3942
|
loop.run_until_complete(check_models(provider_name, model_names))
|
|
3616
|
-
|
|
3943
|
+
return ExitCode.SUCCESS
|
|
3617
3944
|
|
|
3618
3945
|
if cli_args.serve is not None:
|
|
3619
3946
|
# Disable inactive providers and save to config before starting server
|
|
@@ -3658,7 +3985,7 @@ def main():
|
|
|
3658
3985
|
print("ERROR: Authentication is enabled but GitHub OAuth is not properly configured.")
|
|
3659
3986
|
print("Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables,")
|
|
3660
3987
|
print("or disable authentication by setting 'auth.enabled' to false in llms.json")
|
|
3661
|
-
|
|
3988
|
+
return ExitCode.FAILED
|
|
3662
3989
|
|
|
3663
3990
|
_log("Authentication enabled - GitHub OAuth configured")
|
|
3664
3991
|
|
|
@@ -4252,7 +4579,7 @@ def main():
|
|
|
4252
4579
|
|
|
4253
4580
|
print(f"Starting server on port {port}...")
|
|
4254
4581
|
web.run_app(app, host="0.0.0.0", port=port, print=_log)
|
|
4255
|
-
|
|
4582
|
+
return ExitCode.SUCCESS
|
|
4256
4583
|
|
|
4257
4584
|
if cli_args.enable is not None:
|
|
4258
4585
|
if cli_args.enable.endswith(","):
|
|
@@ -4271,7 +4598,7 @@ def main():
|
|
|
4271
4598
|
if provider not in g_config["providers"]:
|
|
4272
4599
|
print(f"Provider '{provider}' not found")
|
|
4273
4600
|
print(f"Available providers: {', '.join(g_config['providers'].keys())}")
|
|
4274
|
-
|
|
4601
|
+
return ExitCode.FAILED
|
|
4275
4602
|
if provider in g_config["providers"]:
|
|
4276
4603
|
provider_config, msg = enable_provider(provider)
|
|
4277
4604
|
print(f"\nEnabled provider {provider}:")
|
|
@@ -4282,7 +4609,7 @@ def main():
|
|
|
4282
4609
|
print_status()
|
|
4283
4610
|
if len(msgs) > 0:
|
|
4284
4611
|
print("\n" + "\n".join(msgs))
|
|
4285
|
-
|
|
4612
|
+
return ExitCode.SUCCESS
|
|
4286
4613
|
|
|
4287
4614
|
if cli_args.disable is not None:
|
|
4288
4615
|
if cli_args.disable.endswith(","):
|
|
@@ -4300,24 +4627,24 @@ def main():
|
|
|
4300
4627
|
if provider not in g_config["providers"]:
|
|
4301
4628
|
print(f"Provider {provider} not found")
|
|
4302
4629
|
print(f"Available providers: {', '.join(g_config['providers'].keys())}")
|
|
4303
|
-
|
|
4630
|
+
return ExitCode.FAILED
|
|
4304
4631
|
disable_provider(provider)
|
|
4305
4632
|
print(f"\nDisabled provider {provider}")
|
|
4306
4633
|
|
|
4307
4634
|
print_status()
|
|
4308
|
-
|
|
4635
|
+
return ExitCode.SUCCESS
|
|
4309
4636
|
|
|
4310
4637
|
if cli_args.default is not None:
|
|
4311
4638
|
default_model = cli_args.default
|
|
4312
4639
|
provider_model = get_provider_model(default_model)
|
|
4313
4640
|
if provider_model is None:
|
|
4314
4641
|
print(f"Model {default_model} not found")
|
|
4315
|
-
|
|
4642
|
+
return ExitCode.FAILED
|
|
4316
4643
|
default_text = g_config["defaults"]["text"]
|
|
4317
4644
|
default_text["model"] = default_model
|
|
4318
4645
|
save_config(g_config)
|
|
4319
4646
|
print(f"\nDefault model set to: {default_model}")
|
|
4320
|
-
|
|
4647
|
+
return ExitCode.SUCCESS
|
|
4321
4648
|
|
|
4322
4649
|
if (
|
|
4323
4650
|
cli_args.chat is not None
|
|
@@ -4339,13 +4666,13 @@ def main():
|
|
|
4339
4666
|
template = f"out:{cli_args.out}"
|
|
4340
4667
|
if template not in g_config["defaults"]:
|
|
4341
4668
|
print(f"Template for output modality '{cli_args.out}' not found")
|
|
4342
|
-
|
|
4669
|
+
return ExitCode.FAILED
|
|
4343
4670
|
chat = g_config["defaults"][template]
|
|
4344
4671
|
if cli_args.chat is not None:
|
|
4345
4672
|
chat_path = os.path.join(os.path.dirname(__file__), cli_args.chat)
|
|
4346
4673
|
if not os.path.exists(chat_path):
|
|
4347
4674
|
print(f"Chat request template not found: {chat_path}")
|
|
4348
|
-
|
|
4675
|
+
return ExitCode.FAILED
|
|
4349
4676
|
_log(f"Using chat: {chat_path}")
|
|
4350
4677
|
|
|
4351
4678
|
with open(chat_path) as f:
|
|
@@ -4386,19 +4713,48 @@ def main():
|
|
|
4386
4713
|
raw=cli_args.raw,
|
|
4387
4714
|
)
|
|
4388
4715
|
)
|
|
4389
|
-
|
|
4716
|
+
return ExitCode.SUCCESS
|
|
4390
4717
|
except Exception as e:
|
|
4391
4718
|
print(f"{cli_args.logprefix}Error: {e}")
|
|
4392
4719
|
if cli_args.verbose:
|
|
4393
4720
|
traceback.print_exc()
|
|
4394
|
-
|
|
4721
|
+
return ExitCode.FAILED
|
|
4395
4722
|
|
|
4396
4723
|
handled = run_extension_cli()
|
|
4724
|
+
return ExitCode.SUCCESS if handled else ExitCode.UNHANDLED
|
|
4725
|
+
|
|
4726
|
+
|
|
4727
|
+
def get_app():
|
|
4728
|
+
return g_app
|
|
4397
4729
|
|
|
4398
|
-
|
|
4730
|
+
|
|
4731
|
+
def cli(command_line: str):
|
|
4732
|
+
parser = create_arg_parser()
|
|
4733
|
+
|
|
4734
|
+
# Load parser extensions, go through all extensions and load their parser arguments
|
|
4735
|
+
if load_extensions:
|
|
4736
|
+
init_extensions(parser)
|
|
4737
|
+
|
|
4738
|
+
args = shlex.split(command_line)
|
|
4739
|
+
cli_args, extra_args = parser.parse_known_args(args)
|
|
4740
|
+
return cli_exec(cli_args, extra_args)
|
|
4741
|
+
|
|
4742
|
+
|
|
4743
|
+
def main():
|
|
4744
|
+
parser = create_arg_parser()
|
|
4745
|
+
|
|
4746
|
+
# Load parser extensions, go through all extensions and load their parser arguments
|
|
4747
|
+
init_extensions(parser)
|
|
4748
|
+
|
|
4749
|
+
cli_args, extra_args = parser.parse_known_args()
|
|
4750
|
+
exit_code = cli_exec(cli_args, extra_args)
|
|
4751
|
+
|
|
4752
|
+
if exit_code == ExitCode.UNHANDLED:
|
|
4399
4753
|
# show usage from ArgumentParser
|
|
4400
4754
|
parser.print_help()
|
|
4401
|
-
g_app.exit(0)
|
|
4755
|
+
g_app.exit(0) if g_app else exit(0)
|
|
4756
|
+
|
|
4757
|
+
g_app.exit(exit_code) if g_app else exit(exit_code)
|
|
4402
4758
|
|
|
4403
4759
|
|
|
4404
4760
|
if __name__ == "__main__":
|