""" Shell Tools ai.shellの既存機能をMCPツールとして統合 - コード生成 - ファイル分析 - プロジェクト管理 - LLM統合 """ from typing import Dict, Any, List, Optional import os import subprocess import tempfile from pathlib import Path import requests from .base_tools import BaseMCPTool, config_manager class ShellTools(BaseMCPTool): """シェルツール(元ai.shell機能)""" def __init__(self, config_dir: Optional[str] = None): super().__init__(config_dir) self.ollama_url = "http://localhost:11434" async def code_with_local_llm(self, prompt: str, language: str = "python") -> Dict[str, Any]: """ローカルLLMでコード生成""" config = config_manager.load_config() model = config.get("providers", {}).get("ollama", {}).get("default_model", "qwen2.5-coder:7b") system_prompt = f"You are an expert {language} programmer. Generate clean, well-commented code." try: response = requests.post( f"{self.ollama_url}/api/generate", json={ "model": model, "prompt": f"{system_prompt}\\n\\nUser: {prompt}\\n\\nPlease provide the code:", "stream": False, "options": { "temperature": 0.1, "top_p": 0.95, } }, timeout=300 ) if response.status_code == 200: result = response.json() code = result.get("response", "") return {"code": code, "language": language} else: return {"error": f"Ollama returned status {response.status_code}"} except Exception as e: return {"error": str(e)} async def analyze_file(self, file_path: str, analysis_prompt: str = "Analyze this file") -> Dict[str, Any]: """ファイルを分析""" try: if not os.path.exists(file_path): return {"error": f"File not found: {file_path}"} with open(file_path, 'r', encoding='utf-8') as f: content = f.read() # ファイル拡張子から言語を判定 ext = Path(file_path).suffix language_map = { '.py': 'python', '.rs': 'rust', '.js': 'javascript', '.ts': 'typescript', '.go': 'go', '.java': 'java', '.cpp': 'cpp', '.c': 'c', '.sh': 'shell', '.toml': 'toml', '.json': 'json', '.md': 'markdown' } language = language_map.get(ext, 'text') config = config_manager.load_config() model = config.get("providers", {}).get("ollama", {}).get("default_model", "qwen2.5-coder:7b") prompt = f"{analysis_prompt}\\n\\nFile: {file_path}\\nLanguage: {language}\\n\\nContent:\\n{content}" response = requests.post( f"{self.ollama_url}/api/generate", json={ "model": model, "prompt": prompt, "stream": False, }, timeout=300 ) if response.status_code == 200: result = response.json() analysis = result.get("response", "") return { "analysis": analysis, "file_path": file_path, "language": language, "file_size": len(content), "line_count": len(content.split('\\n')) } else: return {"error": f"Analysis failed: {response.status_code}"} except Exception as e: return {"error": str(e)} async def explain_code(self, code: str, language: str = "python") -> Dict[str, Any]: """コードを説明""" config = config_manager.load_config() model = config.get("providers", {}).get("ollama", {}).get("default_model", "qwen2.5-coder:7b") prompt = f"Explain this {language} code in detail:\\n\\n{code}" try: response = requests.post( f"{self.ollama_url}/api/generate", json={ "model": model, "prompt": prompt, "stream": False, }, timeout=300 ) if response.status_code == 200: result = response.json() explanation = result.get("response", "") return {"explanation": explanation} else: return {"error": f"Explanation failed: {response.status_code}"} except Exception as e: return {"error": str(e)} async def create_project(self, project_type: str, project_name: str, location: str = ".") -> Dict[str, Any]: """プロジェクトを作成""" try: project_path = Path(location) / project_name if project_path.exists(): return {"error": f"Project directory already exists: {project_path}"} project_path.mkdir(parents=True, exist_ok=True) # プロジェクトタイプに応じたテンプレートを作成 if project_type == "rust": await self._create_rust_project(project_path) elif project_type == "python": await self._create_python_project(project_path) elif project_type == "node": await self._create_node_project(project_path) else: # 基本的なプロジェクト構造 (project_path / "src").mkdir() (project_path / "README.md").write_text(f"# {project_name}\\n\\nA new {project_type} project.") return { "status": "success", "project_path": str(project_path), "project_type": project_type, "files_created": list(self._get_project_files(project_path)) } except Exception as e: return {"error": str(e)} async def _create_rust_project(self, project_path: Path): """Rustプロジェクトを作成""" # Cargo.toml cargo_toml = f"""[package] name = "{project_path.name}" version = "0.1.0" edition = "2021" [dependencies] """ (project_path / "Cargo.toml").write_text(cargo_toml) # src/main.rs src_dir = project_path / "src" src_dir.mkdir() (src_dir / "main.rs").write_text('fn main() {\\n println!("Hello, world!");\\n}\\n') # README.md (project_path / "README.md").write_text(f"# {project_path.name}\\n\\nA Rust project.") async def _create_python_project(self, project_path: Path): """Pythonプロジェクトを作成""" # pyproject.toml pyproject_toml = f"""[project] name = "{project_path.name}" version = "0.1.0" description = "A Python project" requires-python = ">=3.8" dependencies = [] [build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" """ (project_path / "pyproject.toml").write_text(pyproject_toml) # src/ src_dir = project_path / "src" / project_path.name src_dir.mkdir(parents=True) (src_dir / "__init__.py").write_text("") (src_dir / "main.py").write_text('def main():\\n print("Hello, world!")\\n\\nif __name__ == "__main__":\\n main()\\n') # README.md (project_path / "README.md").write_text(f"# {project_path.name}\\n\\nA Python project.") async def _create_node_project(self, project_path: Path): """Node.jsプロジェクトを作成""" # package.json package_json = f"""{{ "name": "{project_path.name}", "version": "1.0.0", "description": "A Node.js project", "main": "index.js", "scripts": {{ "start": "node index.js", "test": "echo \\"Error: no test specified\\" && exit 1" }}, "dependencies": {{}} }} """ (project_path / "package.json").write_text(package_json) # index.js (project_path / "index.js").write_text('console.log("Hello, world!");\\n') # README.md (project_path / "README.md").write_text(f"# {project_path.name}\\n\\nA Node.js project.") def _get_project_files(self, project_path: Path) -> List[str]: """プロジェクト内のファイル一覧を取得""" files = [] for file_path in project_path.rglob("*"): if file_path.is_file(): files.append(str(file_path.relative_to(project_path))) return files async def execute_command(self, command: str, working_dir: str = ".") -> Dict[str, Any]: """シェルコマンドを実行""" try: result = subprocess.run( command, shell=True, cwd=working_dir, capture_output=True, text=True, timeout=60 ) return { "status": "success" if result.returncode == 0 else "error", "returncode": result.returncode, "stdout": result.stdout, "stderr": result.stderr, "command": command, "working_dir": working_dir } except subprocess.TimeoutExpired: return {"error": "Command timed out"} except Exception as e: return {"error": str(e)} async def write_file(self, file_path: str, content: str, backup: bool = True) -> Dict[str, Any]: """ファイルを書き込み(バックアップオプション付き)""" try: file_path_obj = Path(file_path) # バックアップ作成 backup_path = None if backup and file_path_obj.exists(): backup_path = f"{file_path}.backup" with open(file_path, 'r', encoding='utf-8') as src: with open(backup_path, 'w', encoding='utf-8') as dst: dst.write(src.read()) # ファイル書き込み file_path_obj.parent.mkdir(parents=True, exist_ok=True) with open(file_path, 'w', encoding='utf-8') as f: f.write(content) return { "status": "success", "file_path": file_path, "backup_path": backup_path, "bytes_written": len(content.encode('utf-8')) } except Exception as e: return {"error": str(e)} def get_tools(self) -> List[Dict[str, Any]]: """利用可能なツール一覧""" return [ { "name": "generate_code", "description": "ローカルLLMでコード生成", "parameters": { "prompt": "string", "language": "string (optional, default: python)" } }, { "name": "analyze_file", "description": "ファイルを分析", "parameters": { "file_path": "string", "analysis_prompt": "string (optional)" } }, { "name": "explain_code", "description": "コードを説明", "parameters": { "code": "string", "language": "string (optional, default: python)" } }, { "name": "create_project", "description": "新しいプロジェクトを作成", "parameters": { "project_type": "string (rust/python/node)", "project_name": "string", "location": "string (optional, default: .)" } }, { "name": "execute_command", "description": "シェルコマンドを実行", "parameters": { "command": "string", "working_dir": "string (optional, default: .)" } }, { "name": "write_file", "description": "ファイルを書き込み", "parameters": { "file_path": "string", "content": "string", "backup": "boolean (optional, default: true)" } } ] async def execute_tool(self, tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]: """ツールを実行""" try: if tool_name == "generate_code": result = await self.code_with_local_llm( prompt=params["prompt"], language=params.get("language", "python") ) return result elif tool_name == "analyze_file": result = await self.analyze_file( file_path=params["file_path"], analysis_prompt=params.get("analysis_prompt", "Analyze this file") ) return result elif tool_name == "explain_code": result = await self.explain_code( code=params["code"], language=params.get("language", "python") ) return result elif tool_name == "create_project": result = await self.create_project( project_type=params["project_type"], project_name=params["project_name"], location=params.get("location", ".") ) return result elif tool_name == "execute_command": result = await self.execute_command( command=params["command"], working_dir=params.get("working_dir", ".") ) return result elif tool_name == "write_file": result = await self.write_file( file_path=params["file_path"], content=params["content"], backup=params.get("backup", True) ) return result else: return {"error": f"Unknown tool: {tool_name}"} except Exception as e: return {"error": str(e)}