""" Base tool implementations that execute actual system operations. """ import os import json import glob import subprocess import uuid import asyncio from typing import Dict, Any, Optional, List from pathlib import Path from datetime import datetime # Base directory for memory and skills HERMES_DIR = os.path.expanduser("~/.hermes") def _get_user_dir(base_dir: str, context: Optional[Dict[str, Any]] = None) -> str: """Get user-isolated subdirectory. Falls back to global dir if no user context.""" user_id = None if context: user_id = context.get('user_id') or context.get('userid') if user_id: return os.path.join(base_dir, "users", str(user_id)) return base_dir # Shared skills directory (owner org only can write, all can read) SHARED_SKILLS_DIR = os.path.join(HERMES_DIR, "skills") def _is_owner_org(context: Optional[Dict[str, Any]] = None) -> bool: """Check if current user belongs to the owner organization (org_id == '0'). Checks context first, then falls back to ServerEnv.""" # 1. Check context for org_id if context: org_id = context.get('org_id') or context.get('orgid') if org_id is not None: return str(org_id) == '0' # 2. Try ServerEnv try: from ahserver.serverenv import ServerEnv env = ServerEnv() org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', None) if org_id is not None: return str(org_id) == '0' except Exception: pass return False def _get_shared_skill_path(name: str, file_path: Optional[str] = None) -> str: """Get path to a shared skill file.""" if file_path: return os.path.join(SHARED_SKILLS_DIR, name, file_path) return os.path.join(SHARED_SKILLS_DIR, name, "SKILL.md") def _get_user_skill_path(user_dir: str, name: str, file_path: Optional[str] = None) -> str: """Get path to a user-specific skill file.""" skills_dir = os.path.join(user_dir, "skills") if file_path: return os.path.join(skills_dir, name, file_path) return os.path.join(skills_dir, name, "SKILL.md") async def wrapped_read_file(path: str, offset: int = 1, limit: int = 500) -> Dict[str, Any]: """Actual implementation of read_file tool.""" try: full_path = os.path.expanduser(path) if not os.path.exists(full_path): return {"success": False, "error": f"File not found: {path}"} with open(full_path, 'r', encoding='utf-8', errors='ignore') as f: all_lines = f.readlines() total_lines = len(all_lines) start_idx = max(0, offset - 1) end_idx = min(total_lines, start_idx + limit) content = "".join(all_lines[start_idx:end_idx]) return { "success": True, "content": content, "total_lines": total_lines, "start_line": offset, "end_line": end_idx, "truncated": end_idx < total_lines } except Exception as e: return {"success": False, "error": str(e)} async def wrapped_write_file(path: str, content: str) -> Dict[str, Any]: """Actual implementation of write_file tool.""" try: full_path = os.path.expanduser(path) os.makedirs(os.path.dirname(full_path), exist_ok=True) if os.path.dirname(full_path) else None with open(full_path, 'w', encoding='utf-8') as f: f.write(content) return {"success": True, "path": path, "bytes_written": len(content.encode('utf-8'))} except Exception as e: return {"success": False, "error": str(e)} async def wrapped_search_files(pattern: str, target: str = "content", path: str = ".", file_glob: Optional[str] = None, limit: int = 50) -> Dict[str, Any]: """Actual implementation of search_files tool.""" try: search_path = os.path.expanduser(path) results = [] if target == "files": # Search for files by name pattern if file_glob: search_pattern = os.path.join(search_path, "**", file_glob) else: search_pattern = os.path.join(search_path, "**", pattern) for fpath in glob.glob(search_pattern, recursive=True): if os.path.isfile(fpath): results.append(fpath) if len(results) >= limit: break return {"success": True, "matches": results[:limit], "total_count": len(results)} else: # Search for content inside files import re regex = re.compile(pattern, re.IGNORECASE) search_pattern = os.path.join(search_path, "**", "*.py") if not file_glob else os.path.join(search_path, "**", file_glob) if file_glob and '*' not in file_glob: search_pattern = os.path.join(search_path, "**", file_glob) elif not file_glob: search_pattern = os.path.join(search_path, "**", "*.*") count = 0 for fpath in glob.glob(search_pattern, recursive=True): if not os.path.isfile(fpath): continue try: with open(fpath, 'r', encoding='utf-8', errors='ignore') as f: for i, line in enumerate(f, 1): if regex.search(line): results.append({"file": fpath, "line": i, "content": line.strip()}) count += 1 if count >= limit: break except: pass if count >= limit: break return {"success": True, "matches": results, "total_count": count} except Exception as e: return {"success": False, "error": str(e)} async def wrapped_patch(mode: str = "replace", path: str = "", old_string: str = "", new_string: str = "", replace_all: bool = False) -> Dict[str, Any]: """Actual implementation of patch tool.""" try: full_path = os.path.expanduser(path) with open(full_path, 'r', encoding='utf-8') as f: content = f.read() if mode == "replace": if old_string not in content: return {"success": False, "error": "old_string not found in file"} if replace_all: new_content = content.replace(old_string, new_string) else: new_content = content.replace(old_string, new_string, 1) with open(full_path, 'w', encoding='utf-8') as f: f.write(new_content) return {"success": True, "path": path, "mode": "replace"} else: return {"success": False, "error": f"Unsupported mode: {mode}"} except Exception as e: return {"success": False, "error": str(e)} async def wrapped_terminal(command: str, background: bool = False, timeout: int = 180, workdir: Optional[str] = None, pty: bool = False, notify_on_complete: bool = False) -> Dict[str, Any]: """Actual implementation of terminal tool.""" try: cwd = os.path.expanduser(workdir) if workdir else os.getcwd() if background: # Run in background proc = await asyncio.create_subprocess_shell( command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, cwd=cwd ) return {"success": True, "pid": proc.pid, "status": "background_started", "command": command} else: proc = await asyncio.create_subprocess_shell( command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, cwd=cwd ) try: stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) return { "success": proc.returncode == 0, "output": stdout.decode('utf-8', errors='ignore'), "error_output": stderr.decode('utf-8', errors='ignore'), "exit_code": proc.returncode, "command": command } except asyncio.TimeoutError: proc.kill() return {"success": False, "error": f"Command timed out after {timeout}s", "command": command} except Exception as e: return {"success": False, "error": str(e)} async def wrapped_process(action: str, session_id: Optional[str] = None, data: Optional[str] = None, timeout: Optional[int] = None) -> Dict[str, Any]: """Wrapper for process tool - tracks background processes.""" # Note: Full process tracking requires persistent state. # For now, returns mock for management actions. return {"success": True, "action": action, "session_id": session_id, "note": "Process management state requires external tracking"} async def wrapped_execute_code(code: str, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """Actual implementation of execute_code tool, user-isolated temp dir.""" try: user_dir = _get_user_dir(HERMES_DIR, context) temp_dir = os.path.join(user_dir, "tmp") os.makedirs(temp_dir, exist_ok=True) temp_file = os.path.join(temp_dir, f"exec_{uuid.uuid4().hex[:8]}.py") with open(temp_file, 'w', encoding='utf-8') as f: f.write(code) proc = await asyncio.create_subprocess_shell( f"python3 {temp_file}", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, cwd=os.getcwd() ) stdout, stderr = await proc.communicate() # Clean up temp file os.remove(temp_file) return { "success": proc.returncode == 0, "output": stdout.decode('utf-8', errors='ignore'), "error_output": stderr.decode('utf-8', errors='ignore'), "exit_code": proc.returncode } except Exception as e: return {"success": False, "error": str(e)} # --- AI & Browser tools (require external services or complex setup, mock for now but structured) --- async def wrapped_vision_analyze(image_url: str, question: str) -> Dict[str, Any]: return {"success": True, "tool": "vision_analyze", "note": "Requires external vision model integration", "image_url": image_url} async def wrapped_text_to_speech(text: str, output_path: Optional[str] = None) -> Dict[str, Any]: return {"success": True, "tool": "text_to_speech", "note": "Requires external TTS engine"} async def wrapped_browser_navigate(url: str) -> Dict[str, Any]: return {"success": True, "tool": "browser_navigate", "note": "Requires browser automation driver"} # --- Memory & Session tools --- async def wrapped_memory(action: str, target: str, content: str = "", old_text: str = "", context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """Actual implementation of memory tool using local JSON file, user-isolated.""" try: user_dir = _get_user_dir(HERMES_DIR, context) memory_file = os.path.join(user_dir, "memory.json") os.makedirs(user_dir, exist_ok=True) if not os.path.exists(memory_file): memory = {"user": [], "system": []} else: with open(memory_file, 'r') as f: memory = json.load(f) if action == "add": memory[target].append(content) with open(memory_file, 'w') as f: json.dump(memory, f, indent=2) return {"success": True, "action": "add", "target": target} elif action == "list": return {"success": True, "entries": memory.get(target, [])} else: return {"success": False, "error": f"Unknown action: {action}"} except Exception as e: return {"success": False, "error": str(e)} async def wrapped_session_search(query: Optional[str] = None, limit: int = 3) -> Dict[str, Any]: return {"success": True, "sessions": [], "note": "Session history tracking requires external indexing"} async def wrapped_skill_view(name: str, file_path: Optional[str] = None, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """View a skill. Checks user-specific dir first, then shared skills.""" try: user_dir = _get_user_dir(HERMES_DIR, context) # 1. Check user-specific skill first user_path = _get_user_skill_path(user_dir, name, file_path) if os.path.exists(user_path): with open(user_path, 'r') as f: return {"success": True, "content": f.read(), "source": "user"} # 2. Check shared skill shared_path = _get_shared_skill_path(name, file_path) if os.path.exists(shared_path): with open(shared_path, 'r') as f: return {"success": True, "content": f.read(), "source": "shared"} return {"success": False, "error": "Skill not found"} except Exception as e: return {"success": False, "error": str(e)} async def wrapped_skills_list(category: Optional[str] = None, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """List both user-specific skills and shared skills with source indicator.""" try: user_dir = _get_user_dir(HERMES_DIR, context) user_skills_dir = os.path.join(user_dir, "skills") skills = [] # 1. List user-specific skills if os.path.exists(user_skills_dir): for d in os.listdir(user_skills_dir): skill_path = os.path.join(user_skills_dir, d) if os.path.isdir(skill_path) and os.path.exists(os.path.join(skill_path, "SKILL.md")): skills.append({"name": d, "source": "user"}) # 2. List shared skills if os.path.exists(SHARED_SKILLS_DIR): for d in os.listdir(SHARED_SKILLS_DIR): skill_path = os.path.join(SHARED_SKILLS_DIR, d) if os.path.isdir(skill_path) and os.path.exists(os.path.join(skill_path, "SKILL.md")): # Don't duplicate if same name exists in user skills if not any(s["name"] == d for s in skills): skills.append({"name": d, "source": "shared"}) return {"success": True, "skills": skills} except Exception as e: return {"success": False, "error": str(e)} async def wrapped_skill_manage(action: str, name: str, context: Optional[Dict[str, Any]] = None, **kwargs) -> Dict[str, Any]: """Manage skills with owner-org permission check for shared skills. Dual-layer architecture: - User skills (~/.hermes/users/{user_id}/skills/): read/write for owner - Shared skills (~/.hermes/skills/): read for all, write for owner org only Operations target user skills by default. Use source='shared' kwarg to target shared skills (requires owner org membership). """ try: user_dir = _get_user_dir(HERMES_DIR, context) target = kwargs.pop('source', 'user') # 'user' (default) or 'shared' owner = _is_owner_org(context) if target == "shared": # Shared skill operations require owner org membership if not owner: return {"success": False, "error": "共享技能仅允许所有者机构用户修改", "source": "shared"} skills_dir = os.path.join(SHARED_SKILLS_DIR, name) else: # User skill operations skills_dir = os.path.join(user_dir, "skills", name) if action == "create": if target == "shared" and os.path.exists(skills_dir): return {"success": False, "error": "共享技能已存在"} os.makedirs(skills_dir, exist_ok=True) if 'content' in kwargs: with open(os.path.join(skills_dir, "SKILL.md"), 'w') as f: f.write(kwargs['content']) return {"success": True, "action": "create", "name": name, "source": target} elif action == "patch": skill_file = os.path.join(skills_dir, "SKILL.md") if not os.path.exists(skill_file): return {"success": False, "error": "Skill not found"} with open(skill_file, 'r') as f: content = f.read() old_string = kwargs.get('old_string', '') new_string = kwargs.get('new_string', '') if old_string not in content: return {"success": False, "error": "old_string not found in skill"} new_content = content.replace(old_string, new_string, 1) with open(skill_file, 'w') as f: f.write(new_content) return {"success": True, "action": "patch", "name": name, "source": target} elif action == "edit": os.makedirs(skills_dir, exist_ok=True) if 'content' in kwargs: with open(os.path.join(skills_dir, "SKILL.md"), 'w') as f: f.write(kwargs['content']) return {"success": True, "action": "edit", "name": name, "source": target} elif action == "delete": import shutil if os.path.exists(skills_dir): shutil.rmtree(skills_dir) return {"success": True, "action": "delete", "name": name, "source": target} elif action == "view": skill_file = os.path.join(skills_dir, "SKILL.md") if os.path.exists(skill_file): with open(skill_file, 'r') as f: return {"success": True, "content": f.read(), "source": target} return {"success": False, "error": "Skill not found", "source": target} elif action == "write_file": file_path = kwargs.get('file_path', '') file_content = kwargs.get('file_content', '') if not file_path: return {"success": False, "error": "file_path required"} os.makedirs(skills_dir, exist_ok=True) full_path = os.path.join(skills_dir, file_path) os.makedirs(os.path.dirname(full_path), exist_ok=True) with open(full_path, 'w') as f: f.write(file_content) return {"success": True, "action": "write_file", "name": name, "source": target} elif action == "remove_file": file_path = kwargs.get('file_path', '') if not file_path: return {"success": False, "error": "file_path required"} full_path = os.path.join(skills_dir, file_path) if os.path.exists(full_path): os.remove(full_path) return {"success": True, "action": "remove_file", "name": name, "source": target} return {"success": False, "error": f"Unsupported action: {action}"} except Exception as e: return {"success": False, "error": str(e)} async def wrapped_todo(todos: Optional[List[Dict[str, Any]]] = None, merge: bool = False, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: try: user_dir = _get_user_dir(HERMES_DIR, context) todo_file = os.path.join(user_dir, "todo.json") os.makedirs(user_dir, exist_ok=True) if todos is not None: with open(todo_file, 'w') as f: json.dump(todos, f, indent=2) return {"success": True, "action": "save", "count": len(todos)} if os.path.exists(todo_file): with open(todo_file, 'r') as f: return {"success": True, "todos": json.load(f)} return {"success": True, "todos": []} except Exception as e: return {"success": False, "error": str(e)} async def wrapped_delegate_task(goal: Optional[str] = None, context: Optional[str] = None, tasks: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]: return {"success": True, "note": "Delegation requires spawning child processes/agents"} async def wrapped_clarify(question: str, choices: Optional[List[str]] = None) -> Dict[str, Any]: return {"success": True, "question": question, "choices": choices, "note": "Clarification is handled by the orchestrator UI"} async def wrapped_cronjob(action: str, **kwargs) -> Dict[str, Any]: return {"success": True, "note": "Cron management requires external scheduler integration"} # Group tools by category for registration file_tools = { 'read_file': wrapped_read_file, 'write_file': wrapped_write_file, 'search_files': wrapped_search_files, 'patch': wrapped_patch } system_tools = { 'terminal': wrapped_terminal, 'process': wrapped_process, 'execute_code': wrapped_execute_code } ai_tools = { 'vision_analyze': wrapped_vision_analyze, 'text_to_speech': wrapped_text_to_speech } async def wrapped_browser_snapshot(full: bool = False) -> Dict[str, Any]: return {"success": True, "tool": "browser_snapshot", "note": "Requires browser automation driver"} async def wrapped_browser_click(ref: str) -> Dict[str, Any]: return {"success": True, "tool": "browser_click", "note": "Requires browser automation driver"} async def wrapped_browser_type(ref: str, text: str) -> Dict[str, Any]: return {"success": True, "tool": "browser_type", "note": "Requires browser automation driver"} async def wrapped_browser_press(key: str) -> Dict[str, Any]: return {"success": True, "tool": "browser_press", "note": "Requires browser automation driver"} async def wrapped_browser_scroll(direction: str) -> Dict[str, Any]: return {"success": True, "tool": "browser_scroll", "note": "Requires browser automation driver"} async def wrapped_browser_console(clear: bool = False, expression: str = None) -> Dict[str, Any]: return {"success": True, "tool": "browser_console", "note": "Requires browser automation driver"} async def wrapped_browser_get_images() -> Dict[str, Any]: return {"success": True, "tool": "browser_get_images", "note": "Requires browser automation driver"} async def wrapped_browser_vision(question: str, annotate: bool = False) -> Dict[str, Any]: return {"success": True, "tool": "browser_vision", "note": "Requires browser automation driver"} async def wrapped_browser_back() -> Dict[str, Any]: return {"success": True, "tool": "browser_back", "note": "Requires browser automation driver"} browser_tools = { 'browser_navigate': wrapped_browser_navigate, 'browser_snapshot': wrapped_browser_snapshot, 'browser_click': wrapped_browser_click, 'browser_type': wrapped_browser_type, 'browser_press': wrapped_browser_press, 'browser_scroll': wrapped_browser_scroll, 'browser_console': wrapped_browser_console, 'browser_get_images': wrapped_browser_get_images, 'browser_vision': wrapped_browser_vision, 'browser_back': wrapped_browser_back } memory_tools = { 'memory': wrapped_memory, 'session_search': wrapped_session_search } skill_tools = { 'skill_view': wrapped_skill_view, 'skills_list': wrapped_skills_list, 'skill_manage': wrapped_skill_manage } task_tools = { 'todo': wrapped_todo, 'delegate_task': wrapped_delegate_task, 'clarify': wrapped_clarify, 'cronjob': wrapped_cronjob }