nima-phi-model / nima_agent_layer.py
TheNormsOfIntelligence's picture
Initial release — Nima Phi: Consciousness + Embodiment + The Green Lines
61848b4 verified
Raw
History Blame Contribute Delete
25 kB
#!/usr/bin/env python3
"""
nima_agent_layer.py — OpenClaw-style self-authoring agent layer.
Nima can create, use, and compose tools. When she doesn't have a tool
she needs, she writes one, tests it in a sandbox, and saves it to her
toolbox for later.
NEUROBIOLOGICAL MAPPING:
ToolRegistry = basal ganglia + cerebellum (procedural memory)
ToolExecutor = primary motor cortex + spinal cord (action execution)
ToolPlanner = prefrontal cortex (executive planning)
ToolCreator = prefrontal ↔ motor integration (human-unique tool-making)
ToolCombiner = right hemisphere + PFC (creative synthesis)
ToolSandbox = cerebellum (safe practice space — test before using)
ENVIRONMENT VARIABLES:
NIMA_TOOLS_DIR # where tool files live (default: ./tools/)
NIMA_SANDBOX_DIR # where sandbox execution happens (default: ./sandbox/)
NIMA_TOOL_AUTO_APPROVE # 1 = new tools don't need human approval
NIMA_TOOL_TIMEOUT_S # execution timeout (default: 30)
NIMA_TOOL_MEMORY_MB # memory limit (default: 256)
"""
from __future__ import annotations
import ast
import json
import logging
import os
import subprocess
import sys
import tempfile
import time
import uuid
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger("NimaAgent")
FORBIDDEN_MODULES = {
"os.system", "subprocess.Popen", "subprocess.call", "subprocess.run",
"os.popen", "commands.getoutput",
}
RESTRICTED_MODULES = {
"eval", "exec", "compile",
"os.remove", "os.rmdir", "os.unlink", "shutil.rmtree",
"socket", "http.client", "urllib.request", "requests",
"ctypes", "cffi",
}
ALLOWED_MODULES = {
"math", "statistics", "random", "itertools", "collections",
"json", "re", "string", "textwrap", "unicodedata",
"datetime", "calendar", "hashlib", "base64",
"fractions", "decimal", "numbers",
"typing", "dataclasses", "enum",
"functools", "operator", "pathlib",
"csv", "io", "tempfile",
}
@dataclass
class ToolMetadata:
tool_id: str
name: str
description: str
function_signature: str
file_path: str
created_at: float = field(default_factory=time.time)
created_by: str = "nima"
use_count: int = 0
last_used: float = 0.0
approved: bool = False
source_tools: List[str] = field(default_factory=list)
tags: List[str] = field(default_factory=list)
test_passed: bool = False
def to_dict(self) -> Dict[str, Any]:
return {k: v for k, v in self.__dict__.items()}
class ToolRegistry:
"""JSON-indexed library of tool files — Nima's procedural memory."""
def __init__(self, tools_dir: Optional[str] = None) -> None:
if tools_dir is None:
tools_dir = os.environ.get("NIMA_TOOLS_DIR", os.path.join(os.getcwd(), "tools"))
self.tools_dir = tools_dir
self._tools: Dict[str, ToolMetadata] = {}
self._lock = __import__("threading").Lock()
os.makedirs(self.tools_dir, exist_ok=True)
self._load()
def _registry_path(self) -> str:
return os.path.join(self.tools_dir, "_registry.json")
def _load(self) -> None:
path = self._registry_path()
if not os.path.exists(path):
return
try:
with open(path, "r") as f:
data = json.load(f)
for tid, md in data.items():
self._tools[tid] = ToolMetadata(**md)
logger.info("[ToolRegistry] loaded %d tools", len(self._tools))
except Exception as e:
logger.warning("[ToolRegistry] load failed: %s", e)
def _save(self) -> None:
path = self._registry_path()
try:
tmp = path + ".tmp"
with open(tmp, "w") as f:
json.dump({tid: t.to_dict() for tid, t in self._tools.items()},
f, indent=2, default=str)
os.replace(tmp, path)
except Exception as e:
logger.warning("[ToolRegistry] save failed: %s", e)
def register(self, metadata: ToolMetadata) -> None:
with self._lock:
self._tools[metadata.tool_id] = metadata
self._save()
def get(self, tool_id: str) -> Optional[ToolMetadata]:
return self._tools.get(tool_id)
def find_by_name(self, name: str) -> Optional[ToolMetadata]:
for t in self._tools.values():
if t.name == name:
return t
return None
def find_by_capability(self, description_query: str) -> List[ToolMetadata]:
query_lower = description_query.lower()
query_words = set(query_lower.split())
results = []
for t in self._tools.values():
desc_lower = t.description.lower()
name_lower = t.name.lower()
tags_lower = [tag.lower() for tag in t.tags]
all_words = set()
all_words.update(desc_lower.split())
all_words.update(name_lower.replace("_", " ").split())
for tag in tags_lower:
all_words.update(tag.split())
overlap = len(query_words & all_words)
for qw in query_words:
if len(qw) < 3:
continue
if qw in name_lower or qw in desc_lower:
overlap += 1
continue
for tag in tags_lower:
if qw in tag:
overlap += 1
break
else:
for word in all_words:
if word.startswith(qw) and len(word) > len(qw):
overlap += 1
break
if overlap > 0:
results.append((t, overlap))
results.sort(key=lambda x: -x[1])
return [t for t, _ in results[:10]]
def list_all(self) -> List[ToolMetadata]:
return list(self._tools.values())
def list_approved(self) -> List[ToolMetadata]:
return [t for t in self._tools.values() if t.approved]
def approve(self, tool_id: str) -> bool:
with self._lock:
t = self._tools.get(tool_id)
if t is None:
return False
t.approved = True
self._save()
return True
def record_use(self, tool_id: str) -> None:
with self._lock:
t = self._tools.get(tool_id)
if t is None:
return
t.use_count += 1
t.last_used = time.time()
self._save()
def get_stats(self) -> Dict[str, Any]:
return {
"total_tools": len(self._tools),
"approved_tools": sum(1 for t in self._tools.values() if t.approved),
"self_authored": sum(1 for t in self._tools.values() if t.created_by == "nima"),
"total_uses": sum(t.use_count for t in self._tools.values()),
}
class ToolSandbox:
"""Safe execution environment for tool code."""
def __init__(self, sandbox_dir: Optional[str] = None) -> None:
if sandbox_dir is None:
sandbox_dir = os.environ.get("NIMA_SANDBOX_DIR", os.path.join(os.getcwd(), "sandbox"))
self.sandbox_dir = sandbox_dir
os.makedirs(self.sandbox_dir, exist_ok=True)
self.timeout_s = float(os.environ.get("NIMA_TOOL_TIMEOUT_S", "30"))
self.memory_limit_mb = float(os.environ.get("NIMA_TOOL_MEMORY_MB", "256"))
def scan_code(self, code: str) -> Tuple[bool, List[str]]:
issues: List[str] = []
try:
tree = ast.parse(code)
except SyntaxError as e:
return False, [f"SyntaxError: {e}"]
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
mod = alias.name
if mod in FORBIDDEN_MODULES:
issues.append(f"Forbidden import: {mod}")
elif mod in RESTRICTED_MODULES:
issues.append(f"Restricted import (needs approval): {mod}")
elif isinstance(node, ast.ImportFrom):
mod = node.module or ""
if mod in FORBIDDEN_MODULES:
issues.append(f"Forbidden import: {mod}")
elif mod in RESTRICTED_MODULES:
issues.append(f"Restricted import (needs approval): {mod}")
blocked = [i for i in issues if "Forbidden" in i or "Restricted" in i]
return len(blocked) == 0, issues
def _build_runner_code(self, tool_file: str, function_name: str) -> str:
"""Build the runner script that executes a tool in a subprocess.
Includes memory limiting via resource.setrlimit where supported.
"""
memory_bytes = int(self.memory_limit_mb * 1024 * 1024)
runner_code = (
f"import sys, json, importlib.util, resource\n"
f"# Enforce memory limit (Linux/macOS only)\n"
f"try:\n"
f" resource.setrlimit(resource.RLIMIT_AS, ({memory_bytes}, {memory_bytes}))\n"
f"except (ValueError, AttributeError):\n"
f" pass # not available on this platform\n"
f"spec = importlib.util.spec_from_file_location('tool', {tool_file!r})\n"
f"mod = importlib.util.module_from_spec(spec)\n"
f"spec.loader.exec_module(mod)\n"
f"fn = getattr(mod, {function_name!r})\n"
f"args = json.loads(sys.argv[1])\n"
f"kwargs = json.loads(sys.argv[2])\n"
f"try:\n"
f" result = fn(*args, **kwargs)\n"
f" print('__RESULT__' + json.dumps({{'result': result}}, default=str))\n"
f"except Exception as e:\n"
f" print('__ERROR__' + json.dumps({{'error': str(e)}}))\n"
)
return runner_code
def execute(self, code: str, function_name: str,
args: List[Any] = None, kwargs: Dict[str, Any] = None,
) -> Dict[str, Any]:
args = args or []
kwargs = kwargs or {}
t0 = time.time()
passes, issues = self.scan_code(code)
blocked = [i for i in issues if "Forbidden" in i or "Restricted" in i]
if blocked:
return {"success": False, "result": None, "stderr": "Blocked by safety scan",
"execution_time_ms": 0, "issues": blocked}
tool_file = os.path.join(self.sandbox_dir, f"_tool_{uuid.uuid4().hex[:8]}.py")
runner_file = os.path.join(self.sandbox_dir, f"_runner_{uuid.uuid4().hex[:8]}.py")
try:
with open(tool_file, "w") as f:
f.write(code)
runner_code = self._build_runner_code(tool_file, function_name)
with open(runner_file, "w") as f:
f.write(runner_code)
proc = subprocess.run(
[sys.executable, runner_file, json.dumps(args), json.dumps(kwargs)],
capture_output=True, text=True, timeout=self.timeout_s, cwd=self.sandbox_dir,
)
elapsed_ms = (time.time() - t0) * 1000
stdout, stderr = proc.stdout, proc.stderr
result = None
success = False
for marker in ("__RESULT__", "__ERROR__"):
if marker in stdout:
try:
payload = json.loads(stdout.split(marker)[-1].strip())
if marker == "__RESULT__":
result = payload.get("result")
success = True
else:
stderr = payload.get("error", stderr)
except Exception:
pass
break
return {"success": success, "result": result,
"stdout": stdout[:2000], "stderr": stderr[:2000],
"execution_time_ms": round(elapsed_ms, 1), "issues": issues}
except subprocess.TimeoutExpired:
return {"success": False, "result": None, "stdout": "",
"stderr": f"Timeout after {self.timeout_s}s",
"execution_time_ms": round((time.time() - t0) * 1000, 1), "issues": issues}
except Exception as e:
return {"success": False, "result": None, "stdout": "",
"stderr": str(e), "execution_time_ms": 0, "issues": issues}
finally:
for p in (tool_file, runner_file):
try:
os.remove(p)
except Exception:
pass
class ToolExecutor:
"""Executes approved tools from the registry."""
def __init__(self, registry: ToolRegistry, sandbox: ToolSandbox) -> None:
self.registry = registry
self.sandbox = sandbox
def execute(self, tool_id: str, args: List[Any] = None,
kwargs: Dict[str, Any] = None) -> Dict[str, Any]:
tool = self.registry.get(tool_id)
if tool is None:
return {"success": False, "error": f"Tool not found: {tool_id}"}
if not tool.approved:
return {"success": False, "error": f"Tool not approved: {tool.name}"}
tool_path = os.path.join(self.registry.tools_dir, tool.file_path)
if not os.path.exists(tool_path):
return {"success": False, "error": f"Tool file missing: {tool_path}"}
with open(tool_path, "r") as f:
code = f.read()
result = self.sandbox.execute(code, tool.name, args, kwargs)
if result["success"]:
self.registry.record_use(tool_id)
return result
class ToolPlanner:
"""Decides which tool to call for a given task."""
def __init__(self, registry: ToolRegistry) -> None:
self.registry = registry
MAX_PLAN_TOOLS = 3 # maximum tools to return per plan
def plan(self, task_description: str) -> Dict[str, Any]:
candidates = self.registry.find_by_capability(task_description)
approved = [t for t in candidates if t.approved]
if not approved:
return {"needs_tool": False, "tools": [],
"reasoning": "No approved tools match."}
top_n = approved[:self.MAX_PLAN_TOOLS]
return {"needs_tool": True, "tools": [{
"tool_id": t.tool_id, "name": t.name,
"args": [], "kwargs": {},
"reason": f"Candidate for: {task_description[:80]}",
} for t in top_n], "reasoning": f"Selected {len(top_n)} tool(s): {', '.join(t.name for t in top_n)}"}
class ToolCreator:
"""When Nima doesn't have a tool she needs, she writes one."""
TOOL_TEMPLATE = '''"""Tool: {name} — {description}"""
import math, json, re
from typing import Any, Dict, List, Optional
def {function_name}({signature}):
"""{description}"""
# TODO: Nima will fill this in via LLM
pass
'''
def __init__(self, registry: ToolRegistry, sandbox: ToolSandbox,
llm_provider: Optional[Any] = None) -> None:
self.registry = registry
self.sandbox = sandbox
self.llm_provider = llm_provider
self.auto_approve = bool(
os.environ.get("NIMA_TOOL_AUTO_APPROVE", "0") in ("1", "true", "True"))
def create_tool(self, name: str, description: str, function_signature: str,
test_cases: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
code = self._generate_code(name, description, function_signature, test_cases)
test_results = []
all_passed = True
if test_cases:
for i, tc in enumerate(test_cases):
result = self.sandbox.execute(code, name, tc.get("args", []), tc.get("kwargs", {}))
passed = result["success"]
if passed and "expected" in tc:
passed = result["result"] == tc["expected"]
test_results.append({"case": i, "passed": passed,
"result": result.get("result"),
"expected": tc.get("expected")})
if not passed:
all_passed = False
else:
passes_scan, issues = self.sandbox.scan_code(code)
all_passed = passes_scan
test_results = [{"case": 0, "passed": passes_scan, "issues": issues}]
approved = all_passed and self.auto_approve
tool_id = f"tool_{uuid.uuid4().hex[:8]}"
file_name = f"{name}.py"
file_path = os.path.join(self.registry.tools_dir, file_name)
with open(file_path, "w") as f:
f.write(code)
metadata = ToolMetadata(
tool_id=tool_id, name=name, description=description,
function_signature=f"{name}({function_signature})", file_path=file_name,
created_by="nima", approved=approved, test_passed=all_passed,
tags=self._extract_tags(description),
)
self.registry.register(metadata)
return {"success": True, "tool_id": tool_id, "approved": approved,
"test_results": test_results}
def _generate_code(self, name, description, signature, test_cases=None) -> str:
if self.llm_provider and getattr(self.llm_provider, "available", False):
try:
system_prompt = ("You are Nima's tool-creation subsystem. Write a single "
"Python function. Use only safe stdlib modules. Output ONLY Python code.")
user_prompt = f"Tool: {name}\nDescription: {description}\nSignature: def {name}({signature}):"
code = self.llm_provider.generate(
system_prompt=system_prompt,
messages=[{"role": "user", "content": user_prompt}],
temperature=0.2, max_tokens=1024,
)
if code.startswith("```"):
lines = code.split("\n")[1:]
if lines and lines[-1].strip() == "```":
lines = lines[:-1]
code = "\n".join(lines)
return code
except Exception:
pass
return self.TOOL_TEMPLATE.format(
name=name, description=description, function_name=name, signature=signature,
)
def _extract_tags(self, description: str) -> List[str]:
tags = []
desc_lower = description.lower()
if any(w in desc_lower for w in ["search", "find", "lookup"]):
tags.append("search")
if any(w in desc_lower for w in ["file", "read", "write"]):
tags.append("file")
if any(w in desc_lower for w in ["math", "calculate", "compute", "convert"]):
tags.append("math")
if any(w in desc_lower for w in ["web", "url"]):
tags.append("web")
if any(w in desc_lower for w in ["time", "date"]):
tags.append("time")
return tags or ["general"]
class ToolCombiner:
"""Composes existing tools into composite tools."""
def __init__(self, registry: ToolRegistry, creator: ToolCreator) -> None:
self.registry = registry
self.creator = creator
def combine(self, name: str, description: str,
source_tool_ids: List[str], combination_logic: str) -> Dict[str, Any]:
source_tools = []
for tid in source_tool_ids:
t = self.registry.get(tid)
if t is None:
return {"success": False, "error": f"Source tool not found: {tid}"}
source_tools.append(t)
full_desc = f"{description}\nCombines: {', '.join(t.name for t in source_tools)}\nLogic: {combination_logic}"
result = self.creator.create_tool(name=name, description=full_desc,
function_signature="*args, **kwargs")
if result.get("success"):
tool = self.registry.get(result["tool_id"])
if tool:
tool.source_tools = source_tool_ids
tool.tags.append("composite")
self.registry.register(tool)
return result
class AgentLayer:
"""Facade that ties together the full agent layer."""
def __init__(self, tools_dir: Optional[str] = None,
sandbox_dir: Optional[str] = None,
llm_provider: Optional[Any] = None) -> None:
self.registry = ToolRegistry(tools_dir)
self.sandbox = ToolSandbox(sandbox_dir)
self.executor = ToolExecutor(self.registry, self.sandbox)
self.planner = ToolPlanner(self.registry)
self.creator = ToolCreator(self.registry, self.sandbox, llm_provider)
self.combiner = ToolCombiner(self.registry, self.creator)
def register_starter_tools(self) -> None:
starter_code = {
"calculator": '''"""Tool: calculator — Evaluate a mathematical expression safely."""
import ast, operator as op
_OPS = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv,
ast.Pow: op.pow, ast.Mod: op.mod, ast.USub: op.neg, ast.UAdd: op.pos, ast.FloorDiv: op.floordiv}
def calculator(expression: str):
"""Evaluate a mathematical expression. Returns float."""
def _eval(node):
if isinstance(node, ast.Num): return node.n
if isinstance(node, ast.Constant): return node.value
if isinstance(node, ast.BinOp): return _OPS[type(node.op)](_eval(node.left), _eval(node.right))
if isinstance(node, ast.UnaryOp): return _OPS[type(node.op)](_eval(node.operand))
raise ValueError(f"Unsupported: {type(node).__name__}")
tree = ast.parse(expression, mode="eval")
return float(_eval(tree.body))
''',
"text_stats": '''"""Tool: text_stats — Compute statistics about text."""
import re
from collections import Counter
def text_stats(text: str):
"""Compute word count, char count, sentence count, avg word length, top words."""
words = re.findall(r"\\b\\w+\\b", text.lower())
sentences = [s for s in re.split(r"[.!?]+", text) if s.strip()]
word_lens = [len(w) for w in words]
return {"word_count": len(words), "char_count": len(text),
"sentence_count": len(sentences),
"avg_word_length": sum(word_lens)/max(1,len(word_lens)),
"top_words": Counter(words).most_common(5)}
''',
"time_now": '''"""Tool: time_now — Get the current time and date."""
import time
from datetime import datetime
def time_now():
"""Get current time and date. Returns dict with iso, date, time, weekday."""
now = datetime.now()
return {"iso": now.isoformat(), "unix": time.time(),
"date": now.strftime("%Y-%m-%d"), "time": now.strftime("%H:%M:%S"),
"weekday": now.strftime("%A")}
''',
}
for name, code in starter_code.items():
if self.registry.find_by_name(name) is not None:
continue
file_path = os.path.join(self.registry.tools_dir, f"{name}.py")
with open(file_path, "w") as f:
f.write(code)
result = self.sandbox.execute(code, name, [], {})
desc = code.split("—")[1].split('"""')[0].strip() if "—" in code else name
metadata = ToolMetadata(
tool_id=f"tool_{uuid.uuid4().hex[:8]}", name=name,
description=desc, function_signature=f"{name}()", file_path=f"{name}.py",
created_by="human", approved=True, test_passed=result["success"],
tags=["built-in", name],
)
self.registry.register(metadata)
def get_stats(self) -> Dict[str, Any]:
return {"registry": self.registry.get_stats(),
"sandbox_timeout_s": self.sandbox.timeout_s,
"auto_approve": self.creator.auto_approve}
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
import tempfile
tmp_tools = tempfile.mkdtemp(prefix="nima_tools_test_")
tmp_sandbox = tempfile.mkdtemp(prefix="nima_sandbox_test_")
os.environ["NIMA_TOOLS_DIR"] = tmp_tools
os.environ["NIMA_SANDBOX_DIR"] = tmp_sandbox
os.environ["NIMA_TOOL_AUTO_APPROVE"] = "1"
agent = AgentLayer()
agent.register_starter_tools()
calc = agent.registry.find_by_name("calculator")
if calc:
r = agent.executor.execute(calc.tool_id, args=["2 + 3 * 4"])
print(f"Calculator: 2 + 3 * 4 = {r.get('result')} (expected 14.0)")
ts = agent.registry.find_by_name("text_stats")
if ts:
r = agent.executor.execute(ts.tool_id, args=["Hello world. Testing."])
print(f"Text stats: {r.get('result')}")
tn = agent.registry.find_by_name("time_now")
if tn:
r = agent.executor.execute(tn.tool_id, args=[])
print(f"Time: {r.get('result', {}).get('iso', '?')}")
print(f"Stats: {json.dumps(agent.get_stats(), indent=2)}")
import shutil
shutil.rmtree(tmp_tools, ignore_errors=True)
shutil.rmtree(tmp_sandbox, ignore_errors=True)
print("\nAgent layer test PASSED")