import json import os from typing import Optional, Dict, Any, List from agentpress.tool import ToolResult, openapi_schema, usage_example from agentpress.thread_manager import ThreadManager from sandbox.tool_base import SandboxToolsBase from utils.logger import logger class SandboxTemplatesTool(SandboxToolsBase): """Tool to discover and scaffold web project templates.""" def __init__(self, project_id: str, thread_manager: ThreadManager): super().__init__(project_id, thread_manager) self.workspace_path = "/workspace" self.local_templates_dir = "/opt/templates" self.local_manifest = f"{self.local_templates_dir}/manifest.json" self.remote_manifest_url = os.getenv("TEMPLATES_MANIFEST_URL") self.remote_archive_base = os.getenv("TEMPLATES_ARCHIVE_BASE_URL") # e.g. https://example.com/templates/{name}.zip async def _read_local_manifest(self) -> Optional[Dict[str, Any]]: await self._ensure_sandbox() try: check = await self.sandbox.process.exec(f"/bin/sh -c 'test -f {self.local_manifest} && echo OK || echo NO'", timeout=10) if "OK" not in (getattr(check, "result", "") or ""): return None content = await self.sandbox.fs.download_file(self.local_manifest) return json.loads(content.decode()) except Exception: return None async def _fetch_remote_manifest(self) -> Optional[Dict[str, Any]]: await self._ensure_sandbox() if not self.remote_manifest_url: return None try: resp = await self.sandbox.process.exec(f"/bin/sh -c 'curl -fsSL {self.remote_manifest_url} -o /tmp/templates_manifest.json'", timeout=30) if getattr(resp, "exit_code", 1) != 0: return None content = await self.sandbox.fs.download_file("/tmp/templates_manifest.json") return json.loads(content.decode()) except Exception: return None def _format_templates(self, manifest: Dict[str, Any]) -> List[Dict[str, Any]]: templates = manifest.get("templates", []) formatted: List[Dict[str, Any]] = [] for t in templates: formatted.append({ "name": t.get("name"), "description": t.get("description", ""), "tags": t.get("tags", []), "path": t.get("path"), "repo": t.get("repo"), }) return formatted @openapi_schema({ "type": "function", "function": { "name": "list_templates", "description": "List available web project templates (prefers local manifest, falls back to remote).", "parameters": {"type": "object", "properties": {}, "required": []} } }) @usage_example(''' ''') async def list_templates(self) -> ToolResult: try: await self._ensure_sandbox() manifest = await self._read_local_manifest() if not manifest: manifest = await self._fetch_remote_manifest() if not manifest: return self.fail_response("No templates manifest found locally or remotely.") return self.success_response({ "templates": self._format_templates(manifest) }) except Exception as e: logger.error(f"Error listing templates: {e}", exc_info=True) return self.fail_response(f"Error listing templates: {e}") @openapi_schema({ "type": "function", "function": { "name": "scaffold_from_template", "description": "Create a new project from a template. Prefers local /opt/templates/, otherwise downloads {name}.zip from TEMPLATES_ARCHIVE_BASE_URL.", "parameters": { "type": "object", "properties": { "template_name": {"type": "string"}, "project_name": {"type": "string"}, "package_manager": {"type": "string", "default": "pnpm"} }, "required": ["template_name", "project_name"] } } }) @usage_example(''' next14-shadcn webdemo pnpm ''') async def scaffold_from_template(self, template_name: str, project_name: str, package_manager: str = "pnpm") -> ToolResult: try: await self._ensure_sandbox() target_dir = f"{self.workspace_path}/{project_name}" # Ensure target doesn't exist exists = await self.sandbox.process.exec(f"/bin/sh -c 'test -e {target_dir} && echo EXISTS || echo NEW'", timeout=10) if "EXISTS" in (getattr(exists, "result", "") or ""): return self.fail_response(f"Target '{project_name}' already exists") local_path = f"{self.local_templates_dir}/{template_name}" local_ok = await self.sandbox.process.exec(f"/bin/sh -c 'test -d {local_path} && echo OK || echo NO'", timeout=10) if "OK" in (getattr(local_ok, "result", "") or ""): copy = await self.sandbox.process.exec(f"/bin/sh -c 'cp -R {local_path} {target_dir}'", timeout=120) if getattr(copy, "exit_code", 1) != 0: return self.fail_response(f"Failed to copy local template: {getattr(copy, 'result', '')}") else: # Remote zip fallback if not self.remote_archive_base: return self.fail_response("Template not found locally and TEMPLATES_ARCHIVE_BASE_URL is not set") zip_url = self.remote_archive_base.rstrip('/') + f"/{template_name}.zip" cmds = ( f"cd /tmp && rm -rf tmpl_{template_name} template.zip && " f"curl -fsSL {zip_url} -o template.zip && " f"mkdir -p tmpl_{template_name} && unzip -q template.zip -d tmpl_{template_name} && " f"cp -R tmpl_{template_name}/* {target_dir}" ) dl = await self.sandbox.process.exec(f"/bin/sh -c \"{cmds}\"", timeout=300) if getattr(dl, "exit_code", 1) != 0: return self.fail_response(f"Failed to download template: {getattr(dl, 'result', '')}") # Install deps if package_manager == "pnpm": install = await self.sandbox.process.exec(f"/bin/sh -c 'cd {target_dir} && pnpm install --prefer-offline'", timeout=900) else: install = await self.sandbox.process.exec(f"/bin/sh -c 'cd {target_dir} && npm install --no-audit --no-fund --progress=false'", timeout=900) if getattr(install, "exit_code", 1) != 0: return self.fail_response(f"Dependency install failed: {getattr(install, 'result', '')}") return self.success_response({ "message": f"Project '{project_name}' created from template '{template_name}'.", "project": project_name }) except Exception as e: logger.error(f"Error scaffolding from template: {e}", exc_info=True) return self.fail_response(f"Error scaffolding from template: {e}")