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.
Files changed (41) hide show
  1. llms/extensions/app/__init__.py +0 -1
  2. llms/extensions/app/db.py +7 -3
  3. llms/extensions/app/ui/threadStore.mjs +10 -3
  4. llms/extensions/computer/README.md +96 -0
  5. llms/extensions/computer/__init__.py +59 -0
  6. llms/extensions/computer/base.py +80 -0
  7. llms/extensions/computer/bash.py +185 -0
  8. llms/extensions/computer/computer.py +523 -0
  9. llms/extensions/computer/edit.py +299 -0
  10. llms/extensions/computer/filesystem.py +542 -0
  11. llms/extensions/computer/platform.py +461 -0
  12. llms/extensions/computer/run.py +37 -0
  13. llms/extensions/core_tools/__init__.py +0 -38
  14. llms/extensions/providers/anthropic.py +28 -1
  15. llms/extensions/providers/cerebras.py +0 -1
  16. llms/extensions/providers/google.py +112 -34
  17. llms/extensions/skills/LICENSE +202 -0
  18. llms/extensions/skills/__init__.py +130 -0
  19. llms/extensions/skills/errors.py +25 -0
  20. llms/extensions/skills/models.py +39 -0
  21. llms/extensions/skills/parser.py +178 -0
  22. llms/extensions/skills/ui/index.mjs +376 -0
  23. llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
  24. llms/extensions/skills/validator.py +177 -0
  25. llms/extensions/system_prompts/ui/index.mjs +6 -10
  26. llms/extensions/tools/__init__.py +5 -82
  27. llms/extensions/tools/ui/index.mjs +194 -63
  28. llms/main.py +502 -146
  29. llms/ui/ai.mjs +1 -1
  30. llms/ui/app.css +530 -0
  31. llms/ui/ctx.mjs +53 -6
  32. llms/ui/modules/chat/ChatBody.mjs +200 -20
  33. llms/ui/modules/chat/index.mjs +108 -104
  34. llms/ui/tailwind.input.css +10 -0
  35. llms/ui/utils.mjs +25 -1
  36. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/METADATA +2 -2
  37. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/RECORD +41 -24
  38. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
  39. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
  40. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +0 -0
  41. {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
- {{$threads.currentThread.value?.systemPrompt || 'No System Prompt was used' }}
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 = request.messages.find(x => x.role === 'system')
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
- // Only add the selected system prompt for new requests
263
- if (ext.prefs.systemPrompt && request.messages.length <= 1) {
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 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: