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