llms-py 3.0.15__py3-none-any.whl → 3.0.16__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/computer/__init__.py +59 -0
- llms/extensions/{computer_use → computer}/bash.py +2 -2
- llms/extensions/{computer_use → computer}/edit.py +10 -14
- llms/extensions/computer/filesystem.py +542 -0
- llms/extensions/core_tools/__init__.py +0 -38
- llms/extensions/providers/google.py +57 -30
- llms/extensions/skills/ui/index.mjs +27 -0
- llms/extensions/tools/__init__.py +5 -82
- llms/extensions/tools/ui/index.mjs +92 -4
- llms/main.py +208 -31
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +491 -0
- llms/ui/modules/chat/ChatBody.mjs +64 -9
- llms/ui/modules/chat/index.mjs +103 -91
- {llms_py-3.0.15.dist-info → llms_py-3.0.16.dist-info}/METADATA +1 -1
- {llms_py-3.0.15.dist-info → llms_py-3.0.16.dist-info}/RECORD +25 -24
- {llms_py-3.0.15.dist-info → llms_py-3.0.16.dist-info}/WHEEL +1 -1
- llms/extensions/computer_use/__init__.py +0 -27
- /llms/extensions/{computer_use → computer}/README.md +0 -0
- /llms/extensions/{computer_use → computer}/base.py +0 -0
- /llms/extensions/{computer_use → computer}/computer.py +0 -0
- /llms/extensions/{computer_use → computer}/platform.py +0 -0
- /llms/extensions/{computer_use → computer}/run.py +0 -0
- {llms_py-3.0.15.dist-info → llms_py-3.0.16.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.15.dist-info → llms_py-3.0.16.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.15.dist-info → llms_py-3.0.16.dist-info}/top_level.txt +0 -0
llms/main.py
CHANGED
|
@@ -23,6 +23,7 @@ import shutil
|
|
|
23
23
|
import site
|
|
24
24
|
import subprocess
|
|
25
25
|
import sys
|
|
26
|
+
import tempfile
|
|
26
27
|
import time
|
|
27
28
|
import traceback
|
|
28
29
|
from datetime import datetime
|
|
@@ -56,7 +57,7 @@ try:
|
|
|
56
57
|
except ImportError:
|
|
57
58
|
HAS_PIL = False
|
|
58
59
|
|
|
59
|
-
VERSION = "3.0.
|
|
60
|
+
VERSION = "3.0.16"
|
|
60
61
|
_ROOT = None
|
|
61
62
|
DEBUG = os.getenv("DEBUG") == "1"
|
|
62
63
|
MOCK = os.getenv("MOCK") == "1"
|
|
@@ -379,6 +380,46 @@ def get_literal_values(typ):
|
|
|
379
380
|
return None
|
|
380
381
|
|
|
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
|
+
|
|
382
423
|
def function_to_tool_definition(func):
|
|
383
424
|
type_hints = get_type_hints(func, include_extras=True)
|
|
384
425
|
signature = inspect.signature(func)
|
|
@@ -386,8 +427,6 @@ def function_to_tool_definition(func):
|
|
|
386
427
|
|
|
387
428
|
for name, param in signature.parameters.items():
|
|
388
429
|
param_type = type_hints.get(name, str)
|
|
389
|
-
param_type_name = "string"
|
|
390
|
-
enum_values = None
|
|
391
430
|
description = None
|
|
392
431
|
|
|
393
432
|
# Check for Annotated (for description)
|
|
@@ -399,35 +438,24 @@ def function_to_tool_definition(func):
|
|
|
399
438
|
description = arg
|
|
400
439
|
break
|
|
401
440
|
|
|
402
|
-
#
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
#
|
|
407
|
-
|
|
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]
|
|
408
449
|
|
|
409
|
-
|
|
410
|
-
# Infer type from the first value
|
|
411
|
-
value_type = type(enum_values[0])
|
|
412
|
-
if value_type is int:
|
|
413
|
-
param_type_name = "integer"
|
|
414
|
-
elif value_type is float:
|
|
415
|
-
param_type_name = "number"
|
|
416
|
-
elif value_type is bool:
|
|
417
|
-
param_type_name = "boolean"
|
|
418
|
-
|
|
419
|
-
elif param_type is int:
|
|
420
|
-
param_type_name = "integer"
|
|
421
|
-
elif param_type is float:
|
|
422
|
-
param_type_name = "number"
|
|
423
|
-
elif param_type is bool:
|
|
424
|
-
param_type_name = "boolean"
|
|
450
|
+
param_type_name, enum_values, items = _py_type_to_json_type(param_type)
|
|
425
451
|
|
|
426
452
|
prop = {"type": param_type_name}
|
|
427
453
|
if description:
|
|
428
454
|
prop["description"] = description
|
|
429
455
|
if enum_values:
|
|
430
456
|
prop["enum"] = enum_values
|
|
457
|
+
if items:
|
|
458
|
+
prop["items"] = items
|
|
431
459
|
parameters["properties"][name] = prop
|
|
432
460
|
|
|
433
461
|
if param.default == inspect.Parameter.empty:
|
|
@@ -1600,9 +1628,118 @@ def g_tool_result(result, function_name: Optional[str] = None, function_args: Op
|
|
|
1600
1628
|
return text, resources
|
|
1601
1629
|
|
|
1602
1630
|
|
|
1631
|
+
def convert_tool_args(function_name, function_args):
|
|
1632
|
+
"""
|
|
1633
|
+
Convert tool arg values to their specified types.
|
|
1634
|
+
types: string, number, integer, boolean, object, array, null
|
|
1635
|
+
example prop_def = [
|
|
1636
|
+
{
|
|
1637
|
+
"type": "string"
|
|
1638
|
+
},
|
|
1639
|
+
{
|
|
1640
|
+
"default": "name",
|
|
1641
|
+
"type": "string",
|
|
1642
|
+
"enum": ["name", "size"]
|
|
1643
|
+
},
|
|
1644
|
+
{
|
|
1645
|
+
"default": [],
|
|
1646
|
+
"type": "array",
|
|
1647
|
+
"items": {
|
|
1648
|
+
"type": "string"
|
|
1649
|
+
}
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
"anyOf": [
|
|
1653
|
+
{
|
|
1654
|
+
"type": "string"
|
|
1655
|
+
},
|
|
1656
|
+
{
|
|
1657
|
+
"type": "null"
|
|
1658
|
+
}
|
|
1659
|
+
],
|
|
1660
|
+
"default": null,
|
|
1661
|
+
},
|
|
1662
|
+
]
|
|
1663
|
+
"""
|
|
1664
|
+
tool_def = g_app.get_tool_definition(function_name)
|
|
1665
|
+
if not tool_def:
|
|
1666
|
+
return function_args
|
|
1667
|
+
|
|
1668
|
+
if "function" in tool_def and "parameters" in tool_def["function"]:
|
|
1669
|
+
parameters = tool_def.get("function", {}).get("parameters")
|
|
1670
|
+
properties = parameters.get("properties", {})
|
|
1671
|
+
required = parameters.get("required", [])
|
|
1672
|
+
new_args = function_args.copy()
|
|
1673
|
+
|
|
1674
|
+
for key, value in function_args.items():
|
|
1675
|
+
if key in properties and isinstance(value, str):
|
|
1676
|
+
prop_type = properties[key].get("type")
|
|
1677
|
+
str_val = value.strip()
|
|
1678
|
+
|
|
1679
|
+
if str_val == "":
|
|
1680
|
+
if prop_type in ("integer", "number"):
|
|
1681
|
+
new_args[key] = None
|
|
1682
|
+
else:
|
|
1683
|
+
new_args.pop(key)
|
|
1684
|
+
continue
|
|
1685
|
+
|
|
1686
|
+
if prop_type == "integer":
|
|
1687
|
+
with contextlib.suppress(ValueError, TypeError):
|
|
1688
|
+
new_args[key] = int(str_val)
|
|
1689
|
+
|
|
1690
|
+
elif prop_type == "number":
|
|
1691
|
+
with contextlib.suppress(ValueError, TypeError):
|
|
1692
|
+
new_args[key] = float(str_val)
|
|
1693
|
+
|
|
1694
|
+
elif prop_type == "boolean":
|
|
1695
|
+
lower_val = str_val.lower()
|
|
1696
|
+
if lower_val in ("true", "1", "yes"):
|
|
1697
|
+
new_args[key] = True
|
|
1698
|
+
elif lower_val in ("false", "0", "no"):
|
|
1699
|
+
new_args[key] = False
|
|
1700
|
+
|
|
1701
|
+
elif prop_type == "object":
|
|
1702
|
+
if str_val == "":
|
|
1703
|
+
new_args[key] = None
|
|
1704
|
+
else:
|
|
1705
|
+
with contextlib.suppress(json.JSONDecodeError, TypeError):
|
|
1706
|
+
new_args[key] = json.loads(str_val)
|
|
1707
|
+
|
|
1708
|
+
elif prop_type == "array":
|
|
1709
|
+
if str_val == "":
|
|
1710
|
+
new_args[key] = []
|
|
1711
|
+
else:
|
|
1712
|
+
# Simple CSV split for arrays; could be more robust with JSON parsing if wrapped in brackets
|
|
1713
|
+
# Check if it looks like a JSON array
|
|
1714
|
+
if str_val.startswith("[") and str_val.endswith("]"):
|
|
1715
|
+
with contextlib.suppress(json.JSONDecodeError):
|
|
1716
|
+
items = json.loads(str_val)
|
|
1717
|
+
else:
|
|
1718
|
+
items = [s.strip() for s in str_val.split(",")]
|
|
1719
|
+
item_type = properties[key].get("items", {}).get("type")
|
|
1720
|
+
if item_type == "integer":
|
|
1721
|
+
items = [int(i) for i in items]
|
|
1722
|
+
elif item_type == "number":
|
|
1723
|
+
items = [float(i) for i in items]
|
|
1724
|
+
new_args[key] = items
|
|
1725
|
+
|
|
1726
|
+
# Validate required parameters
|
|
1727
|
+
missing = [key for key in required if key not in new_args]
|
|
1728
|
+
if missing:
|
|
1729
|
+
raise ValueError(f"Missing required arguments: {', '.join(missing)}")
|
|
1730
|
+
|
|
1731
|
+
return new_args
|
|
1732
|
+
|
|
1733
|
+
return function_args
|
|
1734
|
+
|
|
1735
|
+
|
|
1603
1736
|
async def g_exec_tool(function_name, function_args):
|
|
1737
|
+
_log(f"g_exec_tool: {function_name}")
|
|
1604
1738
|
if function_name in g_app.tools:
|
|
1605
1739
|
try:
|
|
1740
|
+
# Type conversion based on tool definition
|
|
1741
|
+
function_args = convert_tool_args(function_name, function_args)
|
|
1742
|
+
|
|
1606
1743
|
func = g_app.tools[function_name]
|
|
1607
1744
|
is_async = inspect.iscoroutinefunction(func)
|
|
1608
1745
|
_dbg(f"Executing {'async' if is_async else 'sync'} tool '{function_name}' with args: {function_args}")
|
|
@@ -1611,7 +1748,7 @@ async def g_exec_tool(function_name, function_args):
|
|
|
1611
1748
|
else:
|
|
1612
1749
|
return g_tool_result(func(**function_args), function_name, function_args)
|
|
1613
1750
|
except Exception as e:
|
|
1614
|
-
return f"Error executing tool '{function_name}'
|
|
1751
|
+
return f"Error executing tool '{function_name}':\n{to_error_message(e)}", None
|
|
1615
1752
|
return f"Error: Tool '{function_name}' not found", None
|
|
1616
1753
|
|
|
1617
1754
|
|
|
@@ -2657,6 +2794,7 @@ class AppExtensions:
|
|
|
2657
2794
|
self.tool_groups = {}
|
|
2658
2795
|
self.index_headers = []
|
|
2659
2796
|
self.index_footers = []
|
|
2797
|
+
self.allowed_directories = []
|
|
2660
2798
|
self.request_args = {
|
|
2661
2799
|
"image_config": dict, # e.g. { "aspect_ratio": "1:1" }
|
|
2662
2800
|
"temperature": float, # e.g: 0.7
|
|
@@ -2713,6 +2851,22 @@ class AppExtensions:
|
|
|
2713
2851
|
self.config = config
|
|
2714
2852
|
self.auth_enabled = self.config.get("auth", {}).get("enabled", False)
|
|
2715
2853
|
|
|
2854
|
+
def set_allowed_directories(
|
|
2855
|
+
self, directories: List[Annotated[str, "List of absolute paths that are allowed to be accessed."]]
|
|
2856
|
+
) -> None:
|
|
2857
|
+
"""Set the list of allowed directories."""
|
|
2858
|
+
self.allowed_directories = [os.path.abspath(d) for d in directories]
|
|
2859
|
+
|
|
2860
|
+
def add_allowed_directory(self, path: str) -> None:
|
|
2861
|
+
"""Add an allowed directory."""
|
|
2862
|
+
abs_path = os.path.abspath(path)
|
|
2863
|
+
if abs_path not in self.allowed_directories:
|
|
2864
|
+
self.allowed_directories.append(abs_path)
|
|
2865
|
+
|
|
2866
|
+
def get_allowed_directories(self) -> List[str]:
|
|
2867
|
+
"""Get the list of allowed directories."""
|
|
2868
|
+
return self.allowed_directories
|
|
2869
|
+
|
|
2716
2870
|
# Authentication middleware helper
|
|
2717
2871
|
def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
|
2718
2872
|
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
@@ -2780,7 +2934,9 @@ class AppExtensions:
|
|
|
2780
2934
|
context["stackTrace"] = traceback.format_exc()
|
|
2781
2935
|
for filter_func in self.chat_error_filters:
|
|
2782
2936
|
try:
|
|
2783
|
-
|
|
2937
|
+
task = filter_func(e, context)
|
|
2938
|
+
if asyncio.isfuture(task):
|
|
2939
|
+
await task
|
|
2784
2940
|
except Exception as e:
|
|
2785
2941
|
_err("chat error filter failed", e)
|
|
2786
2942
|
|
|
@@ -2825,6 +2981,12 @@ class AppExtensions:
|
|
|
2825
2981
|
current_chat["tools"].append(tool_def)
|
|
2826
2982
|
return current_chat
|
|
2827
2983
|
|
|
2984
|
+
def get_tool_definition(self, name: str) -> Optional[Dict[str, Any]]:
|
|
2985
|
+
for tool_def in self.tool_definitions:
|
|
2986
|
+
if tool_def["function"]["name"] == name:
|
|
2987
|
+
return tool_def
|
|
2988
|
+
return None
|
|
2989
|
+
|
|
2828
2990
|
|
|
2829
2991
|
def handler_name(handler):
|
|
2830
2992
|
if hasattr(handler, "__name__"):
|
|
@@ -2851,6 +3013,20 @@ class ExtensionContext:
|
|
|
2851
3013
|
self.request_args = app.request_args
|
|
2852
3014
|
self.disabled = False
|
|
2853
3015
|
|
|
3016
|
+
def set_allowed_directories(
|
|
3017
|
+
self, directories: List[Annotated[str, "List of absolute paths that are allowed to be accessed."]]
|
|
3018
|
+
) -> None:
|
|
3019
|
+
"""Set the list of allowed directories."""
|
|
3020
|
+
self.app.set_allowed_directories(directories)
|
|
3021
|
+
|
|
3022
|
+
def add_allowed_directory(self, path: str) -> None:
|
|
3023
|
+
"""Add an allowed directory."""
|
|
3024
|
+
self.app.add_allowed_directory(path)
|
|
3025
|
+
|
|
3026
|
+
def get_allowed_directories(self) -> List[str]:
|
|
3027
|
+
"""Get the list of allowed directories."""
|
|
3028
|
+
return self.app.get_allowed_directories()
|
|
3029
|
+
|
|
2854
3030
|
def chat_to_prompt(self, chat: Dict[str, Any]) -> str:
|
|
2855
3031
|
return chat_to_prompt(chat)
|
|
2856
3032
|
|
|
@@ -3097,10 +3273,7 @@ class ExtensionContext:
|
|
|
3097
3273
|
self.app.tool_groups[group].append(name)
|
|
3098
3274
|
|
|
3099
3275
|
def get_tool_definition(self, name: str) -> Optional[Dict[str, Any]]:
|
|
3100
|
-
|
|
3101
|
-
if tool_def["function"]["name"] == name:
|
|
3102
|
-
return tool_def
|
|
3103
|
-
return None
|
|
3276
|
+
return self.app.get_tool_definition(name)
|
|
3104
3277
|
|
|
3105
3278
|
def group_resources(self, resources: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
|
3106
3279
|
return group_resources(resources)
|
|
@@ -3684,6 +3857,10 @@ def cli_exec(cli_args, extra_args):
|
|
|
3684
3857
|
asyncio.run(update_extensions(cli_args.update))
|
|
3685
3858
|
return ExitCode.SUCCESS
|
|
3686
3859
|
|
|
3860
|
+
g_app.add_allowed_directory(home_llms_path(".agent")) # info for agents, e.g: skills
|
|
3861
|
+
g_app.add_allowed_directory(os.getcwd()) # add current directory
|
|
3862
|
+
g_app.add_allowed_directory(tempfile.gettempdir()) # add temp directory
|
|
3863
|
+
|
|
3687
3864
|
g_app.extensions = install_extensions()
|
|
3688
3865
|
|
|
3689
3866
|
# Use a persistent event loop to ensure async connections (like MCP)
|