llms-py 3.0.10__py3-none-any.whl → 3.0.18__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 +7 -3
- 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/base.py +80 -0
- llms/extensions/computer/bash.py +185 -0
- llms/extensions/computer/computer.py +523 -0
- llms/extensions/computer/edit.py +299 -0
- llms/extensions/computer/filesystem.py +542 -0
- llms/extensions/computer/platform.py +461 -0
- llms/extensions/computer/run.py +37 -0
- llms/extensions/core_tools/__init__.py +0 -38
- llms/extensions/providers/anthropic.py +28 -1
- llms/extensions/providers/cerebras.py +0 -1
- llms/extensions/providers/google.py +112 -34
- 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 +376 -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 +194 -63
- llms/main.py +502 -146
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +530 -0
- llms/ui/ctx.mjs +53 -6
- llms/ui/modules/chat/ChatBody.mjs +200 -20
- llms/ui/modules/chat/index.mjs +108 -104
- llms/ui/tailwind.input.css +10 -0
- llms/ui/utils.mjs +25 -1
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/METADATA +2 -2
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/RECORD +41 -24
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Skill validation logic."""
|
|
2
|
+
|
|
3
|
+
import unicodedata
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .errors import ParseError
|
|
8
|
+
from .parser import find_skill_md, parse_frontmatter
|
|
9
|
+
|
|
10
|
+
MAX_SKILL_NAME_LENGTH = 64
|
|
11
|
+
MAX_DESCRIPTION_LENGTH = 1024
|
|
12
|
+
MAX_COMPATIBILITY_LENGTH = 500
|
|
13
|
+
|
|
14
|
+
# Allowed frontmatter fields per Agent Skills Spec
|
|
15
|
+
ALLOWED_FIELDS = {
|
|
16
|
+
"name",
|
|
17
|
+
"description",
|
|
18
|
+
"license",
|
|
19
|
+
"allowed-tools",
|
|
20
|
+
"metadata",
|
|
21
|
+
"compatibility",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _validate_name(name: str, skill_dir: Path) -> list[str]:
|
|
26
|
+
"""Validate skill name format and directory match.
|
|
27
|
+
|
|
28
|
+
Skill names support i18n characters (Unicode letters) plus hyphens.
|
|
29
|
+
Names must be lowercase and cannot start/end with hyphens.
|
|
30
|
+
"""
|
|
31
|
+
errors = []
|
|
32
|
+
|
|
33
|
+
if not name or not isinstance(name, str) or not name.strip():
|
|
34
|
+
errors.append("Field 'name' must be a non-empty string")
|
|
35
|
+
return errors
|
|
36
|
+
|
|
37
|
+
name = unicodedata.normalize("NFKC", name.strip())
|
|
38
|
+
|
|
39
|
+
if len(name) > MAX_SKILL_NAME_LENGTH:
|
|
40
|
+
errors.append(
|
|
41
|
+
f"Skill name '{name}' exceeds {MAX_SKILL_NAME_LENGTH} character limit "
|
|
42
|
+
f"({len(name)} chars)"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if name != name.lower():
|
|
46
|
+
errors.append(f"Skill name '{name}' must be lowercase")
|
|
47
|
+
|
|
48
|
+
if name.startswith("-") or name.endswith("-"):
|
|
49
|
+
errors.append("Skill name cannot start or end with a hyphen")
|
|
50
|
+
|
|
51
|
+
if "--" in name:
|
|
52
|
+
errors.append("Skill name cannot contain consecutive hyphens")
|
|
53
|
+
|
|
54
|
+
if not all(c.isalnum() or c == "-" for c in name):
|
|
55
|
+
errors.append(
|
|
56
|
+
f"Skill name '{name}' contains invalid characters. "
|
|
57
|
+
"Only letters, digits, and hyphens are allowed."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if skill_dir:
|
|
61
|
+
dir_name = unicodedata.normalize("NFKC", skill_dir.name)
|
|
62
|
+
if dir_name != name:
|
|
63
|
+
errors.append(
|
|
64
|
+
f"Directory name '{skill_dir.name}' must match skill name '{name}'"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return errors
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _validate_description(description: str) -> list[str]:
|
|
71
|
+
"""Validate description format."""
|
|
72
|
+
errors = []
|
|
73
|
+
|
|
74
|
+
if not description or not isinstance(description, str) or not description.strip():
|
|
75
|
+
errors.append("Field 'description' must be a non-empty string")
|
|
76
|
+
return errors
|
|
77
|
+
|
|
78
|
+
if len(description) > MAX_DESCRIPTION_LENGTH:
|
|
79
|
+
errors.append(
|
|
80
|
+
f"Description exceeds {MAX_DESCRIPTION_LENGTH} character limit "
|
|
81
|
+
f"({len(description)} chars)"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return errors
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _validate_compatibility(compatibility: str) -> list[str]:
|
|
88
|
+
"""Validate compatibility format."""
|
|
89
|
+
errors = []
|
|
90
|
+
|
|
91
|
+
if not isinstance(compatibility, str):
|
|
92
|
+
errors.append("Field 'compatibility' must be a string")
|
|
93
|
+
return errors
|
|
94
|
+
|
|
95
|
+
if len(compatibility) > MAX_COMPATIBILITY_LENGTH:
|
|
96
|
+
errors.append(
|
|
97
|
+
f"Compatibility exceeds {MAX_COMPATIBILITY_LENGTH} character limit "
|
|
98
|
+
f"({len(compatibility)} chars)"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return errors
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _validate_metadata_fields(metadata: dict) -> list[str]:
|
|
105
|
+
"""Validate that only allowed fields are present."""
|
|
106
|
+
errors = []
|
|
107
|
+
|
|
108
|
+
extra_fields = set(metadata.keys()) - ALLOWED_FIELDS
|
|
109
|
+
if extra_fields:
|
|
110
|
+
errors.append(
|
|
111
|
+
f"Unexpected fields in frontmatter: {', '.join(sorted(extra_fields))}. "
|
|
112
|
+
f"Only {sorted(ALLOWED_FIELDS)} are allowed."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return errors
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def validate_metadata(metadata: dict, skill_dir: Optional[Path] = None) -> list[str]:
|
|
119
|
+
"""Validate parsed skill metadata.
|
|
120
|
+
|
|
121
|
+
This is the core validation function that works on already-parsed metadata,
|
|
122
|
+
avoiding duplicate file I/O when called from the parser.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
metadata: Parsed YAML frontmatter dictionary
|
|
126
|
+
skill_dir: Optional path to skill directory (for name-directory match check)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of validation error messages. Empty list means valid.
|
|
130
|
+
"""
|
|
131
|
+
errors = []
|
|
132
|
+
errors.extend(_validate_metadata_fields(metadata))
|
|
133
|
+
|
|
134
|
+
if "name" not in metadata:
|
|
135
|
+
errors.append("Missing required field in frontmatter: name")
|
|
136
|
+
else:
|
|
137
|
+
errors.extend(_validate_name(metadata["name"], skill_dir))
|
|
138
|
+
|
|
139
|
+
if "description" not in metadata:
|
|
140
|
+
errors.append("Missing required field in frontmatter: description")
|
|
141
|
+
else:
|
|
142
|
+
errors.extend(_validate_description(metadata["description"]))
|
|
143
|
+
|
|
144
|
+
if "compatibility" in metadata:
|
|
145
|
+
errors.extend(_validate_compatibility(metadata["compatibility"]))
|
|
146
|
+
|
|
147
|
+
return errors
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def validate(skill_dir: Path) -> list[str]:
|
|
151
|
+
"""Validate a skill directory.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
skill_dir: Path to the skill directory
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of validation error messages. Empty list means valid.
|
|
158
|
+
"""
|
|
159
|
+
skill_dir = Path(skill_dir)
|
|
160
|
+
|
|
161
|
+
if not skill_dir.exists():
|
|
162
|
+
return [f"Path does not exist: {skill_dir}"]
|
|
163
|
+
|
|
164
|
+
if not skill_dir.is_dir():
|
|
165
|
+
return [f"Not a directory: {skill_dir}"]
|
|
166
|
+
|
|
167
|
+
skill_md = find_skill_md(skill_dir)
|
|
168
|
+
if skill_md is None:
|
|
169
|
+
return ["Missing required file: SKILL.md"]
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
content = skill_md.read_text()
|
|
173
|
+
metadata, _ = parse_frontmatter(content)
|
|
174
|
+
except ParseError as e:
|
|
175
|
+
return [str(e)]
|
|
176
|
+
|
|
177
|
+
return validate_metadata(metadata, skill_dir)
|
|
@@ -163,7 +163,7 @@ const SystemPromptEditor = {
|
|
|
163
163
|
</div>
|
|
164
164
|
</div>
|
|
165
165
|
<div v-if="hasMessages" class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 px-3 py-2 text-sm">
|
|
166
|
-
|
|
166
|
+
<TextViewer prefsName="systemPrompt" :text="$threads.getCurrentThreadSystemPrompt() || 'No System Prompt was used'" />
|
|
167
167
|
</div>
|
|
168
168
|
<div v-else>
|
|
169
169
|
<textarea
|
|
@@ -251,21 +251,17 @@ export default {
|
|
|
251
251
|
}
|
|
252
252
|
})
|
|
253
253
|
|
|
254
|
-
ctx.chatRequestFilters.push(({ request, thread }) => {
|
|
254
|
+
ctx.chatRequestFilters.push(({ request, thread, context }) => {
|
|
255
255
|
|
|
256
|
-
const hasSystemPrompt =
|
|
256
|
+
const hasSystemPrompt = !!context.systemPrompt
|
|
257
|
+
console.log('system_prompts chatRequestFilters', hasSystemPrompt)
|
|
257
258
|
if (hasSystemPrompt) {
|
|
258
259
|
console.log('Already has system prompt', hasSystemPrompt.content)
|
|
259
260
|
return
|
|
260
261
|
}
|
|
261
262
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// add message to start
|
|
265
|
-
request.messages.unshift({
|
|
266
|
-
role: 'system',
|
|
267
|
-
content: ext.prefs.systemPrompt
|
|
268
|
-
})
|
|
263
|
+
if (ext.prefs.systemPrompt) {
|
|
264
|
+
context.systemPrompt = ext.prefs.systemPrompt
|
|
269
265
|
}
|
|
270
266
|
})
|
|
271
267
|
|
|
@@ -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:
|