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.
- llms/__init__.py +3 -1
- llms/db.py +359 -0
- llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +254 -327
- llms/extensions/app/README.md +20 -0
- llms/extensions/app/__init__.py +588 -0
- llms/extensions/app/db.py +540 -0
- llms/{ui → extensions/app/ui}/Recents.mjs +99 -73
- llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +139 -68
- llms/extensions/app/ui/threadStore.mjs +440 -0
- llms/extensions/computer/README.md +96 -0
- llms/extensions/computer/__init__.py +59 -0
- llms/extensions/computer/base.py +80 -0
- llms/extensions/computer/bash.py +185 -0
- llms/extensions/computer/computer.py +523 -0
- llms/extensions/computer/edit.py +299 -0
- llms/extensions/computer/filesystem.py +542 -0
- llms/extensions/computer/platform.py +461 -0
- llms/extensions/computer/run.py +37 -0
- llms/extensions/core_tools/CALCULATOR.md +32 -0
- llms/extensions/core_tools/__init__.py +599 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
- llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
- llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
- llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
- llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
- llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
- llms/extensions/core_tools/ui/index.mjs +650 -0
- llms/extensions/gallery/README.md +61 -0
- llms/extensions/gallery/__init__.py +63 -0
- llms/extensions/gallery/db.py +243 -0
- llms/extensions/gallery/ui/index.mjs +482 -0
- llms/extensions/katex/README.md +39 -0
- llms/extensions/katex/__init__.py +6 -0
- llms/extensions/katex/ui/README.md +125 -0
- llms/extensions/katex/ui/contrib/auto-render.js +338 -0
- llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
- llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
- llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
- llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
- llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
- llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
- llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
- llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- llms/extensions/katex/ui/index.mjs +92 -0
- llms/extensions/katex/ui/katex-swap.css +1230 -0
- llms/extensions/katex/ui/katex-swap.min.css +1 -0
- llms/extensions/katex/ui/katex.css +1230 -0
- llms/extensions/katex/ui/katex.js +19080 -0
- llms/extensions/katex/ui/katex.min.css +1 -0
- llms/extensions/katex/ui/katex.min.js +1 -0
- llms/extensions/katex/ui/katex.min.mjs +1 -0
- llms/extensions/katex/ui/katex.mjs +18547 -0
- llms/extensions/providers/__init__.py +22 -0
- llms/extensions/providers/anthropic.py +260 -0
- llms/extensions/providers/cerebras.py +36 -0
- llms/extensions/providers/chutes.py +153 -0
- llms/extensions/providers/google.py +559 -0
- llms/extensions/providers/nvidia.py +103 -0
- llms/extensions/providers/openai.py +154 -0
- llms/extensions/providers/openrouter.py +74 -0
- llms/extensions/providers/zai.py +182 -0
- llms/extensions/skills/LICENSE +202 -0
- llms/extensions/skills/__init__.py +130 -0
- llms/extensions/skills/errors.py +25 -0
- llms/extensions/skills/models.py +39 -0
- llms/extensions/skills/parser.py +178 -0
- llms/extensions/skills/ui/index.mjs +376 -0
- llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
- llms/extensions/skills/validator.py +177 -0
- llms/extensions/system_prompts/README.md +22 -0
- llms/extensions/system_prompts/__init__.py +45 -0
- llms/extensions/system_prompts/ui/index.mjs +276 -0
- llms/extensions/system_prompts/ui/prompts.json +1067 -0
- llms/extensions/tools/__init__.py +67 -0
- llms/extensions/tools/ui/index.mjs +837 -0
- llms/index.html +36 -62
- llms/llms.json +180 -879
- llms/main.py +4009 -912
- llms/providers-extra.json +394 -0
- llms/providers.json +1 -0
- llms/ui/App.mjs +176 -8
- llms/ui/ai.mjs +156 -20
- llms/ui/app.css +3768 -321
- llms/ui/ctx.mjs +459 -0
- llms/ui/index.mjs +131 -0
- llms/ui/lib/chart.js +14 -0
- llms/ui/lib/charts.mjs +16 -0
- llms/ui/lib/color.js +14 -0
- llms/ui/lib/highlight.min.mjs +1243 -0
- llms/ui/lib/idb.min.mjs +8 -0
- llms/ui/lib/marked.min.mjs +8 -0
- llms/ui/lib/servicestack-client.mjs +1 -0
- llms/ui/lib/servicestack-vue.mjs +37 -0
- llms/ui/lib/vue-router.min.mjs +6 -0
- llms/ui/lib/vue.min.mjs +13 -0
- llms/ui/lib/vue.mjs +18530 -0
- llms/ui/markdown.mjs +25 -14
- llms/ui/modules/chat/ChatBody.mjs +1156 -0
- llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +74 -74
- llms/ui/modules/chat/index.mjs +995 -0
- llms/ui/modules/icons.mjs +46 -0
- llms/ui/modules/layout.mjs +271 -0
- llms/ui/modules/model-selector.mjs +811 -0
- llms/ui/tailwind.input.css +560 -78
- llms/ui/typography.css +54 -36
- llms/ui/utils.mjs +221 -92
- llms_py-3.0.18.dist-info/METADATA +49 -0
- llms_py-3.0.18.dist-info/RECORD +194 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +1 -2
- llms/ui/Avatar.mjs +0 -28
- llms/ui/Brand.mjs +0 -34
- llms/ui/ChatPrompt.mjs +0 -443
- llms/ui/Main.mjs +0 -740
- llms/ui/ModelSelector.mjs +0 -60
- llms/ui/ProviderIcon.mjs +0 -29
- llms/ui/ProviderStatus.mjs +0 -105
- llms/ui/SignIn.mjs +0 -64
- llms/ui/SystemPromptEditor.mjs +0 -31
- llms/ui/SystemPromptSelector.mjs +0 -36
- llms/ui/Welcome.mjs +0 -8
- llms/ui/threadStore.mjs +0 -524
- llms/ui.json +0 -1069
- llms_py-2.0.20.dist-info/METADATA +0 -931
- llms_py-2.0.20.dist-info/RECORD +0 -36
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
- {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
|