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.
Files changed (29) hide show
  1. llms/extensions/app/__init__.py +0 -1
  2. llms/extensions/app/db.py +5 -1
  3. llms/extensions/computer/__init__.py +59 -0
  4. llms/extensions/{computer_use → computer}/bash.py +2 -2
  5. llms/extensions/{computer_use → computer}/edit.py +10 -14
  6. llms/extensions/computer/filesystem.py +542 -0
  7. llms/extensions/core_tools/__init__.py +0 -38
  8. llms/extensions/providers/cerebras.py +0 -1
  9. llms/extensions/providers/google.py +57 -30
  10. llms/extensions/skills/ui/index.mjs +27 -0
  11. llms/extensions/tools/__init__.py +5 -82
  12. llms/extensions/tools/ui/index.mjs +92 -4
  13. llms/main.py +225 -34
  14. llms/ui/ai.mjs +1 -1
  15. llms/ui/app.css +491 -0
  16. llms/ui/modules/chat/ChatBody.mjs +64 -9
  17. llms/ui/modules/chat/index.mjs +103 -91
  18. {llms_py-3.0.15.dist-info → llms_py-3.0.17.dist-info}/METADATA +1 -1
  19. {llms_py-3.0.15.dist-info → llms_py-3.0.17.dist-info}/RECORD +28 -27
  20. {llms_py-3.0.15.dist-info → llms_py-3.0.17.dist-info}/WHEEL +1 -1
  21. llms/extensions/computer_use/__init__.py +0 -27
  22. /llms/extensions/{computer_use → computer}/README.md +0 -0
  23. /llms/extensions/{computer_use → computer}/base.py +0 -0
  24. /llms/extensions/{computer_use → computer}/computer.py +0 -0
  25. /llms/extensions/{computer_use → computer}/platform.py +0 -0
  26. /llms/extensions/{computer_use → computer}/run.py +0 -0
  27. {llms_py-3.0.15.dist-info → llms_py-3.0.17.dist-info}/entry_points.txt +0 -0
  28. {llms_py-3.0.15.dist-info → llms_py-3.0.17.dist-info}/licenses/LICENSE +0 -0
  29. {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.15"
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
- # Check for Enum
403
- if inspect.isclass(param_type) and issubclass(param_type, Enum):
404
- enum_values = [e.value for e in param_type]
405
- else:
406
- # Check for Literal / Union[Literal]
407
- enum_values = get_literal_values(param_type)
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
- if enum_values:
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}': {to_error_message(e)}", None
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
- await filter_func(e, context)
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
- for tool_def in self.app.tool_definitions:
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)
llms/ui/ai.mjs CHANGED
@@ -6,7 +6,7 @@ const headers = { 'Accept': 'application/json' }
6
6
  const prefsKey = 'llms.prefs'
7
7
 
8
8
  export const o = {
9
- version: '3.0.15',
9
+ version: '3.0.17',
10
10
  base,
11
11
  prefsKey,
12
12
  welcome: 'Welcome to llms.py',