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.
- 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_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/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 +362 -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 +93 -5
- llms/main.py +215 -35
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +527 -22
- llms/ui/ctx.mjs +53 -6
- llms/ui/modules/chat/ChatBody.mjs +186 -24
- llms/ui/modules/chat/index.mjs +107 -103
- llms/ui/tailwind.input.css +10 -0
- llms/ui/utils.mjs +25 -1
- {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/METADATA +1 -1
- {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/RECORD +37 -27
- {llms_py-3.0.14.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}/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.14.dist-info → llms_py-3.0.16.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
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:
|
|
@@ -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
|
|
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
|
|
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
|
|
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}'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|