llms-py 3.0.0b6__tar.gz → 3.0.0b7__tar.gz

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 (100) hide show
  1. {llms_py-3.0.0b6/llms_py.egg-info → llms_py-3.0.0b7}/PKG-INFO +1 -1
  2. llms_py-3.0.0b7/llms/__pycache__/main.cpython-314.pyc +0 -0
  3. llms_py-3.0.0b6/llms/ui/modules/analytics.mjs → llms_py-3.0.0b7/llms/extensions/analytics/ui/index.mjs +4 -2
  4. llms_py-3.0.0b7/llms/extensions/core_tools/__init__.py +358 -0
  5. llms_py-3.0.0b7/llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  6. llms_py-3.0.0b7/llms/extensions/gallery/__init__.py +61 -0
  7. llms_py-3.0.0b7/llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  8. llms_py-3.0.0b7/llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  9. llms_py-3.0.0b7/llms/extensions/gallery/db.py +298 -0
  10. llms_py-3.0.0b7/llms/extensions/gallery/ui/index.mjs +480 -0
  11. llms_py-3.0.0b7/llms/extensions/providers/__init__.py +18 -0
  12. llms_py-3.0.0b7/llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  13. {llms_py-3.0.0b6/llms → llms_py-3.0.0b7/llms/extensions}/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  14. llms_py-3.0.0b7/llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  15. llms_py-3.0.0b7/llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  16. {llms_py-3.0.0b6/llms → llms_py-3.0.0b7/llms/extensions}/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  17. {llms_py-3.0.0b6/llms → llms_py-3.0.0b7/llms/extensions}/providers/__pycache__/openai.cpython-314.pyc +0 -0
  18. llms_py-3.0.0b7/llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  19. {llms_py-3.0.0b6/llms → llms_py-3.0.0b7/llms/extensions}/providers/anthropic.py +1 -4
  20. {llms_py-3.0.0b6/llms → llms_py-3.0.0b7/llms/extensions}/providers/chutes.py +21 -18
  21. {llms_py-3.0.0b6/llms → llms_py-3.0.0b7/llms/extensions}/providers/google.py +99 -27
  22. {llms_py-3.0.0b6/llms → llms_py-3.0.0b7/llms/extensions}/providers/nvidia.py +6 -8
  23. {llms_py-3.0.0b6/llms → llms_py-3.0.0b7/llms/extensions}/providers/openai.py +3 -6
  24. {llms_py-3.0.0b6/llms → llms_py-3.0.0b7/llms/extensions}/providers/openrouter.py +12 -10
  25. llms_py-3.0.0b7/llms/extensions/system_prompts/__init__.py +45 -0
  26. llms_py-3.0.0b7/llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  27. llms_py-3.0.0b7/llms/extensions/system_prompts/ui/index.mjs +284 -0
  28. llms_py-3.0.0b7/llms/extensions/system_prompts/ui/prompts.json +1067 -0
  29. llms_py-3.0.0b6/llms/ui/modules/tools.mjs → llms_py-3.0.0b7/llms/extensions/tools/ui/index.mjs +4 -2
  30. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/llms.json +17 -1
  31. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/main.py +381 -170
  32. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/providers-extra.json +0 -32
  33. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/App.mjs +17 -18
  34. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/ai.mjs +10 -3
  35. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/app.css +1553 -24
  36. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/ctx.mjs +70 -12
  37. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/index.mjs +13 -8
  38. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/modules/chat/ChatBody.mjs +11 -248
  39. llms_py-3.0.0b7/llms/ui/modules/chat/HomeTools.mjs +254 -0
  40. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/modules/chat/SettingsDialog.mjs +1 -1
  41. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/modules/chat/index.mjs +278 -174
  42. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/modules/layout.mjs +2 -26
  43. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/modules/model-selector.mjs +1 -1
  44. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/modules/threads/index.mjs +5 -11
  45. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/modules/threads/threadStore.mjs +56 -2
  46. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/utils.mjs +21 -3
  47. {llms_py-3.0.0b6 → llms_py-3.0.0b7/llms_py.egg-info}/PKG-INFO +1 -1
  48. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms_py.egg-info/SOURCES.txt +29 -14
  49. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/pyproject.toml +2 -2
  50. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/setup.py +1 -1
  51. llms_py-3.0.0b7/tests/test_extensions.py +48 -0
  52. llms_py-3.0.0b6/llms/__pycache__/main.cpython-314.pyc +0 -0
  53. llms_py-3.0.0b6/llms/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  54. llms_py-3.0.0b6/llms/providers/__pycache__/google.cpython-314.pyc +0 -0
  55. llms_py-3.0.0b6/llms/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  56. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/LICENSE +0 -0
  57. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/MANIFEST.in +0 -0
  58. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/README.md +0 -0
  59. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/__init__.py +0 -0
  60. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/__main__.py +0 -0
  61. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/__pycache__/__init__.cpython-312.pyc +0 -0
  62. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/__pycache__/__init__.cpython-313.pyc +0 -0
  63. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/__pycache__/__init__.cpython-314.pyc +0 -0
  64. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/__pycache__/__main__.cpython-312.pyc +0 -0
  65. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/__pycache__/__main__.cpython-314.pyc +0 -0
  66. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/__pycache__/llms.cpython-312.pyc +0 -0
  67. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/__pycache__/main.cpython-312.pyc +0 -0
  68. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/__pycache__/main.cpython-313.pyc +0 -0
  69. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/__pycache__/plugins.cpython-314.pyc +0 -0
  70. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/index.html +0 -0
  71. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/providers.json +0 -0
  72. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/fav.svg +0 -0
  73. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/lib/chart.js +0 -0
  74. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/lib/charts.mjs +0 -0
  75. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/lib/color.js +0 -0
  76. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/lib/highlight.min.mjs +0 -0
  77. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/lib/idb.min.mjs +0 -0
  78. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/lib/marked.min.mjs +0 -0
  79. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/lib/servicestack-client.mjs +0 -0
  80. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/lib/servicestack-vue.mjs +0 -0
  81. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/lib/vue-router.min.mjs +0 -0
  82. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/lib/vue.min.mjs +0 -0
  83. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/lib/vue.mjs +0 -0
  84. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/markdown.mjs +0 -0
  85. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/modules/threads/Recents.mjs +0 -0
  86. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/tailwind.input.css +0 -0
  87. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms/ui/typography.css +0 -0
  88. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms_py.egg-info/dependency_links.txt +0 -0
  89. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms_py.egg-info/entry_points.txt +0 -0
  90. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms_py.egg-info/not-zip-safe +0 -0
  91. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms_py.egg-info/requires.txt +0 -0
  92. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/llms_py.egg-info/top_level.txt +0 -0
  93. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/requirements.txt +0 -0
  94. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/setup.cfg +0 -0
  95. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/tests/test_async.py +0 -0
  96. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/tests/test_config.py +0 -0
  97. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/tests/test_integration.py +0 -0
  98. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/tests/test_provider_checks.py +0 -0
  99. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/tests/test_provider_config.py +0 -0
  100. {llms_py-3.0.0b6 → llms_py-3.0.0b7}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llms-py
3
- Version: 3.0.0b6
3
+ Version: 3.0.0b7
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
@@ -339,8 +339,8 @@ export const Analytics = {
339
339
  <div class="flex-1 min-w-0 w-full">
340
340
  <div class="flex flex-col sm:flex-row justify-between gap-2 mb-2">
341
341
  <div class="flex items-center gap-2 flex-wrap">
342
- <span class="text-xs px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 rounded font-medium">{{ request.model }}</span>
343
- <span class="text-xs px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300 rounded font-medium">{{ request.provider }}</span>
342
+ <span v-if="request.model" class="text-xs px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 rounded font-medium">{{ request.model }}</span>
343
+ <span v-if="request.provider" class="text-xs px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300 rounded font-medium">{{ request.provider }}</span>
344
344
  <span v-if="request.providerRef" class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 rounded font-medium">{{ request.providerRef }}</span>
345
345
  <span v-if="request.finishReason" class="text-xs px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300 rounded font-medium">{{ request.finishReason }}</span>
346
346
  </div>
@@ -1531,6 +1531,8 @@ export const Analytics = {
1531
1531
  }
1532
1532
 
1533
1533
  export default {
1534
+ order: 20 - 100,
1535
+
1534
1536
  install(ctx) {
1535
1537
  ctx.components({
1536
1538
  MonthSelector,
@@ -0,0 +1,358 @@
1
+ """
2
+ Core System Tools providing essential file operations, memory persistence, math expression evaluation, and code execution
3
+ """
4
+
5
+ import ast
6
+ import glob
7
+ import json
8
+ import math
9
+ import operator
10
+ import os
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ from datetime import datetime, timezone
15
+ from statistics import mean, median, stdev, variance
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ # -----------------------------
19
+ # In-memory storage (replace later)
20
+ # -----------------------------
21
+
22
+ _MEMORY_STORE: Dict[str, Any] = {}
23
+ _SEMANTIC_STORE: List[Dict[str, Any]] = [] # {id, text, metadata}
24
+
25
+
26
+ # -----------------------------
27
+ # Memory tools
28
+ # -----------------------------
29
+
30
+
31
+ def memory_read(key: str) -> Any:
32
+ """Read a value from persistent memory."""
33
+ return _MEMORY_STORE.get(key)
34
+
35
+
36
+ def memory_write(key: str, value: Any) -> bool:
37
+ """Write a value to persistent memory."""
38
+ _MEMORY_STORE[key] = value
39
+ return True
40
+
41
+
42
+ # -----------------------------
43
+ # Path safety helpers
44
+ # -----------------------------
45
+
46
+ # Limit tools to only access files and folders within LLMS_BASE_DIR if specified, otherwise the current working directory
47
+ _BASE_DIR = os.environ.get("LLMS_BASE_DIR") or os.path.realpath(os.getcwd())
48
+
49
+
50
+ def _resolve_safe_path(path: str) -> str:
51
+ """
52
+ Resolve a path and ensure it stays within the current working directory.
53
+ Raises ValueError if the path escapes the base directory.
54
+ """
55
+ resolved = os.path.realpath(os.path.join(_BASE_DIR, path))
56
+ if not resolved.startswith(_BASE_DIR + os.sep) and resolved != _BASE_DIR:
57
+ raise ValueError("Access denied: path is outside the working directory")
58
+ return resolved
59
+
60
+
61
+ # -----------------------------
62
+ # Semantic search (placeholder)
63
+ # -----------------------------
64
+
65
+
66
+ def semantic_search(query: str, top_k: int = 5) -> List[Dict[str, Any]]:
67
+ """
68
+ Naive semantic search placeholder.
69
+ Replace with embeddings + vector DB.
70
+ """
71
+ results = []
72
+ for item in _SEMANTIC_STORE:
73
+ if query.lower() in item["text"].lower():
74
+ results.append(item)
75
+ return results[:top_k]
76
+
77
+
78
+ # -----------------------------
79
+ # File system tools (restricted to CWD)
80
+ # -----------------------------
81
+
82
+
83
+ def read_file(path: str) -> str:
84
+ """Read a text file from disk within the current working directory."""
85
+ safe_path = _resolve_safe_path(path)
86
+ with open(safe_path, encoding="utf-8") as f:
87
+ return f.read()
88
+
89
+
90
+ def write_file(path: str, content: str) -> bool:
91
+ """Write text to a file within the current working directory (overwrites)."""
92
+ safe_path = _resolve_safe_path(path)
93
+ os.makedirs(os.path.dirname(safe_path) or _BASE_DIR, exist_ok=True)
94
+ with open(safe_path, "w", encoding="utf-8") as f:
95
+ f.write(content)
96
+ return True
97
+
98
+
99
+ def list_directory(path: str) -> str:
100
+ """List directory contents"""
101
+ safe_path = _resolve_safe_path(path)
102
+ if not os.path.exists(safe_path):
103
+ return f"Error: Path not found: {path}"
104
+
105
+ entries = []
106
+ try:
107
+ for entry in os.scandir(safe_path):
108
+ stat = entry.stat()
109
+ entries.append(
110
+ {
111
+ "name": "/" + entry.name if entry.is_dir() else entry.name,
112
+ "size": stat.st_size,
113
+ "mtime": datetime.fromtimestamp(stat.st_mtime).isoformat(),
114
+ }
115
+ )
116
+ return json.dumps({"path": os.path.relpath(safe_path, _BASE_DIR), "entries": entries}, indent=2)
117
+ except Exception as e:
118
+ return f"Error listing directory: {e}"
119
+
120
+
121
+ def glob_paths(
122
+ pattern: str,
123
+ extensions: Optional[List[str]] = None,
124
+ sort_by: str = "path", # "path" | "modified" | "size"
125
+ max_results: int = 100,
126
+ ) -> Dict[str, List[Dict[str, str]]]:
127
+ """
128
+ Find files and directories matching a glob pattern
129
+ """
130
+ if sort_by not in {"path", "modified", "size"}:
131
+ raise ValueError("sort_by must be one of: path, modified, size")
132
+
133
+ safe_pattern = _resolve_safe_path(pattern)
134
+
135
+ results = []
136
+
137
+ for path in glob.glob(safe_pattern, recursive=True):
138
+ resolved = os.path.realpath(path)
139
+
140
+ # Enforce CWD restriction (important for symlinks)
141
+ if not resolved.startswith(_BASE_DIR):
142
+ continue
143
+
144
+ is_dir = os.path.isdir(resolved)
145
+
146
+ # Extension filtering (files only)
147
+ if extensions and not is_dir:
148
+ ext = os.path.splitext(resolved)[1].lower().lstrip(".")
149
+ if ext not in {e.lower().lstrip(".") for e in extensions}:
150
+ continue
151
+
152
+ stat = os.stat(resolved)
153
+
154
+ results.append(
155
+ {
156
+ "path": os.path.relpath(resolved, _BASE_DIR),
157
+ "type": "directory" if is_dir else "file",
158
+ "size_bytes": stat.st_size,
159
+ "modified_time": stat.st_mtime,
160
+ }
161
+ )
162
+
163
+ if len(results) >= max_results:
164
+ break
165
+
166
+ # Sorting
167
+ if sort_by == "path":
168
+ results.sort(key=lambda x: x["path"])
169
+ elif sort_by == "modified":
170
+ results.sort(key=lambda x: x["modified_time"], reverse=True)
171
+ elif sort_by == "size":
172
+ results.sort(key=lambda x: x["size_bytes"], reverse=True)
173
+
174
+ return {"pattern": pattern, "count": len(results), "results": results}
175
+
176
+
177
+ # -----------------------------
178
+ # Expression evaluation tools
179
+ # -----------------------------
180
+
181
+
182
+ def calc(expression: str) -> str:
183
+ """Evaluate a mathematical expression with boolean operations"""
184
+ # 1. Define allowed operators
185
+ operators = {
186
+ ast.Add: operator.add,
187
+ ast.Sub: operator.sub,
188
+ ast.Mult: operator.mul,
189
+ ast.Div: operator.truediv,
190
+ ast.Pow: operator.pow,
191
+ ast.USub: operator.neg,
192
+ ast.Mod: operator.mod,
193
+ # Comparison operators
194
+ ast.Eq: operator.eq,
195
+ ast.NotEq: operator.ne,
196
+ ast.Lt: operator.lt,
197
+ ast.LtE: operator.le,
198
+ ast.Gt: operator.gt,
199
+ ast.GtE: operator.ge,
200
+ # Boolean operators
201
+ ast.And: operator.and_,
202
+ ast.Or: operator.or_,
203
+ ast.Not: operator.not_,
204
+ }
205
+
206
+ # 2. Define allowed math functions and constants
207
+ allowed_functions = {name: getattr(math, name) for name in dir(math) if not name.startswith("_")}
208
+ allowed_functions.update(
209
+ {
210
+ "mod": operator.mod,
211
+ "mean": mean,
212
+ "median": median,
213
+ "stdev": stdev,
214
+ "variance": variance,
215
+ "abs": abs,
216
+ "min": min,
217
+ "max": max,
218
+ "sum": sum,
219
+ "round": round,
220
+ }
221
+ )
222
+
223
+ def eval_node(node):
224
+ if isinstance(node, ast.Constant): # Numbers and booleans
225
+ return node.value
226
+ elif isinstance(node, ast.BinOp): # Binary Ops (1 + 2)
227
+ return operators[type(node.op)](eval_node(node.left), eval_node(node.right))
228
+ elif isinstance(node, ast.UnaryOp): # Unary Ops (-5, not True)
229
+ return operators[type(node.op)](eval_node(node.operand))
230
+ elif isinstance(node, ast.Compare): # Comparison (5 > 3)
231
+ left = eval_node(node.left)
232
+ for op, comparator in zip(node.ops, node.comparators):
233
+ right = eval_node(comparator)
234
+ if not operators[type(op)](left, right):
235
+ return False
236
+ left = right
237
+ return True
238
+ elif isinstance(node, ast.BoolOp): # Boolean operations (True and False, True or False)
239
+ op = operators[type(node.op)]
240
+ if isinstance(node.op, ast.And):
241
+ # Short-circuit evaluation for 'and'
242
+ result = True
243
+ for value in node.values:
244
+ result = eval_node(value)
245
+ if not result:
246
+ return False
247
+ return result
248
+ elif isinstance(node.op, ast.Or):
249
+ # Short-circuit evaluation for 'or'
250
+ for value in node.values:
251
+ result = eval_node(value)
252
+ if result:
253
+ return True
254
+ return False
255
+ elif isinstance(node, ast.Call): # Function calls (sqrt(16))
256
+ func_name = node.func.id
257
+ if func_name in allowed_functions:
258
+ args = [eval_node(arg) for arg in node.args]
259
+ return allowed_functions[func_name](*args)
260
+ raise NameError(f"Function '{func_name}' is not allowed.")
261
+ elif isinstance(node, ast.Name): # Constants (pi, e, True, False)
262
+ if node.id in allowed_functions:
263
+ return allowed_functions[node.id]
264
+ elif node.id in ("True", "False"):
265
+ return node.id == "True"
266
+ raise NameError(f"Variable '{node.id}' is not defined.")
267
+ elif isinstance(node, ast.List): # List literals [1, 2, 3]
268
+ return [eval_node(item) for item in node.elts]
269
+ else:
270
+ raise TypeError(f"Unsupported operation: {type(node).__name__}")
271
+
272
+ # Replace XOR with power
273
+ expression = expression.replace("^", "**")
274
+
275
+ # Parse and evaluate
276
+ node = ast.parse(expression, mode="eval").body
277
+ return eval_node(node)
278
+
279
+
280
+ # -----------------------------
281
+ # Python execution tool
282
+ # -----------------------------
283
+
284
+
285
+ def run_python(code: str) -> Dict[str, Any]:
286
+ """
287
+ Execute Python code in a temporary sandboxed environment.
288
+ Uses ulimit for resource restriction and runs in a temporary directory.
289
+ """
290
+ with tempfile.TemporaryDirectory() as temp_dir:
291
+ script_path = os.path.join(temp_dir, "script.py")
292
+
293
+ with open(script_path, "w", encoding="utf-8") as f:
294
+ f.write(code)
295
+
296
+ # Construct command with resource limits
297
+ # ulimit -t 5: Max CPU time 5 seconds
298
+ # ulimit -v 1048576: Max virtual memory 1GB
299
+ cmd = f"ulimit -t 5; ulimit -v 1048576; {sys.executable} script.py"
300
+
301
+ try:
302
+ # Run with restricted environment
303
+ # We keep PATH to find basic tools if needed, but remove sensitive vars
304
+ clean_env = {"PATH": os.environ.get("PATH", "")}
305
+
306
+ result = subprocess.run(
307
+ ["bash", "-c", cmd], cwd=temp_dir, env=clean_env, capture_output=True, text=True, timeout=10
308
+ )
309
+ return {"stdout": result.stdout, "stderr": result.stderr, "returncode": result.returncode}
310
+ except subprocess.TimeoutExpired:
311
+ return {"stdout": "", "stderr": "Execution timed out", "returncode": -1}
312
+ except Exception as e:
313
+ return {"stdout": "", "stderr": f"Error: {e}", "returncode": -1}
314
+
315
+
316
+ # -----------------------------
317
+ # Time tool
318
+ # -----------------------------
319
+
320
+
321
+ def get_current_time(tz_name: Optional[str] = None) -> str:
322
+ """
323
+ Get current time in ISO-8601 format.
324
+
325
+ Args:
326
+ tz_name: Optional timezone name (e.g. 'America/New_York'). Defaults to UTC.
327
+ """
328
+ if tz_name:
329
+ try:
330
+ try:
331
+ from zoneinfo import ZoneInfo
332
+ except ImportError:
333
+ from backports.zoneinfo import ZoneInfo
334
+
335
+ tz = ZoneInfo(tz_name)
336
+ except Exception:
337
+ return f"Error: Invalid timezone '{tz_name}'"
338
+ else:
339
+ tz = timezone.utc
340
+
341
+ return datetime.now(tz).isoformat()
342
+
343
+
344
+ def install(ctx):
345
+ # Examples of registering tools using automatic definition generation
346
+ ctx.register_tool(memory_read)
347
+ ctx.register_tool(memory_write)
348
+ ctx.register_tool(semantic_search)
349
+ ctx.register_tool(read_file)
350
+ ctx.register_tool(write_file)
351
+ ctx.register_tool(list_directory)
352
+ ctx.register_tool(glob_paths)
353
+ ctx.register_tool(calc)
354
+ ctx.register_tool(run_python)
355
+ ctx.register_tool(get_current_time)
356
+
357
+
358
+ __install__ = install
@@ -0,0 +1,61 @@
1
+ import json
2
+ import os
3
+
4
+ from aiohttp import web
5
+
6
+ g_db = None
7
+
8
+ try:
9
+ from llms.extensions.gallery.db import GalleryDB
10
+ except ImportError as e:
11
+ print(f"Failed to import GalleryDB: {e}")
12
+ GalleryDB = None
13
+
14
+
15
+ def install(ctx):
16
+ def get_gallery_db():
17
+ global g_db
18
+ if g_db is None and GalleryDB:
19
+ try:
20
+ db_path = os.path.join(ctx.get_user_path(), "gallery", "gallery.sqlite")
21
+ g_db = GalleryDB(ctx, db_path)
22
+ except Exception as e:
23
+ ctx.err("Failed to init GalleryDB", e)
24
+ return g_db
25
+
26
+ if not get_gallery_db():
27
+ return
28
+
29
+ def on_cache_save(context):
30
+ url = context["url"]
31
+ info = context["info"]
32
+ ctx.log(f"cache saved: {url}")
33
+ ctx.log(json.dumps(info, indent=2))
34
+
35
+ if "url" not in info:
36
+ info["url"] = url
37
+ g_db.insert_media(info)
38
+
39
+ ctx.register_cache_saved_filter(on_cache_save)
40
+
41
+ async def query_media(request):
42
+ rows = g_db.query_media(request.query, user=ctx.get_username(request))
43
+ return web.json_response(rows)
44
+
45
+ ctx.add_get("media", query_media)
46
+
47
+ async def media_totals(request):
48
+ rows = g_db.media_totals(user=ctx.get_username(request))
49
+ return web.json_response(rows)
50
+
51
+ ctx.add_get("media/totals", media_totals)
52
+
53
+ async def delete_media(request):
54
+ hash = request.match_info["hash"]
55
+ g_db.delete_media(hash, user=ctx.get_username(request))
56
+ return web.json_response({})
57
+
58
+ ctx.add_delete("media/{hash}", delete_media)
59
+
60
+
61
+ __install__ = install