llms-py 3.0.26__py3-none-any.whl → 3.0.27__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.
@@ -496,15 +496,20 @@ def move_file(source: Annotated[str, "Source path"], destination: Annotated[str,
496
496
 
497
497
 
498
498
  def search_files(
499
- path: Annotated[str, "Path to search in"],
500
499
  pattern: Annotated[str, "Glob pattern to match"],
500
+ path: Annotated[str, "Path to search in"] = None,
501
501
  exclude_patterns: Annotated[List[str], "Glob patterns to exclude"] = None,
502
+ sort_by: Annotated[Literal["path", "modified", "size"], "Sort by path, modified or size"] = "path",
503
+ max_results: int = 200,
502
504
  ) -> str:
503
505
  """
504
506
  Recursively search for files and directories matching a pattern. The patterns should be glob-style patterns that match paths relative to the working directory.
505
507
  Use pattern like '.ext' to match files in current directory, and '**/.ext' to match files in all subdirectories.
506
508
  Returns full paths to all matching items. Great for finding files when you don't know their exact location. Only searches within allowed directories.
509
+ If no path is provided, searches in the first allowed directory.
507
510
  """
511
+ if not path:
512
+ path = get_allowed_directories()[0]
508
513
  valid_path = _validate_path(path)
509
514
  results = []
510
515
  if exclude_patterns is None:
@@ -533,6 +538,16 @@ def search_files(
533
538
  except Exception as e:
534
539
  raise RuntimeError(f"Error searching files in {valid_path}: {e}") from e
535
540
 
541
+ if sort_by == "size":
542
+ results.sort(key=lambda p: os.path.getsize(p) if os.path.exists(p) else 0, reverse=True)
543
+ elif sort_by == "modified":
544
+ results.sort(key=lambda p: os.path.getmtime(p) if os.path.exists(p) else 0, reverse=True)
545
+ else: # path
546
+ results.sort()
547
+
548
+ if max_results > 0:
549
+ results = results[:max_results]
550
+
536
551
  if not results:
537
552
  return "No matches found"
538
553
 
@@ -4,8 +4,6 @@ Core System Tools providing essential file operations, memory persistence, math
4
4
 
5
5
  import ast
6
6
  import contextlib
7
- import glob
8
- import json
9
7
  import math
10
8
  import operator
11
9
  import os
@@ -21,165 +19,6 @@ from aiohttp import web
21
19
 
22
20
  g_ctx = None
23
21
 
24
- # -----------------------------
25
- # In-memory storage (replace later)
26
- # -----------------------------
27
-
28
- _MEMORY_STORE: Dict[str, Any] = {}
29
- _SEMANTIC_STORE: List[Dict[str, Any]] = [] # {id, text, metadata}
30
-
31
-
32
- # -----------------------------
33
- # Memory tools
34
- # -----------------------------
35
-
36
-
37
- def memory_read(key: str) -> Any:
38
- """Read a value from persistent memory."""
39
- return _MEMORY_STORE.get(key)
40
-
41
-
42
- def memory_write(key: str, value: Any) -> bool:
43
- """Write a value to persistent memory."""
44
- _MEMORY_STORE[key] = value
45
- return True
46
-
47
-
48
- # -----------------------------
49
- # Path safety helpers
50
- # -----------------------------
51
-
52
- # Limit tools to only access files and folders within LLMS_BASE_DIR if specified, otherwise the current working directory
53
- _BASE_DIR = os.environ.get("LLMS_BASE_DIR") or os.path.realpath(os.getcwd())
54
-
55
-
56
- def _resolve_safe_path(path: str) -> str:
57
- """
58
- Resolve a path and ensure it stays within the current working directory.
59
- Raises ValueError if the path escapes the base directory.
60
- """
61
- resolved = os.path.realpath(os.path.join(_BASE_DIR, path))
62
- if not resolved.startswith(_BASE_DIR + os.sep) and resolved != _BASE_DIR:
63
- raise ValueError("Access denied: path is outside the working directory")
64
- return resolved
65
-
66
-
67
- # -----------------------------
68
- # Semantic search (placeholder)
69
- # -----------------------------
70
-
71
-
72
- def semantic_search(query: str, top_k: int = 5) -> List[Dict[str, Any]]:
73
- """
74
- Naive semantic search placeholder.
75
- Replace with embeddings + vector DB.
76
- """
77
- results = []
78
- for item in _SEMANTIC_STORE:
79
- if query.lower() in item["text"].lower():
80
- results.append(item)
81
- return results[:top_k]
82
-
83
-
84
- # -----------------------------
85
- # File system tools (restricted to CWD)
86
- # -----------------------------
87
-
88
-
89
- def read_file(path: str) -> str:
90
- """Read a text file from disk within the current working directory."""
91
- safe_path = _resolve_safe_path(path)
92
- with open(safe_path, encoding="utf-8") as f:
93
- return f.read()
94
-
95
-
96
- def write_file(path: str, content: str) -> bool:
97
- """Write text to a file within the current working directory (overwrites)."""
98
- safe_path = _resolve_safe_path(path)
99
- os.makedirs(os.path.dirname(safe_path) or _BASE_DIR, exist_ok=True)
100
- with open(safe_path, "w", encoding="utf-8") as f:
101
- f.write(content)
102
- return True
103
-
104
-
105
- def list_directory(path: str) -> str:
106
- """List directory contents"""
107
- safe_path = _resolve_safe_path(path)
108
- if not os.path.exists(safe_path):
109
- return f"Error: Path not found: {path}"
110
-
111
- entries = []
112
- try:
113
- for entry in os.scandir(safe_path):
114
- stat = entry.stat()
115
- entries.append(
116
- {
117
- "name": "/" + entry.name if entry.is_dir() else entry.name,
118
- "size": stat.st_size,
119
- "mtime": datetime.fromtimestamp(stat.st_mtime).isoformat(),
120
- }
121
- )
122
- return json.dumps({"path": os.path.relpath(safe_path, _BASE_DIR), "entries": entries}, indent=2)
123
- except Exception as e:
124
- return f"Error listing directory: {e}"
125
-
126
-
127
- def glob_paths(
128
- pattern: str,
129
- extensions: Optional[List[str]] = None,
130
- sort_by: str = "path", # "path" | "modified" | "size"
131
- max_results: int = 100,
132
- ) -> Dict[str, List[Dict[str, str]]]:
133
- """
134
- Find files and directories matching a glob pattern
135
- """
136
- if sort_by not in {"path", "modified", "size"}:
137
- raise ValueError("sort_by must be one of: path, modified, size")
138
-
139
- safe_pattern = _resolve_safe_path(pattern)
140
-
141
- results = []
142
-
143
- for path in glob.glob(safe_pattern, recursive=True):
144
- resolved = os.path.realpath(path)
145
-
146
- # Enforce CWD restriction (important for symlinks)
147
- if not resolved.startswith(_BASE_DIR):
148
- continue
149
-
150
- is_dir = os.path.isdir(resolved)
151
-
152
- # Extension filtering (files only)
153
- if extensions and not is_dir:
154
- ext = os.path.splitext(resolved)[1].lower().lstrip(".")
155
- if ext not in {e.lower().lstrip(".") for e in extensions}:
156
- continue
157
-
158
- stat = os.stat(resolved)
159
-
160
- results.append(
161
- {
162
- "path": os.path.relpath(resolved, _BASE_DIR),
163
- "type": "directory" if is_dir else "file",
164
- "size_bytes": stat.st_size,
165
- "modified_time": stat.st_mtime,
166
- }
167
- )
168
-
169
- if len(results) >= max_results:
170
- break
171
-
172
- # Sorting
173
- if sort_by == "path":
174
- results.sort(key=lambda x: x["path"])
175
- elif sort_by == "modified":
176
- results.sort(key=lambda x: x["modified_time"], reverse=True)
177
- elif sort_by == "size":
178
- results.sort(key=lambda x: x["size_bytes"], reverse=True)
179
-
180
- return {"pattern": pattern, "count": len(results), "results": results}
181
-
182
-
183
22
  # -----------------------------
184
23
  # Expression evaluation tools
185
24
  # -----------------------------
@@ -522,19 +361,12 @@ def install(ctx):
522
361
  g_ctx = ctx
523
362
  group = "core_tools"
524
363
  # Examples of registering tools using automatic definition generation
525
- ctx.register_tool(memory_read, group=group)
526
- ctx.register_tool(memory_write, group=group)
527
- # ctx.register_tool(semantic_search) # TODO: implement
528
- ctx.register_tool(read_file, group=group)
529
- ctx.register_tool(write_file, group=group)
530
- ctx.register_tool(list_directory, group=group)
531
- ctx.register_tool(glob_paths, group=group)
364
+ ctx.register_tool(get_current_time, group=group)
532
365
  ctx.register_tool(calc, group=group)
533
366
  ctx.register_tool(run_python, group=group)
534
367
  ctx.register_tool(run_typescript, group=group)
535
368
  ctx.register_tool(run_javascript, group=group)
536
369
  ctx.register_tool(run_csharp, group=group)
537
- ctx.register_tool(get_current_time, group=group)
538
370
 
539
371
  def exec_language(language: str, code: str) -> Dict[str, Any]:
540
372
  if language == "python":
@@ -921,14 +921,23 @@ export default {
921
921
  },
922
922
  show({ thread }) {
923
923
  if (thread.messages.length < 2) return false
924
- const msgRoles = thread.messages.map(m => m.role)
925
- if (msgRoles[msgRoles.length - 1] != "assistant") return false
924
+
925
+ const lastMessage = thread.messages[thread.messages.length - 1]
926
+ // only show if the last message is from the assistant
927
+ if (lastMessage.role != "assistant") return false
928
+
929
+ // and it has a skill tool call
926
930
  const hasSkillToolCall = thread.messages.some(m =>
927
931
  m.tool_calls?.some(tc => tc.type == "function" && tc.function.name == "skill"))
932
+ // or a plan system prompt
928
933
  const systemPrompt = thread.messages.find(m => m.role == "system")?.content.toLowerCase() || ''
929
934
  const line1 = leftPart(systemPrompt.trim(), "\n")
930
935
  const hasPlanSystemPrompt = line1.includes("plan") || systemPrompt.includes("# plan")
931
- return hasSkillToolCall || hasPlanSystemPrompt
936
+
937
+ // or the last message has no content but has reasoning
938
+ const hasOnlyThinking = !lastMessage.content?.trim() && lastMessage.reasoning?.trim()
939
+
940
+ return hasSkillToolCall || hasPlanSystemPrompt || hasOnlyThinking
932
941
  }
933
942
  }
934
943
  })
llms/main.py CHANGED
@@ -57,7 +57,7 @@ except ImportError:
57
57
  HAS_PIL = False
58
58
 
59
59
  _ROOT = None
60
- VERSION = "3.0.26"
60
+ VERSION = "3.0.27"
61
61
  DEBUG = os.getenv("DEBUG") == "1"
62
62
  MOCK = os.getenv("MOCK") == "1"
63
63
  MOCK_DIR = os.getenv("MOCK_DIR")
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.26',
9
+ version: '3.0.27',
10
10
  base,
11
11
  prefsKey,
12
12
  welcome: 'Welcome to llms.py',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llms-py
3
- Version: 3.0.26
3
+ Version: 3.0.27
4
4
  Summary: A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers
5
5
  Home-page: https://github.com/ServiceStack/llms
6
6
  Author: ServiceStack
@@ -3,7 +3,7 @@ llms/__main__.py,sha256=hrBulHIt3lmPm1BCyAEVtB6DQ0Hvc3gnIddhHCmJasg,151
3
3
  llms/db.py,sha256=oozp5I5lECVO8oZEFwcZl3ES5mARqWeR1BkoqG5kSqM,11687
4
4
  llms/index.html,sha256=nGk1Djtn9p7l6LuKp4Kg0JIB9fCzxtTWXFfmDb4ggpc,1658
5
5
  llms/llms.json,sha256=ar7f5uti80RrYhbsYLtCuqdHSFWWlwyVeV0nPCgUzeA,11564
6
- llms/main.py,sha256=I_Qn2-z-JLRvO4IC5Hww_3fTjYE3PnpIaFcV_GLzTtE,176657
6
+ llms/main.py,sha256=JETnTf2TkJd14WTM6nkfiaqH6VZ72JnaXY_ySWb2uZY,176657
7
7
  llms/providers-extra.json,sha256=_6DmGBiQY9LM6_Y0zOiObYn7ba4g3akSNQfmHcYlENc,11101
8
8
  llms/providers.json,sha256=yls3OUqPIBLSf2rk0xgwUHKkvd-8drGq4JW7w49rEws,299324
9
9
  llms/extensions/analytics/ui/index.mjs,sha256=m1XwaqYCLwK267JAUCAltkN_nOXep0GxfpvGNS5i4_w,69547
@@ -19,11 +19,11 @@ llms/extensions/computer/base.py,sha256=Igio5R6kPQOxIbmpaA7X6j6eC4cpF3jwTTR8rURf
19
19
  llms/extensions/computer/bash.py,sha256=-xo67wVAdrqxtXgR7MK-iAkJ4Wne7Dm1JmnuHC2xW8o,5953
20
20
  llms/extensions/computer/computer.py,sha256=wehwcrYwi9usCRcziE_loMhWDbVgfjLk_T4_4TZa4W4,19642
21
21
  llms/extensions/computer/edit.py,sha256=QluhvRhYSSQJfbih4QyfC4M8W8aVqiOApfYXZgZTI5M,12725
22
- llms/extensions/computer/filesystem.py,sha256=DoyCwu8PJxhtstX39MV7cdjXu7uKzWz9yyCEEYXhzqA,21196
22
+ llms/extensions/computer/filesystem.py,sha256=V6UI2rtGxreQyPvYnXfeQQuLocTDloAQU1k_jCTybnY,21825
23
23
  llms/extensions/computer/platform.py,sha256=w5ECar8lM4Lag7rTYUQmU7wEWaqCeejNXwwM3CB8ulQ,14866
24
24
  llms/extensions/computer/run.py,sha256=ZIcoYyy2cc3IKR_T4yJgx6IUHu2m7UusIJi9Dx1s7dA,1566
25
25
  llms/extensions/core_tools/CALCULATOR.md,sha256=pJRtCVF01BgxFrSNh2Ys_lrRi3SFwLgJzAX93AGh93Q,1944
26
- llms/extensions/core_tools/__init__.py,sha256=w8ovJRgXsvrcL8NF-XOrhuBE1oQXfnSQo-Xu7ww3NQY,21641
26
+ llms/extensions/core_tools/__init__.py,sha256=YazB9yM5mLEtZWmCb_Dguz9XU2aPw4O6QU2Jk0vWI44,16422
27
27
  llms/extensions/core_tools/ui/index.mjs,sha256=KycJ2FcQ6BieBY7fjWGxVBGHN6WuFx712OFrO6flXww,31770
28
28
  llms/extensions/core_tools/ui/codemirror/codemirror.css,sha256=60lOqXLSZh74b39qxlbdZ4bXIeScnBtG4euWfktvm_M,8720
29
29
  llms/extensions/core_tools/ui/codemirror/codemirror.js,sha256=7cA89SlK249o7tVfiEWIiqDEA6ZEWxX4CoZmofVA14s,402008
@@ -156,7 +156,7 @@ llms/extensions/skills/installer.py,sha256=GfNYRK6LbYSZtIOesQ_fQvjtBsiZC9sTXLVuU
156
156
  llms/extensions/skills/models.py,sha256=xmRfz8BMeOdzZXhW6MYFjkOVHOKD6bMDynId8aysans,1461
157
157
  llms/extensions/skills/parser.py,sha256=Mb4NOtoY3Ip4Nng8ixb26oE8U_Sp5CI3n3l_qxbg1UM,5500
158
158
  llms/extensions/skills/validator.py,sha256=te49hTfIPJWcMcLLCApSJ2Ru3lrqVF8ayDXtPEZF9sU,5154
159
- llms/extensions/skills/ui/index.mjs,sha256=1hdvU9dOcYWzNosqoKtGhmy6PwpQx3FX0KAhFi_fxkI,63207
159
+ llms/extensions/skills/ui/index.mjs,sha256=YYVaa4qWiLrcj9USnQWHz6NVblXzahV7R9SVJ-irUPc,63587
160
160
  llms/extensions/skills/ui/data/skills-top-5000.json,sha256=e8fUF_NQSUZRK0rGRUqBFecW5JEX6QO6H_P3WqTBBQ4,540056
161
161
  llms/extensions/skills/ui/skills/create-plan/SKILL.md,sha256=ZAtiM2qPHcc8Z3Ongl1NgX5ythITPwyvcIqisgqWrGA,2493
162
162
  llms/extensions/skills/ui/skills/skill-creator/LICENSE.txt,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
@@ -173,7 +173,7 @@ llms/extensions/system_prompts/ui/prompts.json,sha256=t5DD3bird-87wFa4OlW-bC2wdo
173
173
  llms/extensions/tools/__init__.py,sha256=PRZe0QMfsOymJ3jTqO0VFppNEWI4f2bYSOImK_YrGQM,2036
174
174
  llms/extensions/tools/ui/index.mjs,sha256=1TgCn74oX_rUAhxO8w54HlIgNkHnI5ma-GCqXp-qYVY,39434
175
175
  llms/ui/App.mjs,sha256=8yljf7M7LUp4q7XPEHTCUKpJB3X2d8ePnatRamwTM00,7622
176
- llms/ui/ai.mjs,sha256=7IO43ppX3hHoVcIEqdSB5dstw9pGsiZly4TtITgLp9U,6540
176
+ llms/ui/ai.mjs,sha256=s3DYVkugdmqbkjePM4-koPoy7kPIFyAqGU8M__K2sI8,6540
177
177
  llms/ui/app.css,sha256=SVVzmFhTd0chuGq5yhv3FjgNudgo6WXlM2fnb-csK4c,190220
178
178
  llms/ui/ctx.mjs,sha256=4x-LTmofhf6OvLThSlDSTQOsLkzyBFOEMRGIOLHszqs,14974
179
179
  llms/ui/fav.svg,sha256=_R6MFeXl6wBFT0lqcUxYQIDWgm246YH_3hSTW0oO8qw,734
@@ -199,9 +199,9 @@ llms/ui/modules/model-selector.mjs,sha256=6U4rAZ7vmQELFRQGWk4YEtq02v3lyHdMq6yUOp
199
199
  llms/ui/modules/chat/ChatBody.mjs,sha256=OyjAQPHNIbdqEhQq01ysbx7Cbt1CezUERbgFpcbnrNI,58081
200
200
  llms/ui/modules/chat/SettingsDialog.mjs,sha256=HMBJTwrapKrRIAstIIqp0QlJL5O-ho4hzgvfagPfsX8,19930
201
201
  llms/ui/modules/chat/index.mjs,sha256=nS_L6G1RSuCybgnA6n-q8Sn3OeSbQWL2iW3-zCIFqJk,39548
202
- llms_py-3.0.26.dist-info/licenses/LICENSE,sha256=bus9cuAOWeYqBk2OuhSABVV1P4z7hgrEFISpyda_H5w,1532
203
- llms_py-3.0.26.dist-info/METADATA,sha256=iuijSDRsS7hynJZwGv__4nT6Yh_smbtGmSGn-lLEY6U,2195
204
- llms_py-3.0.26.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
205
- llms_py-3.0.26.dist-info/entry_points.txt,sha256=WswyE7PfnkZMIxboC-MS6flBD6wm-CYU7JSUnMhqMfM,40
206
- llms_py-3.0.26.dist-info/top_level.txt,sha256=gC7hk9BKSeog8gyg-EM_g2gxm1mKHwFRfK-10BxOsa4,5
207
- llms_py-3.0.26.dist-info/RECORD,,
202
+ llms_py-3.0.27.dist-info/licenses/LICENSE,sha256=bus9cuAOWeYqBk2OuhSABVV1P4z7hgrEFISpyda_H5w,1532
203
+ llms_py-3.0.27.dist-info/METADATA,sha256=Ojx7FD0AjI3enkul3b9wiEqEVnDf0Yo_QN-3uynyN4c,2195
204
+ llms_py-3.0.27.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
205
+ llms_py-3.0.27.dist-info/entry_points.txt,sha256=WswyE7PfnkZMIxboC-MS6flBD6wm-CYU7JSUnMhqMfM,40
206
+ llms_py-3.0.27.dist-info/top_level.txt,sha256=gC7hk9BKSeog8gyg-EM_g2gxm1mKHwFRfK-10BxOsa4,5
207
+ llms_py-3.0.27.dist-info/RECORD,,