llms-py 2.0.20__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 (207) hide show
  1. llms/__init__.py +3 -1
  2. llms/db.py +359 -0
  3. llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +254 -327
  4. llms/extensions/app/README.md +20 -0
  5. llms/extensions/app/__init__.py +588 -0
  6. llms/extensions/app/db.py +540 -0
  7. llms/{ui → extensions/app/ui}/Recents.mjs +99 -73
  8. llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +139 -68
  9. llms/extensions/app/ui/threadStore.mjs +440 -0
  10. llms/extensions/computer/README.md +96 -0
  11. llms/extensions/computer/__init__.py +59 -0
  12. llms/extensions/computer/base.py +80 -0
  13. llms/extensions/computer/bash.py +185 -0
  14. llms/extensions/computer/computer.py +523 -0
  15. llms/extensions/computer/edit.py +299 -0
  16. llms/extensions/computer/filesystem.py +542 -0
  17. llms/extensions/computer/platform.py +461 -0
  18. llms/extensions/computer/run.py +37 -0
  19. llms/extensions/core_tools/CALCULATOR.md +32 -0
  20. llms/extensions/core_tools/__init__.py +599 -0
  21. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  22. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  23. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  24. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  25. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  26. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  27. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  28. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  29. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  30. llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
  31. llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
  32. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  33. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  34. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  35. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  36. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  37. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  38. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  39. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  40. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  41. llms/extensions/core_tools/ui/index.mjs +650 -0
  42. llms/extensions/gallery/README.md +61 -0
  43. llms/extensions/gallery/__init__.py +63 -0
  44. llms/extensions/gallery/db.py +243 -0
  45. llms/extensions/gallery/ui/index.mjs +482 -0
  46. llms/extensions/katex/README.md +39 -0
  47. llms/extensions/katex/__init__.py +6 -0
  48. llms/extensions/katex/ui/README.md +125 -0
  49. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  50. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  51. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  52. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  53. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  54. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  55. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  56. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  57. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  58. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  59. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  60. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  61. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  62. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  63. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  120. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  121. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  122. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  123. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  124. llms/extensions/katex/ui/index.mjs +92 -0
  125. llms/extensions/katex/ui/katex-swap.css +1230 -0
  126. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  127. llms/extensions/katex/ui/katex.css +1230 -0
  128. llms/extensions/katex/ui/katex.js +19080 -0
  129. llms/extensions/katex/ui/katex.min.css +1 -0
  130. llms/extensions/katex/ui/katex.min.js +1 -0
  131. llms/extensions/katex/ui/katex.min.mjs +1 -0
  132. llms/extensions/katex/ui/katex.mjs +18547 -0
  133. llms/extensions/providers/__init__.py +22 -0
  134. llms/extensions/providers/anthropic.py +260 -0
  135. llms/extensions/providers/cerebras.py +36 -0
  136. llms/extensions/providers/chutes.py +153 -0
  137. llms/extensions/providers/google.py +559 -0
  138. llms/extensions/providers/nvidia.py +103 -0
  139. llms/extensions/providers/openai.py +154 -0
  140. llms/extensions/providers/openrouter.py +74 -0
  141. llms/extensions/providers/zai.py +182 -0
  142. llms/extensions/skills/LICENSE +202 -0
  143. llms/extensions/skills/__init__.py +130 -0
  144. llms/extensions/skills/errors.py +25 -0
  145. llms/extensions/skills/models.py +39 -0
  146. llms/extensions/skills/parser.py +178 -0
  147. llms/extensions/skills/ui/index.mjs +376 -0
  148. llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
  149. llms/extensions/skills/validator.py +177 -0
  150. llms/extensions/system_prompts/README.md +22 -0
  151. llms/extensions/system_prompts/__init__.py +45 -0
  152. llms/extensions/system_prompts/ui/index.mjs +276 -0
  153. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  154. llms/extensions/tools/__init__.py +67 -0
  155. llms/extensions/tools/ui/index.mjs +837 -0
  156. llms/index.html +36 -62
  157. llms/llms.json +180 -879
  158. llms/main.py +4009 -912
  159. llms/providers-extra.json +394 -0
  160. llms/providers.json +1 -0
  161. llms/ui/App.mjs +176 -8
  162. llms/ui/ai.mjs +156 -20
  163. llms/ui/app.css +3768 -321
  164. llms/ui/ctx.mjs +459 -0
  165. llms/ui/index.mjs +131 -0
  166. llms/ui/lib/chart.js +14 -0
  167. llms/ui/lib/charts.mjs +16 -0
  168. llms/ui/lib/color.js +14 -0
  169. llms/ui/lib/highlight.min.mjs +1243 -0
  170. llms/ui/lib/idb.min.mjs +8 -0
  171. llms/ui/lib/marked.min.mjs +8 -0
  172. llms/ui/lib/servicestack-client.mjs +1 -0
  173. llms/ui/lib/servicestack-vue.mjs +37 -0
  174. llms/ui/lib/vue-router.min.mjs +6 -0
  175. llms/ui/lib/vue.min.mjs +13 -0
  176. llms/ui/lib/vue.mjs +18530 -0
  177. llms/ui/markdown.mjs +25 -14
  178. llms/ui/modules/chat/ChatBody.mjs +1156 -0
  179. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +74 -74
  180. llms/ui/modules/chat/index.mjs +995 -0
  181. llms/ui/modules/icons.mjs +46 -0
  182. llms/ui/modules/layout.mjs +271 -0
  183. llms/ui/modules/model-selector.mjs +811 -0
  184. llms/ui/tailwind.input.css +560 -78
  185. llms/ui/typography.css +54 -36
  186. llms/ui/utils.mjs +221 -92
  187. llms_py-3.0.18.dist-info/METADATA +49 -0
  188. llms_py-3.0.18.dist-info/RECORD +194 -0
  189. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
  190. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +1 -2
  191. llms/ui/Avatar.mjs +0 -28
  192. llms/ui/Brand.mjs +0 -34
  193. llms/ui/ChatPrompt.mjs +0 -443
  194. llms/ui/Main.mjs +0 -740
  195. llms/ui/ModelSelector.mjs +0 -60
  196. llms/ui/ProviderIcon.mjs +0 -29
  197. llms/ui/ProviderStatus.mjs +0 -105
  198. llms/ui/SignIn.mjs +0 -64
  199. llms/ui/SystemPromptEditor.mjs +0 -31
  200. llms/ui/SystemPromptSelector.mjs +0 -36
  201. llms/ui/Welcome.mjs +0 -8
  202. llms/ui/threadStore.mjs +0 -524
  203. llms/ui.json +0 -1069
  204. llms_py-2.0.20.dist-info/METADATA +0 -931
  205. llms_py-2.0.20.dist-info/RECORD +0 -36
  206. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
  207. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,599 @@
1
+ """
2
+ Core System Tools providing essential file operations, memory persistence, math expression evaluation, and code execution
3
+ """
4
+
5
+ import ast
6
+ import contextlib
7
+ import glob
8
+ import json
9
+ import math
10
+ import operator
11
+ import os
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ import tempfile
16
+ from datetime import datetime, timezone
17
+ from statistics import mean, median, stdev, variance
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ from aiohttp import web
21
+
22
+ g_ctx = None
23
+
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
+ # -----------------------------
184
+ # Expression evaluation tools
185
+ # -----------------------------
186
+
187
+
188
+ def get_calculator_functions():
189
+ # 2. Define allowed math functions and constants
190
+ allowed_functions = {
191
+ "mod": operator.mod,
192
+ "mean": mean,
193
+ "median": median,
194
+ "stdev": stdev,
195
+ "variance": variance,
196
+ "abs": abs,
197
+ "min": min,
198
+ "max": max,
199
+ "sum": sum,
200
+ "round": round,
201
+ }
202
+ allowed_functions.update(
203
+ {name: getattr(math, name) for name in dir(math) if not name.startswith("_") and name not in allowed_functions}
204
+ )
205
+ return allowed_functions
206
+
207
+
208
+ def calc(expression: str) -> str:
209
+ """Evaluate a mathematical expression with boolean operations"""
210
+ # 1. Define allowed operators
211
+ operators = {
212
+ ast.Add: operator.add,
213
+ ast.Sub: operator.sub,
214
+ ast.Mult: operator.mul,
215
+ ast.Div: operator.truediv,
216
+ ast.Pow: operator.pow,
217
+ ast.USub: operator.neg,
218
+ ast.Mod: operator.mod,
219
+ # Comparison operators
220
+ ast.Eq: operator.eq,
221
+ ast.NotEq: operator.ne,
222
+ ast.Lt: operator.lt,
223
+ ast.LtE: operator.le,
224
+ ast.Gt: operator.gt,
225
+ ast.GtE: operator.ge,
226
+ # Boolean operators
227
+ ast.And: operator.and_,
228
+ ast.Or: operator.or_,
229
+ ast.Not: operator.not_,
230
+ }
231
+
232
+ # 2. Define allowed math functions and constants
233
+ allowed_functions = get_calculator_functions()
234
+
235
+ def eval_node(node, context=None):
236
+ if context is None:
237
+ context = {}
238
+
239
+ if isinstance(node, ast.Constant): # Numbers and booleans
240
+ return node.value
241
+ elif isinstance(node, ast.BinOp): # Binary Ops (1 + 2)
242
+ return operators[type(node.op)](eval_node(node.left, context), eval_node(node.right, context))
243
+ elif isinstance(node, ast.UnaryOp): # Unary Ops (-5, not True)
244
+ return operators[type(node.op)](eval_node(node.operand, context))
245
+ elif isinstance(node, ast.Compare): # Comparison (5 > 3)
246
+ left = eval_node(node.left, context)
247
+ for op, comparator in zip(node.ops, node.comparators):
248
+ right = eval_node(comparator, context)
249
+ if not operators[type(op)](left, right):
250
+ return False
251
+ left = right
252
+ return True
253
+ elif isinstance(node, ast.BoolOp): # Boolean operations (True and False, True or False)
254
+ if isinstance(node.op, ast.And):
255
+ # Short-circuit evaluation for 'and'
256
+ result = True
257
+ for value in node.values:
258
+ result = eval_node(value, context)
259
+ if not result:
260
+ return False
261
+ return result
262
+ elif isinstance(node.op, ast.Or):
263
+ # Short-circuit evaluation for 'or'
264
+ for value in node.values:
265
+ result = eval_node(value, context)
266
+ if result:
267
+ return True
268
+ return False
269
+ elif isinstance(node, ast.Call): # Function calls (sqrt(16))
270
+ func_name = node.func.id
271
+ if func_name in allowed_functions:
272
+ args = [eval_node(arg, context) for arg in node.args]
273
+ return allowed_functions[func_name](*args)
274
+ if func_name == "range":
275
+ args = [eval_node(arg, context) for arg in node.args]
276
+ return range(*args)
277
+ raise NameError(f"Function '{func_name}' is not allowed.")
278
+ elif isinstance(node, ast.Name): # Constants (pi, e, True, False) or context variables
279
+ if node.id in context:
280
+ return context[node.id]
281
+ if node.id in allowed_functions:
282
+ return allowed_functions[node.id]
283
+ elif node.id in ("True", "False"):
284
+ return node.id == "True"
285
+ raise NameError(f"Variable '{node.id}' is not defined.")
286
+ elif isinstance(node, ast.List): # List literals [1, 2, 3]
287
+ return [eval_node(item, context) for item in node.elts]
288
+ elif isinstance(node, ast.ListComp): # List comprehensions [x*2 for x in [1,2,3]]
289
+ result = []
290
+ generators = node.generators
291
+ if len(generators) != 1:
292
+ raise ValueError("Only single-generator list comprehensions are supported")
293
+ gen = generators[0]
294
+ if not isinstance(gen.target, ast.Name):
295
+ raise ValueError("Only simple name targets in list comprehensions are supported")
296
+
297
+ target_name = gen.target.id
298
+ iterable = eval_node(gen.iter, context)
299
+
300
+ for item in iterable:
301
+ new_context = context.copy()
302
+ new_context[target_name] = item
303
+
304
+ # Check ifs
305
+ include = True
306
+ for if_node in gen.ifs:
307
+ if not eval_node(if_node, new_context):
308
+ include = False
309
+ break
310
+
311
+ if include:
312
+ result.append(eval_node(node.elt, new_context))
313
+ return result
314
+ else:
315
+ raise TypeError(f"Unsupported operation: {type(node).__name__}")
316
+
317
+ # Replace XOR with power
318
+ expression = expression.replace("^", "**")
319
+
320
+ # Parse and evaluate
321
+ node = ast.parse(expression, mode="eval").body
322
+ ret = eval_node(node)
323
+ g_ctx.dbg(f"calc ({expression}) = {ret}")
324
+ return ret
325
+
326
+
327
+ # -----------------------------
328
+ # code execution tools
329
+ # -----------------------------
330
+
331
+ mem_limit = 8589934592 # Max virtual memory 8GB
332
+ cpu_time_limit = 5 # Max CPU time 5 seconds
333
+ resource_limits = f"ulimit -t {cpu_time_limit}; ulimit -v {mem_limit};"
334
+
335
+
336
+ def run_python(code: str) -> Dict[str, Any]:
337
+ """
338
+ Execute Python code in a temporary sandboxed environment.
339
+ Uses ulimit for resource restriction and runs in a temporary directory.
340
+ """
341
+ with tempfile.TemporaryDirectory() as temp_dir:
342
+ script_path = os.path.join(temp_dir, "script.py")
343
+
344
+ with open(script_path, "w", encoding="utf-8") as f:
345
+ f.write(code)
346
+
347
+ cmd = f"{resource_limits} {sys.executable} script.py"
348
+
349
+ run_as = os.environ.get("LLMS_RUN_AS")
350
+ if run_as:
351
+ # Grant access to temp_dir
352
+ with contextlib.suppress(Exception):
353
+ os.chmod(temp_dir, 0o777)
354
+ cmd = f"sudo -u {run_as} bash -c '{cmd}'"
355
+
356
+ try:
357
+ # Run with restricted environment
358
+ # We keep PATH to find basic tools if needed, but remove sensitive vars
359
+ clean_env = {"PATH": os.environ.get("PATH", "")}
360
+
361
+ g_ctx.dbg(f"run_python ({temp_dir}): {cmd}\n{code}")
362
+ result = subprocess.run(
363
+ ["bash", "-c", cmd], cwd=temp_dir, env=clean_env, capture_output=True, text=True, timeout=10
364
+ )
365
+ return {"stdout": result.stdout, "stderr": result.stderr, "returncode": result.returncode}
366
+ except subprocess.TimeoutExpired:
367
+ return {"stdout": "", "stderr": "Execution timed out", "returncode": -1}
368
+ except Exception as e:
369
+ return {"stdout": "", "stderr": f"Error: {e}", "returncode": -1}
370
+
371
+
372
+ def run_javascript(code: str) -> Dict[str, Any]:
373
+ """
374
+ Execute JavaScript code in a temporary sandboxed environment using bun or node.
375
+ """
376
+ # Check for available runtime
377
+ runtime = shutil.which("bun") or shutil.which("node")
378
+ if not runtime:
379
+ return {"stdout": "", "stderr": "Error: Neither 'bun' nor 'node' is available on the system.", "returncode": -1}
380
+
381
+ with tempfile.TemporaryDirectory() as temp_dir:
382
+ script_path = os.path.join(temp_dir, "script.js")
383
+
384
+ with open(script_path, "w", encoding="utf-8") as f:
385
+ f.write(code)
386
+
387
+ cmd = f"{resource_limits} {runtime} script.js"
388
+
389
+ run_as = os.environ.get("LLMS_RUN_AS")
390
+ if run_as:
391
+ with contextlib.suppress(Exception):
392
+ os.chmod(temp_dir, 0o777)
393
+ cmd = f"sudo -u {run_as} bash -c '{cmd}'"
394
+
395
+ try:
396
+ # Run with restricted environment
397
+ clean_env = {"PATH": os.environ.get("PATH", "")}
398
+
399
+ g_ctx.dbg(f"run_javascript ({temp_dir}): {cmd}\n{code}")
400
+ result = subprocess.run(
401
+ ["bash", "-c", cmd], cwd=temp_dir, env=clean_env, capture_output=True, text=True, timeout=10
402
+ )
403
+ return {"stdout": result.stdout, "stderr": result.stderr, "returncode": result.returncode}
404
+ except subprocess.TimeoutExpired:
405
+ return {"stdout": "", "stderr": "Execution timed out", "returncode": -1}
406
+ except Exception as e:
407
+ return {"stdout": "", "stderr": f"Error: {e}", "returncode": -1}
408
+
409
+
410
+ def run_typescript(code: str) -> Dict[str, Any]:
411
+ """
412
+ Execute TypeScript code in a temporary sandboxed environment using bun or node.
413
+ """
414
+ # Check for available runtime
415
+ runtime = shutil.which("bun") or shutil.which("node")
416
+ if not runtime:
417
+ return {"stdout": "", "stderr": "Error: Neither 'bun' nor 'node' is available on the system.", "returncode": -1}
418
+
419
+ with tempfile.TemporaryDirectory() as temp_dir:
420
+ script_path = os.path.join(temp_dir, "script.ts")
421
+
422
+ with open(script_path, "w", encoding="utf-8") as f:
423
+ f.write(code)
424
+
425
+ cmd = f"{resource_limits} {runtime} script.ts"
426
+
427
+ run_as = os.environ.get("LLMS_RUN_AS")
428
+ if run_as:
429
+ with contextlib.suppress(Exception):
430
+ os.chmod(temp_dir, 0o777)
431
+ cmd = f"sudo -u {run_as} bash -c '{cmd}'"
432
+
433
+ try:
434
+ # Run with restricted environment
435
+ clean_env = {"PATH": os.environ.get("PATH", "")}
436
+
437
+ g_ctx.dbg(f"run_typescript ({temp_dir}): {cmd}\n{code}")
438
+ result = subprocess.run(
439
+ ["bash", "-c", cmd], cwd=temp_dir, env=clean_env, capture_output=True, text=True, timeout=10
440
+ )
441
+ return {"stdout": result.stdout, "stderr": result.stderr, "returncode": result.returncode}
442
+ except subprocess.TimeoutExpired:
443
+ return {"stdout": "", "stderr": "Execution timed out", "returncode": -1}
444
+ except Exception as e:
445
+ return {"stdout": "", "stderr": f"Error: {e}", "returncode": -1}
446
+
447
+
448
+ def run_csharp(code: str) -> Dict[str, Any]:
449
+ """
450
+ Execute C# code in a temporary sandboxed environment using dotnet.
451
+ """
452
+ # Check for available runtime
453
+ runtime = shutil.which("dotnet")
454
+ if not runtime:
455
+ return {"stdout": "", "stderr": "Error: 'dotnet' is not available on the system.", "returncode": -1}
456
+
457
+ with tempfile.TemporaryDirectory() as temp_dir:
458
+ script_path = os.path.join(temp_dir, "script.cs")
459
+
460
+ # Ensure we just have the code, user might pass it without wrapping class if it's top-level statements
461
+ with open(script_path, "w", encoding="utf-8") as f:
462
+ f.write(code)
463
+
464
+ # Note: 'dotnet run script.cs' is the command as per user request for .NET 10
465
+ cmd = f"{resource_limits} {runtime} run script.cs"
466
+
467
+ run_as = os.environ.get("LLMS_RUN_AS")
468
+ if run_as:
469
+ with contextlib.suppress(Exception):
470
+ os.chmod(temp_dir, 0o777)
471
+ # For dotnet, we need to set HOME and DOTNET_CLI_HOME to temp_dir for write access
472
+ cmd = f"sudo -u {run_as} env HOME={temp_dir} DOTNET_CLI_HOME={temp_dir} bash -c '{cmd}'"
473
+
474
+ try:
475
+ # Run with restricted environment
476
+ clean_env = {"PATH": os.environ.get("PATH", "")}
477
+
478
+ # Dotnet might need some ENV vars to work correctly, usually DOTNET_CLI_HOME or similar if strictly sandboxed
479
+ # But we are keeping PATH, hopefully commonly needed vars are there or default works.
480
+ # We might want to pass more env vars if it fails.
481
+ g_ctx.dbg(f"run_csharp ({temp_dir}): {cmd}\n{code}")
482
+ result = subprocess.run(
483
+ ["bash", "-c", cmd], cwd=temp_dir, env=clean_env, capture_output=True, text=True, timeout=10
484
+ )
485
+ return {"stdout": result.stdout, "stderr": result.stderr, "returncode": result.returncode}
486
+ except subprocess.TimeoutExpired:
487
+ return {"stdout": "", "stderr": "Execution timed out", "returncode": -1}
488
+ except Exception as e:
489
+ return {"stdout": "", "stderr": f"Error: {e}", "returncode": -1}
490
+
491
+
492
+ # -----------------------------
493
+ # Time tool
494
+ # -----------------------------
495
+
496
+
497
+ def get_current_time(tz_name: Optional[str] = None) -> str:
498
+ """
499
+ Get current time in ISO-8601 format.
500
+
501
+ Args:
502
+ tz_name: Optional timezone name (e.g. 'America/New_York'). Defaults to UTC.
503
+ """
504
+ if tz_name:
505
+ try:
506
+ try:
507
+ from zoneinfo import ZoneInfo
508
+ except ImportError:
509
+ from backports.zoneinfo import ZoneInfo
510
+
511
+ tz = ZoneInfo(tz_name)
512
+ except Exception:
513
+ return f"Error: Invalid timezone '{tz_name}'"
514
+ else:
515
+ tz = timezone.utc
516
+
517
+ return datetime.now(tz).isoformat()
518
+
519
+
520
+ def install(ctx):
521
+ global g_ctx
522
+ g_ctx = ctx
523
+ group = "core_tools"
524
+ # 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)
532
+ ctx.register_tool(calc, group=group)
533
+ ctx.register_tool(run_python, group=group)
534
+ ctx.register_tool(run_typescript, group=group)
535
+ ctx.register_tool(run_javascript, group=group)
536
+ ctx.register_tool(run_csharp, group=group)
537
+ ctx.register_tool(get_current_time, group=group)
538
+
539
+ def exec_language(language: str, code: str) -> Dict[str, Any]:
540
+ if language == "python":
541
+ return run_python(code)
542
+ elif language == "typescript":
543
+ return run_typescript(code)
544
+ elif language == "javascript":
545
+ return run_javascript(code)
546
+ elif language == "csharp":
547
+ return run_csharp(code)
548
+ else:
549
+ return {"stdout": "", "stderr": "Error: Invalid language", "returncode": -1}
550
+
551
+ async def run_code(request):
552
+ language = request.match_info["language"]
553
+ code = await request.text()
554
+ try:
555
+ result = exec_language(language, code)
556
+ except Exception as e:
557
+ result = {"stdout": "", "stderr": str(e), "returncode": -1}
558
+ return web.json_response(result)
559
+
560
+ ctx.add_post("code/{language}/run", run_code)
561
+
562
+ async def get_calculator_features(request):
563
+ operators = ["+", "-", "*", "/", "%", "^", "==", "!=", "<", "<=", ">", ">=", "and", "or", "not"]
564
+ operators = [f" {op} " for op in operators]
565
+ constants = ["pi", "e", "inf", "tau", "nan"]
566
+ functions = [f for f in get_calculator_functions() if f not in constants]
567
+ return web.json_response(
568
+ {
569
+ "numbers": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
570
+ "constants": constants,
571
+ "operators": operators,
572
+ "functions": sorted(functions),
573
+ }
574
+ )
575
+
576
+ ctx.add_get("calc", get_calculator_features)
577
+
578
+ async def run_calc(request):
579
+ code = await request.text()
580
+ result = calc(code)
581
+ return web.json_response({"result": result})
582
+
583
+ ctx.add_post("calc", run_calc)
584
+
585
+ ctx.add_index_footer(
586
+ f"""
587
+ <link rel="stylesheet" href="{ctx.ext_prefix}/codemirror/codemirror.css">
588
+ <link rel="stylesheet" href="{ctx.ext_prefix}/codemirror/theme/mocha.css">
589
+ <script src="{ctx.ext_prefix}/codemirror/codemirror.js"></script>
590
+ <script src="{ctx.ext_prefix}/codemirror/mode/clike/clike.js"></script>
591
+ <script src="{ctx.ext_prefix}/codemirror/mode/javascript/javascript.js"></script>
592
+ <script src="{ctx.ext_prefix}/codemirror/mode/python/python.js"></script>
593
+ <script src="{ctx.ext_prefix}/codemirror/addon/edit/matchbrackets.js"></script>
594
+ <script src="{ctx.ext_prefix}/codemirror/addon/selection/active-line.js"></script>
595
+ """
596
+ )
597
+
598
+
599
+ __install__ = install