"""
clankerDiffusion — local tool executor for the agent loop.
Tools the model can call via args :
calc(expr) safe arithmetic / math expression
python(code) run Python, stdout captured (threaded timeout)
read_file(p) read a text file
list_dir(p) list a directory
retrieve(q) RAG search over the local knowledge base (returns passages)
NOTE: python() executes locally on YOUR machine. It is sandboxed with a
restricted builtins set and a wall-clock timeout, but treat it as you would
any local code. Don't point clanker at untrusted input.
"""
import ast, math, io, contextlib, os, time, threading, builtins
from rag import retrieve as _rag_retrieve
SAFE_NAMES = {k: getattr(math, k) for k in dir(math) if not k.startswith("_")}
SAFE_NAMES.update({"abs": abs, "min": min, "max": max, "round": round,
"pow": pow, "len": len, "sum": sum, "sorted": sorted,
"int": int, "float": float, "str": str, "list": list,
"range": range, "True": True, "False": False, "None": None})
def _safe_eval(expr):
node = ast.parse(expr, mode="eval").body
def ev(n):
if isinstance(n, ast.BinOp):
a, b = ev(n.left), ev(n.right)
if isinstance(n.op, ast.Add): return a + b
if isinstance(n.op, ast.Sub): return a - b
if isinstance(n.op, ast.Mult): return a * b
if isinstance(n.op, ast.Div): return a / b
if isinstance(n.op, ast.FloorDiv): return a // b
if isinstance(n.op, ast.Mod): return a % b
if isinstance(n.op, ast.Pow): return a ** b
raise ValueError("op")
if isinstance(n, ast.UnaryOp):
v = ev(n.operand)
return -v if isinstance(n.op, ast.USub) else +v
if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)):
return n.value
if isinstance(n, ast.Call) and isinstance(n.func, ast.Name):
fn = SAFE_NAMES.get(n.func.id)
if fn is None: raise ValueError(f"no {n.func.id}")
return fn(*[ev(a) for a in n.args])
if isinstance(n, ast.Name):
if n.id in SAFE_NAMES: return SAFE_NAMES[n.id]
raise ValueError(f"name {n.id}")
raise ValueError("bad expr")
return ev(node)
def _run_python(code, timeout=10.0):
ns = {"__builtins__": {k: getattr(builtins, k)
for k in ("print", "len", "range", "list", "dict",
"tuple", "set", "str", "int", "float",
"bool", "min", "max", "sum", "sorted",
"abs", "enumerate", "zip", "map", "filter",
"open", "round", "pow") if hasattr(builtins, k)}}
ns["math"] = math
buf = io.StringIO()
result = {}
def target():
try:
with contextlib.redirect_stdout(buf):
exec(code, ns)
result["out"] = buf.getvalue()
except Exception as e:
result["out"] = buf.getvalue() + f"\nERROR: {type(e).__name__}: {e}"
t = threading.Thread(target=target, daemon=True)
t.start(); t.join(timeout)
if t.is_alive():
return f"ERROR: execution timed out after {timeout}s"
return result.get("out", "").strip() or "(no output)"
def execute_tool(name, arg):
try:
if name == "calc":
val = _safe_eval(arg.strip())
return f"{arg.strip()} = {val}"
if name == "python":
return _run_python(arg)
if name == "read_file":
p = arg.strip().strip('"\'')
if not os.path.exists(p): return f"ERROR: no such file: {p}"
with open(p, "r", errors="replace") as f:
return f.read()[:4000]
if name == "list_dir":
p = arg.strip().strip('"\'') or "."
if not os.path.isdir(p): return f"ERROR: no such dir: {p}"
return "\n".join(sorted(os.listdir(p))[:200])
if name == "retrieve":
try:
passages = _rag_retrieve(arg.strip(), k=4)
if not passages:
return "(no relevant context found in the knowledge base)"
return "\n---\n".join(passages)
except Exception as e:
return f"ERROR: retrieve failed: {e}"
return f"ERROR: unknown tool '{name}'"
except Exception as e:
return f"ERROR: {type(e).__name__}: {e}"
def parse_tool_calls(text):
"""Return list of (name, argstring) from .. blocks."""
calls = []
i = 0
while True:
s = text.find("", s)
if e < 0: break
head = text[s:text.find(">", s) + 1]
# head like:
import re
m = re.search(r'name="([^"]+)"', head)
name = m.group(1) if m else "?"
arg = text[s + len(head):e].strip()
calls.append((name, arg))
i = e + len("")
return calls