llms-py 3.0.15__py3-none-any.whl → 3.0.17__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 +5 -1
- 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/cerebras.py +0 -1
- 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 +225 -34
- 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.17.dist-info}/METADATA +1 -1
- {llms_py-3.0.15.dist-info → llms_py-3.0.17.dist-info}/RECORD +28 -27
- {llms_py-3.0.15.dist-info → llms_py-3.0.17.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.17.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.15.dist-info → llms_py-3.0.17.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.15.dist-info → llms_py-3.0.17.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.17"
|
|
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:
|
|
@@ -1188,8 +1216,8 @@ class OpenAiCompatible:
|
|
|
1188
1216
|
def chat_summary(self, chat):
|
|
1189
1217
|
return chat_summary(chat)
|
|
1190
1218
|
|
|
1191
|
-
def process_chat(self, chat, provider_id=None):
|
|
1192
|
-
return process_chat(chat, provider_id)
|
|
1219
|
+
async def process_chat(self, chat, provider_id=None):
|
|
1220
|
+
return await process_chat(chat, provider_id)
|
|
1193
1221
|
|
|
1194
1222
|
async def chat(self, chat, context=None):
|
|
1195
1223
|
chat["model"] = self.provider_model(chat["model"]) or chat["model"]
|
|
@@ -1244,7 +1272,7 @@ class OpenAiCompatible:
|
|
|
1244
1272
|
if self.enable_thinking is not None:
|
|
1245
1273
|
chat["enable_thinking"] = self.enable_thinking
|
|
1246
1274
|
|
|
1247
|
-
chat = await process_chat(chat, provider_id=self.id)
|
|
1275
|
+
chat = await self.process_chat(chat, provider_id=self.id)
|
|
1248
1276
|
_log(f"POST {self.chat_url}")
|
|
1249
1277
|
_log(chat_summary(chat))
|
|
1250
1278
|
# remove metadata if any (conflicts with some providers, e.g. Z.ai)
|
|
@@ -1276,6 +1304,15 @@ class GroqProvider(OpenAiCompatible):
|
|
|
1276
1304
|
kwargs["api"] = "https://api.groq.com/openai/v1"
|
|
1277
1305
|
super().__init__(**kwargs)
|
|
1278
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
|
+
|
|
1279
1316
|
|
|
1280
1317
|
class XaiProvider(OpenAiCompatible):
|
|
1281
1318
|
sdk = "@ai-sdk/xai"
|
|
@@ -1600,9 +1637,118 @@ def g_tool_result(result, function_name: Optional[str] = None, function_args: Op
|
|
|
1600
1637
|
return text, resources
|
|
1601
1638
|
|
|
1602
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
|
+
|
|
1603
1745
|
async def g_exec_tool(function_name, function_args):
|
|
1746
|
+
_log(f"g_exec_tool: {function_name}")
|
|
1604
1747
|
if function_name in g_app.tools:
|
|
1605
1748
|
try:
|
|
1749
|
+
# Type conversion based on tool definition
|
|
1750
|
+
function_args = convert_tool_args(function_name, function_args)
|
|
1751
|
+
|
|
1606
1752
|
func = g_app.tools[function_name]
|
|
1607
1753
|
is_async = inspect.iscoroutinefunction(func)
|
|
1608
1754
|
_dbg(f"Executing {'async' if is_async else 'sync'} tool '{function_name}' with args: {function_args}")
|
|
@@ -1611,7 +1757,7 @@ async def g_exec_tool(function_name, function_args):
|
|
|
1611
1757
|
else:
|
|
1612
1758
|
return g_tool_result(func(**function_args), function_name, function_args)
|
|
1613
1759
|
except Exception as e:
|
|
1614
|
-
return f"Error executing tool '{function_name}'
|
|
1760
|
+
return f"Error executing tool '{function_name}':\n{to_error_message(e)}", None
|
|
1615
1761
|
return f"Error: Tool '{function_name}' not found", None
|
|
1616
1762
|
|
|
1617
1763
|
|
|
@@ -2657,6 +2803,7 @@ class AppExtensions:
|
|
|
2657
2803
|
self.tool_groups = {}
|
|
2658
2804
|
self.index_headers = []
|
|
2659
2805
|
self.index_footers = []
|
|
2806
|
+
self.allowed_directories = []
|
|
2660
2807
|
self.request_args = {
|
|
2661
2808
|
"image_config": dict, # e.g. { "aspect_ratio": "1:1" }
|
|
2662
2809
|
"temperature": float, # e.g: 0.7
|
|
@@ -2713,6 +2860,22 @@ class AppExtensions:
|
|
|
2713
2860
|
self.config = config
|
|
2714
2861
|
self.auth_enabled = self.config.get("auth", {}).get("enabled", False)
|
|
2715
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
|
+
|
|
2716
2879
|
# Authentication middleware helper
|
|
2717
2880
|
def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
|
2718
2881
|
"""Check if request is authenticated. Returns (is_authenticated, user_data)"""
|
|
@@ -2780,7 +2943,9 @@ class AppExtensions:
|
|
|
2780
2943
|
context["stackTrace"] = traceback.format_exc()
|
|
2781
2944
|
for filter_func in self.chat_error_filters:
|
|
2782
2945
|
try:
|
|
2783
|
-
|
|
2946
|
+
task = filter_func(e, context)
|
|
2947
|
+
if asyncio.isfuture(task):
|
|
2948
|
+
await task
|
|
2784
2949
|
except Exception as e:
|
|
2785
2950
|
_err("chat error filter failed", e)
|
|
2786
2951
|
|
|
@@ -2818,6 +2983,11 @@ class AppExtensions:
|
|
|
2818
2983
|
if "tools" not in current_chat:
|
|
2819
2984
|
current_chat["tools"] = []
|
|
2820
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
|
+
|
|
2821
2991
|
existing_tools = {t["function"]["name"] for t in current_chat["tools"]}
|
|
2822
2992
|
for tool_def in self.tool_definitions:
|
|
2823
2993
|
name = tool_def["function"]["name"]
|
|
@@ -2825,6 +2995,12 @@ class AppExtensions:
|
|
|
2825
2995
|
current_chat["tools"].append(tool_def)
|
|
2826
2996
|
return current_chat
|
|
2827
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
|
+
|
|
2828
3004
|
|
|
2829
3005
|
def handler_name(handler):
|
|
2830
3006
|
if hasattr(handler, "__name__"):
|
|
@@ -2851,6 +3027,20 @@ class ExtensionContext:
|
|
|
2851
3027
|
self.request_args = app.request_args
|
|
2852
3028
|
self.disabled = False
|
|
2853
3029
|
|
|
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
|
+
|
|
2854
3044
|
def chat_to_prompt(self, chat: Dict[str, Any]) -> str:
|
|
2855
3045
|
return chat_to_prompt(chat)
|
|
2856
3046
|
|
|
@@ -3097,10 +3287,7 @@ class ExtensionContext:
|
|
|
3097
3287
|
self.app.tool_groups[group].append(name)
|
|
3098
3288
|
|
|
3099
3289
|
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
|
|
3290
|
+
return self.app.get_tool_definition(name)
|
|
3104
3291
|
|
|
3105
3292
|
def group_resources(self, resources: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
|
3106
3293
|
return group_resources(resources)
|
|
@@ -3684,6 +3871,10 @@ def cli_exec(cli_args, extra_args):
|
|
|
3684
3871
|
asyncio.run(update_extensions(cli_args.update))
|
|
3685
3872
|
return ExitCode.SUCCESS
|
|
3686
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
|
|
3877
|
+
|
|
3687
3878
|
g_app.extensions = install_extensions()
|
|
3688
3879
|
|
|
3689
3880
|
# Use a persistent event loop to ensure async connections (like MCP)
|