llms-py 3.0.22__py3-none-any.whl → 3.0.24__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.
@@ -33,14 +33,21 @@ Access the skills panel by clicking the **Skills** icon in the top toolbar. The
33
33
 
34
34
  ### Skill Groups
35
35
 
36
- Skills are organized into groups based on their source:
36
+ Skills are organized into groups based on their source location. Skills are discovered from these directories in order:
37
37
 
38
- | Group | Description | Editable |
39
- |-------|-------------|----------|
40
- | `~/.llms/.agents` | Your personal skills collection | ✓ Yes |
41
- | Built-in | Skills bundled with the extension | No |
38
+ | Group | Location | Description | Editable |
39
+ |-------|----------|-------------|----------|
40
+ | Project (Agent) | `.agent/skills/` | Skills local to the current project | ✓ Yes |
41
+ | Project (Claude) | `.claude/skills/` | Claude-format skills in the current project | Yes |
42
+ | User (Agent) | `~/.llms/.agents/skills/` | Your personal skills collection | ✓ Yes |
43
+ | User (Claude) | `~/.claude/skills/` | Claude-format skills in your home directory | ✓ Yes |
44
+ | Built-in | Extension directory | Skills bundled with the extension | ✗ No |
42
45
 
43
- Your personal skills (`~/.llms/.agents`) are fully editable. Built-in skills provide reference implementations you can learn from.
46
+ **Project-level skills** (`.agent/` and `.claude/`) are specific to the workspace you're working in. They're ideal for project-specific workflows, coding standards, or team conventions.
47
+
48
+ **User-level skills** (`~/.llms/.agents/` and `~/.claude/`) are available across all projects. Use these for personal workflows and preferences.
49
+
50
+ Both `.agent` and `.claude` directory formats are supported for compatibility with different tooling conventions.
44
51
 
45
52
  ### Selecting Skills for a Conversation
46
53
 
@@ -21,6 +21,9 @@ g_home_skills = None
21
21
  # }
22
22
  g_available_skills = []
23
23
 
24
+ LLMS_HOME_SKILLS = "~/.llms/.agent/skills"
25
+ LLMS_LOCAL_SKILLS = ".agent/skills"
26
+
24
27
 
25
28
  def is_safe_path(base_path: str, requested_path: str) -> bool:
26
29
  """Check if the requested path is safely within the base path."""
@@ -132,10 +135,12 @@ def install(ctx):
132
135
  if os.path.exists(os.path.join(".claude", "skills")):
133
136
  skill_roots[".claude/skills"] = os.path.join(".claude", "skills")
134
137
 
135
- skill_roots["~/.llms/.agents"] = home_skills
138
+ skill_roots[LLMS_HOME_SKILLS] = home_skills
136
139
 
137
- if os.path.exists(os.path.join(".agent", "skills")):
138
- skill_roots[".agents"] = os.path.join(".agent", "skills")
140
+ local_skills = os.path.join(".agent", "skills")
141
+ if os.path.exists(local_skills):
142
+ local_skills = str(Path(local_skills).resolve())
143
+ skill_roots[LLMS_LOCAL_SKILLS] = local_skills
139
144
 
140
145
  g_skills = {}
141
146
  for group, root in skill_roots.items():
@@ -237,7 +242,7 @@ def install(ctx):
237
242
  skill_props = props.to_dict()
238
243
  skill_props.update(
239
244
  {
240
- "group": "~/.llms/.agents",
245
+ "group": LLMS_HOME_SKILLS,
241
246
  "location": str(skill_dir),
242
247
  "files": files,
243
248
  }
@@ -303,9 +308,9 @@ def install(ctx):
303
308
 
304
309
  location = skill_info.get("location")
305
310
 
306
- # Only allow modifications to skills in home directory
307
- if not is_safe_path(home_skills, location):
308
- raise Exception("Cannot modify skills outside of home directory")
311
+ # Only allow modifications to skills in home or local .agent directory
312
+ if not is_safe_path(home_skills, location) and not (local_skills and is_safe_path(local_skills, location)):
313
+ raise Exception("Cannot modify skills outside of allowed directories")
309
314
 
310
315
  full_path = os.path.join(location, file_path)
311
316
 
@@ -319,7 +324,7 @@ def install(ctx):
319
324
  f.write(content)
320
325
 
321
326
  # Reload skill metadata
322
- group = skill_info.get("group", "~/.llms/.agents")
327
+ group = skill_info.get("group", LLMS_HOME_SKILLS)
323
328
  updated_skill = reload_skill(name, location, group)
324
329
 
325
330
  return aiohttp.web.json_response({"path": file_path, "skill": updated_skill})
@@ -342,9 +347,9 @@ def install(ctx):
342
347
 
343
348
  location = skill_info.get("location")
344
349
 
345
- # Only allow modifications to skills in home directory
346
- if not is_safe_path(home_skills, location):
347
- raise Exception("Cannot modify skills outside of home directory")
350
+ # Only allow modifications to skills in home or local .agent directory
351
+ if not is_safe_path(home_skills, location) and not (local_skills and is_safe_path(local_skills, location)):
352
+ raise Exception("Cannot modify skills outside of allowed directories")
348
353
 
349
354
  full_path = os.path.join(location, file_path)
350
355
 
@@ -371,7 +376,7 @@ def install(ctx):
371
376
  break
372
377
 
373
378
  # Reload skill metadata
374
- group = skill_info.get("group", "~/.llms/.agents")
379
+ group = skill_info.get("group", LLMS_HOME_SKILLS)
375
380
  updated_skill = reload_skill(name, location, group)
376
381
 
377
382
  return aiohttp.web.json_response({"path": file_path, "skill": updated_skill})
@@ -433,7 +438,7 @@ def install(ctx):
433
438
  skill_props = props.to_dict()
434
439
  skill_props.update(
435
440
  {
436
- "group": "~/.llms/.agents",
441
+ "group": LLMS_HOME_SKILLS,
437
442
  "location": str(skill_dir_path),
438
443
  "files": files,
439
444
  }
@@ -467,9 +472,9 @@ def install(ctx):
467
472
  else:
468
473
  raise Exception(f"Skill '{name}' not found")
469
474
 
470
- # Only allow deletion of skills in home directory
471
- if not is_safe_path(home_skills, location):
472
- raise Exception("Cannot delete skills outside of home directory")
475
+ # Only allow deletion of skills in home or local .agent directory
476
+ if not is_safe_path(home_skills, location) and not (local_skills and is_safe_path(local_skills, location)):
477
+ raise Exception("Cannot delete skills outside of allowed directories")
473
478
 
474
479
  try:
475
480
  if os.path.exists(location):
@@ -3,6 +3,9 @@ import { leftPart } from "@servicestack/client"
3
3
 
4
4
  let ext
5
5
 
6
+ const LLMS_HOME_SKILLS = "~/.llms/.agent/skills"
7
+ const LLMS_LOCAL_SKILLS = ".agent/skills"
8
+
6
9
  const SkillSelector = {
7
10
  template: `
8
11
  <div class="px-4 py-4 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 max-h-[80vh] overflow-y-auto">
@@ -119,10 +122,10 @@ const SkillSelector = {
119
122
  skills
120
123
  }))
121
124
 
122
- // Sort groups: writable (~/.llms/.agents) first, then alphabetically
125
+ // Sort groups: writable (~/.llms/.agent/skills,.agent/skills) first, then alphabetically
123
126
  definedGroups.sort((a, b) => {
124
- const aEditable = a.name === '~/.llms/.agents'
125
- const bEditable = b.name === '~/.llms/.agents'
127
+ const aEditable = a.name === LLMS_HOME_SKILLS || a.name === LLMS_LOCAL_SKILLS
128
+ const bEditable = b.name === LLMS_HOME_SKILLS || b.name === LLMS_LOCAL_SKILLS
126
129
  if (aEditable !== bEditable) return aEditable ? -1 : 1
127
130
  return a.name.localeCompare(b.name)
128
131
  })
@@ -158,6 +161,10 @@ const SkillSelector = {
158
161
  onlySkills = onlySkills.filter(s => s !== name)
159
162
  } else {
160
163
  onlySkills = [...onlySkills, name]
164
+ // If has all skills set to 'All' (null)
165
+ if (onlySkills.length === availableSkills.value.length) {
166
+ onlySkills = null
167
+ }
161
168
  }
162
169
  }
163
170
 
@@ -395,8 +402,8 @@ const SkillPage = {
395
402
  grouped[group].push(skill)
396
403
  })
397
404
  return Object.entries(grouped).sort((a, b) => {
398
- const aEditable = a[0] === '~/.llms/.agents'
399
- const bEditable = b[0] === '~/.llms/.agents'
405
+ const aEditable = a[0] === LLMS_HOME_SKILLS || a[0] === LLMS_LOCAL_SKILLS
406
+ const bEditable = b[0] === LLMS_HOME_SKILLS || b[0] === LLMS_LOCAL_SKILLS
400
407
  if (aEditable !== bEditable) return aEditable ? -1 : 1
401
408
  return a[0].localeCompare(b[0])
402
409
  }).map(([name, skills]) => ({ name, skills: skills.sort((a, b) => a.name.localeCompare(b.name)) }))
@@ -419,8 +426,8 @@ const SkillPage = {
419
426
  return tree.sort((a, b) => { if (a.isFile !== b.isFile) return a.isFile ? 1 : -1; return a.name.localeCompare(b.name) })
420
427
  }
421
428
  const hasUnsavedChanges = computed(() => isEditing.value && editContent.value !== fileContent.value)
422
- function isGroupEditable(groupName) { return groupName === '~/.llms/.agents' }
423
- function isEditable(skill) { return skill?.group === '~/.llms/.agents' }
429
+ function isGroupEditable(groupName) { return groupName === LLMS_HOME_SKILLS || groupName === LLMS_LOCAL_SKILLS }
430
+ function isEditable(skill) { return skill?.group === LLMS_HOME_SKILLS || skill?.group === LLMS_LOCAL_SKILLS }
424
431
  function isSkillExpanded(name) { return !!expandedSkills.value[name] }
425
432
  function toggleSkillExpand(skill) {
426
433
  expandedSkills.value[skill.name] = !expandedSkills.value[skill.name]
@@ -579,7 +586,7 @@ const SkillStore = {
579
586
  <div class="h-full flex flex-col">
580
587
  <div class="px-4 py-3 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between flex-shrink-0">
581
588
  <div>
582
- <h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">Skill Store</h1>
589
+ <h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">Discover Skills</h1>
583
590
  <p class="text-sm text-gray-500 dark:text-gray-400">{{ total.toLocaleString() }} skills available</p>
584
591
  </div>
585
592
  <div class="flex items-center gap-2">
llms/llms.json CHANGED
@@ -235,17 +235,29 @@
235
235
  "enabled": true,
236
236
  "temperature": 1.0
237
237
  },
238
+ "lmstudio": {
239
+ "enabled": false,
240
+ "npm": "lmstudio",
241
+ "api": "http://127.0.0.1:1234/v1",
242
+ "models": {}
243
+ },
238
244
  "ollama": {
239
245
  "enabled": false,
240
246
  "id": "ollama",
241
247
  "npm": "ollama",
242
248
  "api": "http://localhost:11434"
243
249
  },
244
- "lmstudio": {
250
+ "ollama-cloud": {
251
+ "enabled": true,
252
+ "env": [
253
+ "OLLAMA_API_KEY"
254
+ ]
255
+ },
256
+ "openai-local": {
245
257
  "enabled": false,
246
- "npm": "lmstudio",
247
- "api": "http://127.0.0.1:1234/v1",
248
- "models": {}
258
+ "npm": "openai-local",
259
+ "api": "http://localhost:8000/v1",
260
+ "api_key": "$OPENAI_LOCAL_API_KEY"
249
261
  },
250
262
  "google": {
251
263
  "enabled": true,
llms/main.py CHANGED
@@ -57,7 +57,7 @@ try:
57
57
  except ImportError:
58
58
  HAS_PIL = False
59
59
 
60
- VERSION = "3.0.22"
60
+ VERSION = "3.0.24"
61
61
  _ROOT = None
62
62
  DEBUG = os.getenv("DEBUG") == "1"
63
63
  MOCK = os.getenv("MOCK") == "1"
@@ -871,8 +871,7 @@ def save_image_to_cache(base64_data, filename, image_info, ignore_info=False):
871
871
  return url, info
872
872
 
873
873
 
874
- async def response_json(response):
875
- text = await response.text()
874
+ def http_error_to_message(response, text):
876
875
  if response.status >= 400:
877
876
  message = "HTTP " + str(response.status) + " " + response.reason
878
877
  _dbg(f"HTTP {response.status} {response.reason}\n{dict(response.headers)}\n{text}")
@@ -885,6 +884,13 @@ async def response_json(response):
885
884
  except Exception:
886
885
  if text:
887
886
  message += ": " + text[:100]
887
+ return message
888
+
889
+
890
+ async def response_json(response):
891
+ text = await response.text()
892
+ if response.status >= 400:
893
+ message = http_error_to_message(response, text)
888
894
  raise Exception(message)
889
895
  response.raise_for_status()
890
896
  body = json.loads(text)
@@ -1394,6 +1400,7 @@ class OllamaProvider(OpenAiCompatible):
1394
1400
  "id": k,
1395
1401
  "name": v.replace(":", " "),
1396
1402
  "modalities": {"input": ["text"], "output": ["text"]},
1403
+ "tool_call": True,
1397
1404
  "cost": {
1398
1405
  "input": 0,
1399
1406
  "output": 0,
@@ -1431,6 +1438,10 @@ class LMStudioProvider(OllamaProvider):
1431
1438
  return ret
1432
1439
 
1433
1440
 
1441
+ class OpenAiLocalProvider(LMStudioProvider):
1442
+ sdk = "openai-local"
1443
+
1444
+
1434
1445
  def get_provider_model(model_name):
1435
1446
  for provider in g_handlers.values():
1436
1447
  provider_model = provider.provider_model(model_name)
@@ -2229,7 +2240,7 @@ async def get_text(url):
2229
2240
  async with session.get(url) as resp:
2230
2241
  text = await resp.text()
2231
2242
  if resp.status >= 400:
2232
- raise HTTPError(resp.status, reason=resp.reason, body=text, headers=dict(resp.headers))
2243
+ raise Exception(http_error_to_message(resp, text))
2233
2244
  return text
2234
2245
 
2235
2246
 
@@ -2838,6 +2849,7 @@ class AppExtensions:
2838
2849
  CodestralProvider,
2839
2850
  OllamaProvider,
2840
2851
  LMStudioProvider,
2852
+ OpenAiLocalProvider,
2841
2853
  ]
2842
2854
  self.aspect_ratios = {
2843
2855
  "1:1": "1024×1024",
@@ -2953,7 +2965,7 @@ class AppExtensions:
2953
2965
  for filter_func in self.chat_error_filters:
2954
2966
  try:
2955
2967
  task = filter_func(e, context)
2956
- if asyncio.isfuture(task):
2968
+ if inspect.iscoroutine(task):
2957
2969
  await task
2958
2970
  except Exception as e:
2959
2971
  _err("chat error filter failed", e)