llms-py 3.0.14__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.
Files changed (38) hide show
  1. llms/extensions/app/ui/threadStore.mjs +10 -3
  2. llms/extensions/computer/README.md +96 -0
  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/google.py +57 -30
  9. llms/extensions/skills/LICENSE +202 -0
  10. llms/extensions/skills/__init__.py +130 -0
  11. llms/extensions/skills/errors.py +25 -0
  12. llms/extensions/skills/models.py +39 -0
  13. llms/extensions/skills/parser.py +178 -0
  14. llms/extensions/skills/ui/index.mjs +362 -0
  15. llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
  16. llms/extensions/skills/validator.py +177 -0
  17. llms/extensions/system_prompts/ui/index.mjs +6 -10
  18. llms/extensions/tools/__init__.py +5 -82
  19. llms/extensions/tools/ui/index.mjs +93 -5
  20. llms/main.py +215 -35
  21. llms/ui/ai.mjs +1 -1
  22. llms/ui/app.css +527 -22
  23. llms/ui/ctx.mjs +53 -6
  24. llms/ui/modules/chat/ChatBody.mjs +186 -24
  25. llms/ui/modules/chat/index.mjs +107 -103
  26. llms/ui/tailwind.input.css +10 -0
  27. llms/ui/utils.mjs +25 -1
  28. {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/METADATA +1 -1
  29. {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/RECORD +37 -27
  30. {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/WHEEL +1 -1
  31. llms/extensions/computer_use/__init__.py +0 -27
  32. /llms/extensions/{computer_use → computer}/base.py +0 -0
  33. /llms/extensions/{computer_use → computer}/computer.py +0 -0
  34. /llms/extensions/{computer_use → computer}/platform.py +0 -0
  35. /llms/extensions/{computer_use → computer}/run.py +0 -0
  36. {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/entry_points.txt +0 -0
  37. {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/licenses/LICENSE +0 -0
  38. {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/top_level.txt +0 -0
@@ -14,72 +14,6 @@ def install(ctx):
14
14
 
15
15
  ctx.add_get("", tools_handler)
16
16
 
17
- def prop_def_types(prop_def):
18
- prop_type = prop_def.get("type")
19
- if not prop_type:
20
- any_of = prop_def.get("anyOf")
21
- if any_of:
22
- return [item.get("type") for item in any_of]
23
- else:
24
- return []
25
- return [prop_type]
26
-
27
- def tool_prop_value(value, prop_def):
28
- """
29
- Convert a value to the specified type.
30
- types: string, number, integer, boolean, object, array, null
31
- example prop_def = [
32
- {
33
- "type": "string"
34
- },
35
- {
36
- "default": "name",
37
- "type": "string",
38
- "enum": ["name", "size"]
39
- },
40
- {
41
- "default": [],
42
- "type": "array",
43
- "items": {
44
- "type": "string"
45
- }
46
- },
47
- {
48
- "anyOf": [
49
- {
50
- "type": "string"
51
- },
52
- {
53
- "type": "null"
54
- }
55
- ],
56
- "default": null,
57
- },
58
- ]
59
- """
60
- if value is None:
61
- default = prop_def.get("default")
62
- if default is not None:
63
- default = tool_prop_value(default, prop_def)
64
- return default
65
-
66
- prop_types = prop_def_types(prop_def)
67
- if "integer" in prop_types:
68
- return int(value)
69
- elif "number" in prop_types:
70
- return float(value)
71
- elif "boolean" in prop_types:
72
- return bool(value)
73
- elif "object" in prop_types:
74
- return value if isinstance(value, dict) else json.loads(value)
75
- elif "array" in prop_types:
76
- return value if isinstance(value, list) else value.split(",")
77
- else:
78
- enum = prop_def.get("enum")
79
- if enum and value not in enum:
80
- raise Exception(f"'{value}' is not in {enum}")
81
- return value
82
-
83
17
  async def exec_handler(request):
84
18
  name = request.match_info.get("name")
85
19
  args = await request.json()
@@ -93,27 +27,16 @@ def install(ctx):
93
27
  raise Exception(f"Tool '{name}' of type '{type}' is not supported")
94
28
 
95
29
  ctx.dbg(f"Executing tool '{name}' with args:\n{json.dumps(args, indent=2)}")
30
+
31
+ # Filter args against tool properties
96
32
  function_args = {}
97
33
  parameters = tool_def.get("function", {}).get("parameters")
98
34
  if parameters:
99
35
  properties = parameters.get("properties")
100
- required_props = parameters.get("required", [])
101
36
  if properties:
102
- for prop_name, prop_def in properties.items():
103
- prop_title = prop_def.get("title", prop_name)
104
- prop_types = prop_def_types(prop_def)
105
- value = None
106
- if prop_name in args:
107
- value = tool_prop_value(args[prop_name], prop_def)
108
- elif prop_name in required_props:
109
- if "null" in prop_types:
110
- value = None
111
- elif "default" in prop_def:
112
- value = tool_prop_value(prop_def["default"], prop_def)
113
- else:
114
- raise Exception(f"Missing required parameter '{prop_title}' for tool '{name}'")
115
- if value is not None or "null" in prop_types:
116
- function_args[prop_name] = value
37
+ for key in args:
38
+ if key in properties:
39
+ function_args[key] = args[key]
117
40
  else:
118
41
  ctx.dbg(f"tool '{name}' has no properties:\n{json.dumps(tool_def, indent=2)}")
119
42
  else:
@@ -21,7 +21,7 @@ const ToolResult = {
21
21
  <div class="not-prose py-2">
22
22
  <pre v-if="ext.prefs.toolFormat !== 'preview'" class="tool-output">{{ origResult }}</pre>
23
23
  <div v-else>
24
- <ViewTypes v-if="Array.isArray(result)" :results="result" />
24
+ <ViewTypes v-if="Array.isArray(result) && result[0]?.type" :results="result" />
25
25
  <ViewType v-else :result="result" />
26
26
  </div>
27
27
  </div>
@@ -76,10 +76,92 @@ const ToolResult = {
76
76
  }
77
77
  }
78
78
 
79
- const Tools = {
80
- components: {
81
- ToolResult
79
+ const JsonInput = {
80
+ template: `
81
+ <div class="flex flex-col gap-1">
82
+ <div class="relative">
83
+ <textarea
84
+ v-model="localJson"
85
+ @input="validate"
86
+ rows="5"
87
+ class="w-full p-2 font-mono text-xs border rounded-md resize-y focus:outline-none focus:ring-2 transition-colors"
88
+ :class="error
89
+ ? 'border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/10 focus:ring-red-500'
90
+ : 'border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 focus:ring-blue-500'"
91
+ spellcheck="false"
92
+ ></textarea>
93
+ <div v-if="isValid" class="absolute bottom-2 right-2 text-green-500 bg-white dark:bg-gray-800 rounded-full p-1 shadow-sm">
94
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
95
+ <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
96
+ </svg>
97
+ </div>
98
+ </div>
99
+ <div v-if="error" class="text-xs text-red-600 dark:text-red-400 font-medium px-1">
100
+ {{ error }}
101
+ </div>
102
+ </div>
103
+ `,
104
+ props: {
105
+ modelValue: {
106
+ required: true
107
+ }
82
108
  },
109
+ emits: ['update:modelValue'],
110
+ setup(props, { emit }) {
111
+ // Initialize with formatted JSON
112
+ const localJson = ref(
113
+ props.modelValue !== undefined
114
+ ? JSON.stringify(props.modelValue, null, 4)
115
+ : ''
116
+ )
117
+ const error = ref(null)
118
+ const isValid = ref(true)
119
+
120
+ function validate() {
121
+ try {
122
+ if (!localJson.value.trim()) {
123
+ // Decide if empty string is valid object/array or undefined
124
+ // For now, let's say empty is NOT valid if the prop expects object
125
+ // But maybe we can treat it as valid undefined/null?
126
+ // Let's enforce valid JSON.
127
+ if (localJson.value === '') {
128
+ error.value = null
129
+ isValid.value = true
130
+ emit('update:modelValue', undefined)
131
+ return
132
+ }
133
+ }
134
+
135
+ const parsed = JSON.parse(localJson.value)
136
+ error.value = null
137
+ isValid.value = true
138
+ emit('update:modelValue', parsed)
139
+ } catch (e) {
140
+ error.value = e.message
141
+ isValid.value = false
142
+ // Do not emit invalid values
143
+ }
144
+ }
145
+
146
+ // Watch external changes only if they differ significantly from local
147
+ /*
148
+ Note: two-way binding with text representation is tricky.
149
+ If we watch props.modelValue, we might re-format user's in-progress typing if we aren't careful.
150
+ Usually better to only update localJson if the prop changes "from outside".
151
+ For this simple tool, initial value is likely enough, or we can watch with a deep compare check.
152
+ For now, let's stick to initial + internal validation.
153
+ */
154
+
155
+ return {
156
+ localJson,
157
+ error,
158
+ isValid,
159
+ validate
160
+ }
161
+ }
162
+ }
163
+
164
+ const Tools = {
83
165
  template: `
84
166
  <div class="p-4 md:p-6 max-w-7xl mx-auto w-full relative">
85
167
  <div v-if="Object.keys($ctx.tools.toolPageHeaders).length">
@@ -153,6 +235,10 @@ const Tools = {
153
235
  </select>
154
236
  </div>
155
237
 
238
+ <div v-else-if="prop.type === 'object' || prop.type === 'array'">
239
+ <JsonInput v-model="execForm[name]" />
240
+ </div>
241
+
156
242
  <div v-else>
157
243
  <input :type="prop.type === 'integer' || prop.type === 'number' ? 'number' : 'text'"
158
244
  v-model="execForm[name]"
@@ -615,6 +701,8 @@ export default {
615
701
  ctx.components({
616
702
  Tools,
617
703
  ToolSelector,
704
+ ToolResult,
705
+ JsonInput,
618
706
  })
619
707
 
620
708
  ctx.setGlobals({
@@ -656,7 +744,7 @@ export default {
656
744
  }
657
745
  })
658
746
 
659
- ctx.chatRequestFilters.push(({ request, thread }) => {
747
+ ctx.chatRequestFilters.push(({ request, thread, context }) => {
660
748
  // Tool Preferences
661
749
  const prefs = ctx.prefs
662
750
  if (prefs.onlyTools != null) {
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.14"
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
- # 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:
@@ -857,7 +885,7 @@ def chat_to_prompt(chat):
857
885
  prompt = ""
858
886
  if "messages" in chat:
859
887
  for message in chat["messages"]:
860
- if message["role"] == "user":
888
+ if message.get("role") == "user":
861
889
  # if content is string
862
890
  if isinstance(message["content"], str):
863
891
  if prompt:
@@ -876,7 +904,7 @@ def chat_to_prompt(chat):
876
904
  def chat_to_system_prompt(chat):
877
905
  if "messages" in chat:
878
906
  for message in chat["messages"]:
879
- if message["role"] == "system":
907
+ if message.get("role") == "system":
880
908
  # if content is string
881
909
  if isinstance(message["content"], str):
882
910
  return message["content"]
@@ -904,7 +932,7 @@ def last_user_prompt(chat):
904
932
  prompt = ""
905
933
  if "messages" in chat:
906
934
  for message in chat["messages"]:
907
- if message["role"] == "user":
935
+ if message.get("role") == "user":
908
936
  # if content is string
909
937
  if isinstance(message["content"], str):
910
938
  prompt = message["content"]
@@ -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}': {to_error_message(e)}", None
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
 
@@ -1807,7 +1944,10 @@ async def g_chat_completion(chat, context=None):
1807
1944
  # If we get here, all providers failed
1808
1945
  if first_exception:
1809
1946
  raise first_exception
1810
- raise Exception("All providers failed")
1947
+
1948
+ e = Exception("All providers failed")
1949
+ await g_app.on_chat_error(e, context or {"chat": chat})
1950
+ raise e
1811
1951
 
1812
1952
 
1813
1953
  async def cli_chat(chat, tools=None, image=None, audio=None, file=None, args=None, raw=False):
@@ -2654,6 +2794,7 @@ class AppExtensions:
2654
2794
  self.tool_groups = {}
2655
2795
  self.index_headers = []
2656
2796
  self.index_footers = []
2797
+ self.allowed_directories = []
2657
2798
  self.request_args = {
2658
2799
  "image_config": dict, # e.g. { "aspect_ratio": "1:1" }
2659
2800
  "temperature": float, # e.g: 0.7
@@ -2710,6 +2851,22 @@ class AppExtensions:
2710
2851
  self.config = config
2711
2852
  self.auth_enabled = self.config.get("auth", {}).get("enabled", False)
2712
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
+
2713
2870
  # Authentication middleware helper
2714
2871
  def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
2715
2872
  """Check if request is authenticated. Returns (is_authenticated, user_data)"""
@@ -2777,7 +2934,9 @@ class AppExtensions:
2777
2934
  context["stackTrace"] = traceback.format_exc()
2778
2935
  for filter_func in self.chat_error_filters:
2779
2936
  try:
2780
- await filter_func(e, context)
2937
+ task = filter_func(e, context)
2938
+ if asyncio.isfuture(task):
2939
+ await task
2781
2940
  except Exception as e:
2782
2941
  _err("chat error filter failed", e)
2783
2942
 
@@ -2822,6 +2981,12 @@ class AppExtensions:
2822
2981
  current_chat["tools"].append(tool_def)
2823
2982
  return current_chat
2824
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
+
2825
2990
 
2826
2991
  def handler_name(handler):
2827
2992
  if hasattr(handler, "__name__"):
@@ -2848,6 +3013,20 @@ class ExtensionContext:
2848
3013
  self.request_args = app.request_args
2849
3014
  self.disabled = False
2850
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
+
2851
3030
  def chat_to_prompt(self, chat: Dict[str, Any]) -> str:
2852
3031
  return chat_to_prompt(chat)
2853
3032
 
@@ -3094,10 +3273,7 @@ class ExtensionContext:
3094
3273
  self.app.tool_groups[group].append(name)
3095
3274
 
3096
3275
  def get_tool_definition(self, name: str) -> Optional[Dict[str, Any]]:
3097
- for tool_def in self.app.tool_definitions:
3098
- if tool_def["function"]["name"] == name:
3099
- return tool_def
3100
- return None
3276
+ return self.app.get_tool_definition(name)
3101
3277
 
3102
3278
  def group_resources(self, resources: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
3103
3279
  return group_resources(resources)
@@ -3681,6 +3857,10 @@ def cli_exec(cli_args, extra_args):
3681
3857
  asyncio.run(update_extensions(cli_args.update))
3682
3858
  return ExitCode.SUCCESS
3683
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
+
3684
3864
  g_app.extensions = install_extensions()
3685
3865
 
3686
3866
  # 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.14',
9
+ version: '3.0.16',
10
10
  base,
11
11
  prefsKey,
12
12
  welcome: 'Welcome to llms.py',