From 4cf0fa8752b8caade6369d868b3b4b96282ac819 Mon Sep 17 00:00:00 2001 From: ohtaman Date: Sun, 3 Aug 2025 07:20:59 +0900 Subject: [PATCH 1/4] Test pre-commit hooks --- .claude/commands/tasks.md | 1 - .claude/settings.json | 3 +- .github/workflows/ci.yml | 4 +- .github/workflows/docs.md | 2 +- .github/workflows/main.yml | 4 +- .pre-commit-config.yaml | 24 +++++ .tmp/design.md | 154 +++++++++++++++--------------- .tmp/issue2-solution-summary.md | 10 +- .tmp/pyodide_architecture.md | 34 +++---- .tmp/requirements.md | 2 +- .tmp/solver_selection_design.md | 2 +- .tmp/tasks.md | 130 ++++++++++++------------- Makefile | 17 +++- README.md | 52 ++++++++-- docs/FUNCTIONAL_REQUIREMENTS.md | 4 +- docs/IMPLEMENTATION_PLAN.md | 4 +- docs/LLM_ENHANCED_MIP_RESEARCH.md | 2 +- docs/SYSTEM_REQUIREMENTS.md | 2 +- docs/TECHNICAL_REQUIREMENTS.md | 2 +- docs/USER_REQUIREMENTS.md | 34 +++---- pyproject.toml | 5 +- pytest.ini | 8 +- src/mip_mcp/config/default.yaml | 10 +- tests/README.md | 2 +- uv.lock | 86 +++++++++++++++++ 25 files changed, 378 insertions(+), 220 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.claude/commands/tasks.md b/.claude/commands/tasks.md index 4067bd1..526990c 100644 --- a/.claude/commands/tasks.md +++ b/.claude/commands/tasks.md @@ -143,4 +143,3 @@ Show the task breakdown and: - Include testing tasks throughout, not just at the end think hard - diff --git a/.claude/settings.json b/.claude/settings.json index e791c6a..145c9c1 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -68,7 +68,8 @@ "Bash(git rebase:*)", "Bash(curl:*)", "Bash(wget:*)", - "Bash(uv:*)" + "Bash(uv:*)", + "Bash(gh:*view*)" ], "deny": [ "Bash(sudo:*)", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae3f1b0..cb379f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: coverage: runs-on: ubuntu-latest if: github.event_name == 'pull_request' - + steps: - uses: actions/checkout@v4 @@ -65,4 +65,4 @@ jobs: if: always() with: file: ./htmlcov/index.html - fail_ci_if_error: false \ No newline at end of file + fail_ci_if_error: false diff --git a/.github/workflows/docs.md b/.github/workflows/docs.md index ecc6c08..84e85f0 100644 --- a/.github/workflows/docs.md +++ b/.github/workflows/docs.md @@ -58,4 +58,4 @@ make lint # Check formatting uv run ruff format --check src/ tests/ -``` \ No newline at end of file +``` diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 549ed19..c17a2ee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,7 @@ on: jobs: test-and-build: runs-on: ubuntu-latest - + steps: - uses: actions/checkout@v4 @@ -46,4 +46,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: dist-packages - path: dist/ \ No newline at end of file + path: dist/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b810d33 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +# Pre-commit hooks for automated code quality checks +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-ast + - id: check-merge-conflict + - id: check-toml + - id: debug-statements + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + types_or: [python, pyi] + - id: ruff-format + types_or: [python, pyi] diff --git a/.tmp/design.md b/.tmp/design.md index 33ad148..a605bcf 100644 --- a/.tmp/design.md +++ b/.tmp/design.md @@ -82,15 +82,15 @@ class MIPMCPServer: self.solver_factory = SolverFactory(self.config) self.app = FastMCP("mip-mcp") self._register_handlers() - + def _register_handlers(self): # MCPツール登録 self.app.add_tool("solve_optimization", solve_handler) self.app.add_tool("execute_python_code", execute_code_handler) self.app.add_tool("get_library_examples", get_library_examples_handler) - self.app.add_tool("validate_model", validate_handler) + self.app.add_tool("validate_model", validate_handler) self.app.add_tool("get_solver_status", status_handler) - + async def run(self): await self.app.run() ``` @@ -109,22 +109,22 @@ class BaseSolver(ABC): def __init__(self, config: Dict[str, Any]): self.config = config self.model = None - + @abstractmethod async def solve(self, problem: OptimizationProblem) -> OptimizationSolution: """最適化問題を解決する""" pass - + @abstractmethod def validate_problem(self, problem: OptimizationProblem) -> Dict[str, Any]: """問題の妥当性を検証する""" pass - + @abstractmethod def get_solver_info(self) -> Dict[str, Any]: """ソルバー情報を取得する""" pass - + @abstractmethod def set_parameters(self, params: Dict[str, Any]) -> None: """ソルバーパラメータを設定する""" @@ -143,7 +143,7 @@ class SCIPSolver(BaseSolver): def __init__(self, config: Dict[str, Any]): super().__init__(config) self.model = pyscipopt.Model() - + async def solve(self, problem: OptimizationProblem) -> OptimizationSolution: try: # 変数定義 @@ -155,21 +155,21 @@ class SCIPSolver(BaseSolver): lb=var_def.lower_bound, ub=var_def.upper_bound ) - + # 制約条件追加 for constraint in problem.constraints: expr = self._build_expression(constraint.expression, variables) self.model.addCons(expr <= constraint.rhs, name=constraint.name) - + # 目的関数設定 obj_expr = self._build_expression(problem.objective.expression, variables) self.model.setObjective(obj_expr, sense=problem.objective.sense) - + # 最適化実行 self.model.optimize() - + return self._extract_solution(variables) - + except Exception as e: return OptimizationSolution( status="error", @@ -197,35 +197,35 @@ class PythonCodeExecutor: self.config = config self.security_checker = SecurityChecker() self.allowed_imports = get_allowed_imports() - + async def execute_optimization_code(self, code: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """ Pythonコードを実行して最適化問題を解決する - + Args: code: 実行するPythonコード(PuLP/pyomo/pyscipopt等を使用) data: コードで使用するデータ - + Returns: 最適化結果 """ try: # セキュリティチェック self.security_checker.validate_code(code) - + # 実行環境の準備 namespace = self._prepare_namespace(data) - + # コード実行 output = io.StringIO() with contextlib.redirect_stdout(output): exec(code, namespace) - + # 結果の抽出 result = self._extract_result(namespace, output.getvalue()) - + return result - + except Exception as e: return { "status": "error", @@ -233,20 +233,20 @@ class PythonCodeExecutor: "objective_value": None, "variables": {} } - + def _prepare_namespace(self, data: Optional[Dict[str, Any]]) -> Dict[str, Any]: """実行環境の名前空間を準備""" namespace = { '__builtins__': self._get_safe_builtins(), 'data': data or {}, } - + # 許可されたライブラリをインポート for lib_name, lib_module in self.allowed_imports.items(): namespace[lib_name] = lib_module - + return namespace - + def _get_safe_builtins(self) -> Dict[str, Any]: """安全なbuiltins関数のみを提供""" safe_builtins = { @@ -254,23 +254,23 @@ class PythonCodeExecutor: 'abs', 'round', 'int', 'float', 'str', 'list', 'dict', 'tuple', 'set', 'bool', 'print' } - + return {name: getattr(__builtins__, name) for name in safe_builtins if hasattr(__builtins__, name)} - + def _extract_result(self, namespace: Dict[str, Any], output: str) -> Dict[str, Any]: """実行結果から最適化結果を抽出""" # PuLPの場合 if 'pulp' in output.lower() or any('pulp' in str(v) for v in namespace.values()): return self._extract_pulp_result(namespace) - - # pyomoの場合 + + # pyomoの場合 if 'pyomo' in output.lower() or any('pyomo' in str(type(v)) for v in namespace.values()): return self._extract_pyomo_result(namespace) - + # pyscipoptの場合 if 'scip' in output.lower() or any('scip' in str(type(v)) for v in namespace.values()): return self._extract_scip_result(namespace) - + # 汎用的な結果抽出 return self._extract_generic_result(namespace, output) ``` @@ -283,53 +283,53 @@ from typing import Set, List class SecurityChecker: """コードのセキュリティチェックを行う""" - + DANGEROUS_FUNCTIONS = { 'eval', 'exec', 'compile', '__import__', 'open', 'file', 'input', 'raw_input', 'reload', 'vars', 'globals', 'locals', 'dir', 'getattr', 'setattr', 'delattr', 'hasattr' } - + DANGEROUS_MODULES = { 'os', 'sys', 'subprocess', 'shutil', 'glob', 'socket', 'urllib', 'http', 'ftplib', 'smtplib', 'multiprocessing', 'threading', 'pickle', 'marshal', 'shelve' } - + def validate_code(self, code: str) -> bool: """コードの安全性を検証""" try: tree = ast.parse(code) checker = DangerousNodeVisitor() checker.visit(tree) - + if checker.dangerous_nodes: raise SecurityError(f"Dangerous operations detected: {checker.dangerous_nodes}") - + return True - + except SyntaxError as e: raise SecurityError(f"Syntax error in code: {str(e)}") class DangerousNodeVisitor(ast.NodeVisitor): """危険なASTノードを検出する""" - + def __init__(self): self.dangerous_nodes = [] - + def visit_Call(self, node): # 危険な関数呼び出しをチェック if isinstance(node.func, ast.Name) and node.func.id in SecurityChecker.DANGEROUS_FUNCTIONS: self.dangerous_nodes.append(f"Function call: {node.func.id}") self.generic_visit(node) - + def visit_Import(self, node): # 危険なモジュールのインポートをチェック for alias in node.names: if alias.name in SecurityChecker.DANGEROUS_MODULES: self.dangerous_nodes.append(f"Import: {alias.name}") self.generic_visit(node) - + def visit_ImportFrom(self, node): # from文での危険なインポートをチェック if node.module in SecurityChecker.DANGEROUS_MODULES: @@ -350,16 +350,16 @@ import importlib def get_allowed_imports() -> Dict[str, Any]: """許可されたライブラリのマッピングを返す""" allowed_libs = {} - + # 最適化ライブラリ optimization_libs = [ 'pulp', - 'pyomo', + 'pyomo', 'pyscipopt', 'cvxpy', 'ortools' ] - + # 数値計算ライブラリ numeric_libs = [ 'numpy', @@ -368,7 +368,7 @@ def get_allowed_imports() -> Dict[str, Any]: 'math', 'statistics' ] - + # 全てのライブラリを試行してインポート for lib_name in optimization_libs + numeric_libs: try: @@ -377,7 +377,7 @@ def get_allowed_imports() -> Dict[str, Any]: except ImportError: # ライブラリが利用できない場合はスキップ continue - + return allowed_libs def get_library_info() -> Dict[str, Dict[str, Any]]: @@ -391,7 +391,7 @@ import pulp # 問題定義 prob = pulp.LpProblem("Example", pulp.LpMaximize) -# 変数定義 +# 変数定義 x = pulp.LpVariable("x", 0, None) y = pulp.LpVariable("y", 0, None) @@ -469,22 +469,22 @@ async def execute_code_handler( ) -> Dict[str, Any]: """ Pythonコードを実行して最適化問題を解決する - + Args: code: 実行するPythonコード data: コードで使用するデータ library: 使用する最適化ライブラリのヒント - + Returns: 最適化結果 """ try: executor = PythonCodeExecutor(context.config) result = await executor.execute_optimization_code(code, data) - + logger.info(f"Code execution completed with library {library}") return result - + except Exception as e: logger.error(f"Code execution failed: {str(e)}") return { @@ -499,7 +499,7 @@ async def get_library_examples_handler(context: Context) -> Dict[str, Any]: 利用可能なライブラリとサンプルコードを返す """ from ..executor.libraries import get_library_info - + return { "libraries": get_library_info(), "supported_formats": ["pulp", "pyomo", "pyscipopt", "cvxpy", "ortools"] @@ -529,13 +529,13 @@ class Variable(BaseModel): type: VariableType = VariableType.CONTINUOUS lower_bound: Optional[float] = None upper_bound: Optional[float] = None - + class Constraint(BaseModel): name: str expression: Dict[str, float] # {variable_name: coefficient} sense: Literal["<=", ">=", "="] = "<=" rhs: float - + class Objective(BaseModel): sense: ObjectiveSense expression: Dict[str, float] # {variable_name: coefficient} @@ -596,12 +596,12 @@ async def solve_handler( ) -> Dict[str, Any]: """ 最適化問題を解決する - + Args: problem_data: 最適化問題データ(JSON/LP/MPS形式) solver_name: 使用するソルバー名 solver_params: ソルバーパラメータ - + Returns: 最適化結果 """ @@ -609,21 +609,21 @@ async def solve_handler( # 問題データのパース parser = get_parser(problem_data) problem = parser.parse(problem_data) - + # ソルバー取得 solver = context.solver_factory.get_solver(solver_name) - + # パラメータ設定 if solver_params: solver.set_parameters(solver_params) - + # 最適化実行 solution = await solver.solve(problem) - + logger.info(f"Optimization completed: {solution.status}") - + return solution.dict() - + except Exception as e: logger.error(f"Optimization failed: {str(e)}") return { @@ -643,23 +643,23 @@ async def solve_handler( server: name: "mip-mcp" version: "0.1.0" - + logging: level: "INFO" format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - + solvers: default: "scip" timeout: 3600 # seconds - + executor: enabled: true timeout: 300 # seconds for code execution memory_limit: "1GB" - + parsers: supported_formats: ["json", "lp", "mps", "python"] - + validation: max_variables: 100000 max_constraints: 100000 @@ -674,14 +674,14 @@ scip: parameters: limits/time: 3600 display/verblevel: 1 - + gurobi: class: "GurobiSolver" enabled: false parameters: TimeLimit: 3600 OutputFlag: 1 - + cplex: class: "CPLEXSolver" enabled: false @@ -701,15 +701,15 @@ class ConfigManager: def __init__(self, config_path: Optional[str] = None): self.config_dir = Path(config_path) if config_path else Path(__file__).parent.parent / "config" self.config = self._load_config() - + def _load_config(self) -> Dict[str, Any]: """設定ファイルを読み込む""" default_config = self._load_yaml("default.yaml") solver_config = self._load_yaml("solvers.yaml") - + default_config["solvers_config"] = solver_config return default_config - + def _load_yaml(self, filename: str) -> Dict[str, Any]: """YAMLファイルを読み込む""" config_path = self.config_dir / filename @@ -717,7 +717,7 @@ class ConfigManager: with open(config_path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) or {} return {} - + def get(self, key: str, default: Any = None) -> Any: """設定値を取得する""" keys = key.split('.') @@ -790,7 +790,7 @@ class JSONParser: def parse(self, data: Union[Dict[str, Any], str]) -> OptimizationProblem: """ JSON形式の最適化問題をパースする - + Expected format: { "name": "problem_name", @@ -808,7 +808,7 @@ class JSONParser: """ if isinstance(data, str): data = json.loads(data) - + return OptimizationProblem.parse_obj(data) ``` @@ -973,7 +973,7 @@ prob.solve() # 結果をグローバル変数として設定 result = { "status": pulp.LpStatus[prob.status], - "objective": pulp.value(prob.objective), + "objective": pulp.value(prob.objective), "variables": {v.name: v.varValue for v in prob.variables()} } """, @@ -999,4 +999,4 @@ result = { - 分散処理対応(将来) - キューイングシステム統合 - 結果キャッシュ機能 -- Docker化とKubernetes対応 \ No newline at end of file +- Docker化とKubernetes対応 diff --git a/.tmp/issue2-solution-summary.md b/.tmp/issue2-solution-summary.md index 211b741..8de47ab 100644 --- a/.tmp/issue2-solution-summary.md +++ b/.tmp/issue2-solution-summary.md @@ -10,11 +10,11 @@ Issue #2 identified potential ghost process (orphaned subprocess) problems in th **Solution Implemented**: - **Graceful shutdown cascade**: Try exit command → SIGTERM → SIGKILL with proper timeouts -- **Proper timeout handling**: 2s for graceful exit, 5s for SIGTERM, 2s for SIGKILL +- **Proper timeout handling**: 2s for graceful exit, 5s for SIGTERM, 2s for SIGKILL - **Idempotent cleanup**: Multiple cleanup calls are safe - **Improved `__del__` method**: Synchronous fallback cleanup for destruction scenarios -### ✅ 2. Process Group Management +### ✅ 2. Process Group Management **Location**: `src/mip_mcp/executor/pyodide_executor.py:184` **Solution Implemented**: @@ -69,7 +69,7 @@ Issue #2 identified potential ghost process (orphaned subprocess) problems in th ## Implementation Statistics - **Files Modified**: 4 core files -- **New Files Added**: 1 (executor_registry.py) +- **New Files Added**: 1 (executor_registry.py) - **Test Files Added**: 1 (test_subprocess_cleanup.py) - **Test Cases**: 14 tests covering all scenarios - **Test Pass Rate**: 100% (88 passed, 7 skipped) @@ -77,7 +77,7 @@ Issue #2 identified potential ghost process (orphaned subprocess) problems in th ## Acceptance Criteria Status - ✅ No ghost processes remain after server shutdown -- ✅ Signal handlers properly clean up all subprocesses +- ✅ Signal handlers properly clean up all subprocesses - ✅ Process cleanup works under all exception conditions - ✅ Subprocess cleanup completes within reasonable timeouts - ✅ Process groups prevent orphaned child processes @@ -91,4 +91,4 @@ Issue #2 identified potential ghost process (orphaned subprocess) problems in th 4. **Concurrent execution**: Multiple executors cleanup properly 5. **Error conditions**: Cleanup works even when operations fail -The implementation successfully addresses all concerns raised in Issue #2 and provides a robust, production-ready solution for subprocess management. \ No newline at end of file +The implementation successfully addresses all concerns raised in Issue #2 and provides a robust, production-ready solution for subprocess management. diff --git a/.tmp/pyodide_architecture.md b/.tmp/pyodide_architecture.md index 7d85adc..15e1be4 100644 --- a/.tmp/pyodide_architecture.md +++ b/.tmp/pyodide_architecture.md @@ -12,13 +12,13 @@ interface PyodideExecutor { // Pyodide環境の初期化(遅延読み込み) initialize(): Promise - + // MIPコード実行(PuLP対応) executeMIPCode(code: string, options: ExecutionOptions): Promise - + // ライブラリ検出 detectLibrary(code: string): MIPLibrary - + // パフォーマンス最適化 warmup(): Promise dispose(): void @@ -50,12 +50,12 @@ class ProblemExtractor: return ProblemInfo(format='mps', content=pyodide_globals['__mps_content__']) elif '__lp_content__' in pyodide_globals: return ProblemInfo(format='lp', content=pyodide_globals['__lp_content__']) - + # 2. 自動検出方式 problems = self._detect_problems(pyodide_globals) if problems: return self._generate_format(problems[0]) - + return ProblemInfo(format=None, content=None) ``` @@ -68,11 +68,11 @@ class MIPMCPTools: # ツール名: execute_mip_code (汎用的) # 実装: PuLPサポート、将来的に他ライブラリ対応可能 async def execute_mip_code(self, code: str) -> ExecutionResult - - # ツール名: validate_mip_code (汎用的) + + # ツール名: validate_mip_code (汎用的) # 実装: 現在はPuLP構文検証 async def validate_mip_code(self, code: str) -> ValidationResult - + # その他既存ツールも同様に汎用的な名前を維持 ``` @@ -84,7 +84,7 @@ class MIPMCPTools: ```typescript class PyodideExecutor { private pyodide: PyodideInterface | null = null - + async initialize() { if (!this.pyodide) { this.pyodide = await loadPyodide() @@ -92,7 +92,7 @@ class MIPMCPTools: await this.installMIPLibraries() } } - + private async installMIPLibraries() { await this.pyodide.runPythonAsync(` import micropip @@ -112,7 +112,7 @@ class MIPMCPTools: 'pulp': pulp, # その他必要最小限のみ } - + try: exec(user_code, safe_globals) return { @@ -122,7 +122,7 @@ class MIPMCPTools: } except Exception as e: return { - 'success': False, + 'success': False, 'globals': {}, 'error': str(e) } @@ -136,7 +136,7 @@ class MIPMCPTools: import pulp prob = pulp.LpProblem("example", pulp.LpMaximize) # ... 問題定義 ... - + # LP形式を変数に設定(推奨方式) prob.writeLP("/tmp/problem.lp") with open("/tmp/problem.lp", "r") as f: @@ -159,7 +159,7 @@ class MIPMCPTools: ```typescript class PyodidePool { private instances: PyodideExecutor[] = [] - + async warmup(count: number = 3) { for (let i = 0; i < count; i++) { const executor = new PyodideExecutor() @@ -167,7 +167,7 @@ class MIPMCPTools: this.instances.push(executor) } } - + async getExecutor(): Promise { return this.instances.pop() || new PyodideExecutor() } @@ -182,7 +182,7 @@ class MIPMCPTools: ## 移行計画 ### Step 1: 新しいPyodide実行エンジン実装 -- `src/mip_mcp/executor/pyodide_executor.py` +- `src/mip_mcp/executor/pyodide_executor.py` - Node.js統合とPyodide初期化 ### Step 2: 既存ハンドラーの更新 @@ -224,4 +224,4 @@ class MIPMCPTools: ### 対策 - インスタンスプール - メモリ監視 -- グレースフルフォールバック(RestrictedPython) \ No newline at end of file +- グレースフルフォールバック(RestrictedPython) diff --git a/.tmp/requirements.md b/.tmp/requirements.md index 5534797..ecc3100 100644 --- a/.tmp/requirements.md +++ b/.tmp/requirements.md @@ -170,4 +170,4 @@ LLMからのPuLPコードを受信し、MPS/LPファイルを生成してMIP最 - pyscipoptとの互換性確認 - MCPクライアントとの連携テスト - Cloud Runの制限事項とベストプラクティス -- コンテナ環境でのPuLP + pyscipoptの動作確認 \ No newline at end of file +- コンテナ環境でのPuLP + pyscipoptの動作確認 diff --git a/.tmp/solver_selection_design.md b/.tmp/solver_selection_design.md index c1daeac..4f821c1 100644 --- a/.tmp/solver_selection_design.md +++ b/.tmp/solver_selection_design.md @@ -34,4 +34,4 @@ class SolverFactory: 1. SolverFactoryクラスの実装 2. execute_mip_code_handlerの更新(solverパラメータ追加) 3. MCP tool定義の更新 -4. テストの追加 \ No newline at end of file +4. テストの追加 diff --git a/.tmp/tasks.md b/.tmp/tasks.md index 6cfd1d9..ab9e09c 100644 --- a/.tmp/tasks.md +++ b/.tmp/tasks.md @@ -18,8 +18,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] 必要な依存関係のインストール確認 - [ ] 開発環境のセットアップ -**優先度**: High -**見積時間**: 0.5h +**優先度**: High +**見積時間**: 0.5h **成果物**: 更新されたpyproject.toml #### T002: プロジェクト構造の作成 @@ -27,8 +27,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] 基本的な__init__.pyファイル作成 - [ ] 設定ファイルディレクトリの作成 -**優先度**: High -**見積時間**: 0.5h +**優先度**: High +**見積時間**: 0.5h **成果物**: 基本プロジェクト構造 ### 2.2 データモデル定義 @@ -38,8 +38,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] models/config.py: 設定データモデル実装 - [ ] Pydanticモデルの基本検証 -**優先度**: High -**見積時間**: 1h +**優先度**: High +**見積時間**: 1h **成果物**: データモデルクラス ### 2.3 設定管理システム @@ -49,8 +49,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] config/default.yaml作成 - [ ] 環境変数サポート実装 -**優先度**: High -**見積時間**: 1h +**優先度**: High +**見積時間**: 1h **成果物**: 設定管理システム #### T005: ログシステム実装 @@ -58,8 +58,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] 構造化ログ出力設定 - [ ] ログレベル設定 -**優先度**: Medium -**見積時間**: 0.5h +**優先度**: Medium +**見積時間**: 0.5h **成果物**: ログシステム ### 2.4 PuLPコード実行エンジン @@ -69,8 +69,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] ASTベースの危険コード検出 - [ ] SecurityError例外クラス定義 -**優先度**: High -**見積時間**: 2h +**優先度**: High +**見積時間**: 2h **成果物**: セキュアなコード検証機能 #### T007: PuLP実行環境実装 @@ -78,8 +78,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] PuLPライブラリの安全な実行環境 - [ ] 実行タイムアウト制御 -**優先度**: High -**見積時間**: 2h +**優先度**: High +**見積時間**: 2h **成果物**: PuLP実行エンジン #### T008: MPS/LP生成機能実装 @@ -87,8 +87,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] PuLPモデルからLP形式生成 - [ ] 生成ファイルの妥当性検証 -**優先度**: High -**見積時間**: 1.5h +**優先度**: High +**見積時間**: 1.5h **成果物**: ファイル生成機能 ### 2.5 ソルバー統合 @@ -98,8 +98,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] 抽象ソルバーインターフェース定義 - [ ] エラーハンドリング基盤 -**優先度**: High -**見積時間**: 1h +**優先度**: High +**見積時間**: 1h **成果物**: ソルバー抽象化基盤 #### T010: SCIPソルバー実装 @@ -108,8 +108,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] pyscipoptによる最適化実行 - [ ] 結果抽出機能 -**優先度**: High -**見積時間**: 2h +**優先度**: High +**見積時間**: 2h **成果物**: SCIP統合機能 ### 2.6 MCPサーバー実装 @@ -119,8 +119,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] PuLPコード実行ハンドラー - [ ] エラーハンドリングとレスポンス形成 -**優先度**: High -**見積時間**: 1.5h +**優先度**: High +**見積時間**: 1.5h **成果物**: MCP実行ハンドラー #### T012: MCPサーバーメイン実装 @@ -128,8 +128,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] FastMCPサーバー設定 - [ ] ハンドラー登録とルーティング -**優先度**: High -**見積時間**: 1h +**優先度**: High +**見積時間**: 1h **成果物**: MCPサーバー本体 #### T013: アプリケーションエントリーポイント @@ -137,8 +137,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] main()関数実装 - [ ] コマンドライン起動機能 -**優先度**: High -**見積時間**: 0.5h +**優先度**: High +**見積時間**: 0.5h **成果物**: 実行可能アプリケーション ## 3. Phase 2: MPS/LPファイル生成・処理機能強化 @@ -150,8 +150,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] ソルバー固有の制限事項チェック - [ ] 詳細なエラーレポート機能 -**優先度**: Medium -**見積時間**: 1.5h +**優先度**: Medium +**見積時間**: 1.5h **成果物**: 強化された検証機能 #### T015: ファイル最適化機能 @@ -159,8 +159,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] 冗長な制約の検出と削除 - [ ] 変数名の正規化 -**優先度**: Low -**見積時間**: 2h +**優先度**: Low +**見積時間**: 2h **成果物**: ファイル最適化機能 ### 3.2 エラーハンドリング強化 @@ -170,8 +170,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] ソルバーエラーの分類と説明 - [ ] 修正提案機能 -**優先度**: Medium -**見積時間**: 2h +**優先度**: Medium +**見積時間**: 2h **成果物**: 高度なエラー診断機能 #### T017: カスタム例外クラス @@ -179,8 +179,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] SolverError, ParseError等の専用例外 - [ ] 例外階層の整理 -**優先度**: Medium -**見積時間**: 1h +**優先度**: Medium +**見積時間**: 1h **成果物**: 例外クラス体系 ## 4. Phase 3: Cloud Run対応 + コンテナ化 @@ -192,8 +192,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] Python環境とソルバー依存関係 - [ ] セキュリティ最適化 -**優先度**: High -**見積時間**: 2h +**優先度**: High +**見積時間**: 2h **成果物**: Dockerfile #### T019: Docker環境テスト @@ -201,8 +201,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] コンテナ内でのPuLP + pyscipopt動作確認 - [ ] メモリ・CPU使用量測定 -**優先度**: High -**見積時間**: 1h +**優先度**: High +**見積時間**: 1h **成果物**: 動作確認済みコンテナ ### 4.2 Cloud Run対応 @@ -212,8 +212,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] 環境変数設定 - [ ] リソース制限設定 -**優先度**: High -**見積時間**: 1h +**優先度**: High +**見積時間**: 1h **成果物**: Cloud Run設定ファイル #### T021: ヘルスチェック実装 @@ -221,8 +221,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] システム状態監視機能 - [ ] 依存関係チェック -**優先度**: High -**見積時間**: 1h +**優先度**: High +**見積時間**: 1h **成果物**: ヘルスチェック機能 #### T022: Cloud Runデプロイメント @@ -230,8 +230,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] 動作確認テスト - [ ] パフォーマンス測定 -**優先度**: High -**見積時間**: 1h +**優先度**: High +**見積時間**: 1h **成果物**: デプロイ済みサービス ## 5. テスト実装 @@ -243,8 +243,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] ソルバー統合テスト - [ ] データモデルテスト -**優先度**: High -**見積時間**: 3h +**優先度**: High +**見積時間**: 3h **成果物**: ユニットテストスイート #### T024: セキュリティテスト @@ -252,8 +252,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] サンドボックス機能テスト - [ ] 入力検証テスト -**優先度**: High -**見積時間**: 2h +**優先度**: High +**見積時間**: 2h **成果物**: セキュリティテストスイート ### 5.2 統合テスト @@ -263,8 +263,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] MCPクライアント統合テスト - [ ] エラーケーステスト -**優先度**: High -**見積時間**: 2h +**優先度**: High +**見積時間**: 2h **成果物**: 統合テストスイート #### T026: Cloud Run統合テスト @@ -272,8 +272,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] Cloud Run環境でのテスト - [ ] パフォーマンステスト -**優先度**: Medium -**見積時間**: 1.5h +**優先度**: Medium +**見積時間**: 1.5h **成果物**: クラウド統合テスト ## 6. ドキュメント作成 @@ -285,8 +285,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] リクエスト・レスポンス形式 - [ ] エラーコード一覧 -**優先度**: Medium -**見積時間**: 2h +**優先度**: Medium +**見積時間**: 2h **成果物**: API仕様書 #### T028: 開発環境セットアップガイド @@ -294,8 +294,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] 開発環境構築手順 - [ ] テスト実行方法 -**優先度**: Medium -**見積時間**: 1h +**優先度**: Medium +**見積時間**: 1h **成果物**: 開発者ガイド ### 6.2 運用ドキュメント @@ -305,8 +305,8 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] 設定オプション説明 - [ ] トラブルシューティング -**優先度**: Medium -**見積時間**: 1.5h +**優先度**: Medium +**見積時間**: 1.5h **成果物**: デプロイメントガイド #### T030: 使用例とチュートリアル @@ -314,21 +314,21 @@ LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Result - [ ] 典型的な最適化問題の解決例 - [ ] トラブルシューティング事例 -**優先度**: Low -**見積時間**: 2h +**優先度**: Low +**見積時間**: 2h **成果物**: チュートリアル ## 7. 実装順序とマイルストーン ### Milestone 1: 基本機能完成 (Phase 1) -**期間**: 2-3日 +**期間**: 2-3日 **成果物**: 動作するMCP Server + PuLP + pyscipopt統合 **クリティカルパス**: T001 → T002 → T003 → T006 → T007 → T008 → T010 → T011 → T012 → T013 ### Milestone 2: 機能強化 (Phase 2) -**期間**: 1-2日 +**期間**: 1-2日 **成果物**: エラーハンドリング強化、ファイル処理改善 **並行実装可能**: @@ -336,14 +336,14 @@ T001 → T002 → T003 → T006 → T007 → T008 → T010 → T011 → T012 → - T016, T017 (エラーハンドリング) ### Milestone 3: クラウド対応 (Phase 3) -**期間**: 1-2日 +**期間**: 1-2日 **成果物**: Cloud Run対応完了 **実装順序**: T018 → T019 → T020 → T021 → T022 ### Milestone 4: 品質保証 -**期間**: 2-3日 +**期間**: 2-3日 **成果物**: テスト完備、ドキュメント整備 **並行実装可能**: @@ -377,4 +377,4 @@ T018 → T019 → T020 → T021 → T022 ### セキュリティ - 危険なコードの検出率 > 95% - サンドボックス環境での隔離 -- 入力検証完備 \ No newline at end of file +- 入力検証完備 diff --git a/Makefile b/Makefile index 886b144..f9b9db9 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Makefile for MIP-MCP project -.PHONY: help install test test-unit test-integration test-basic coverage lint format clean dev-install +.PHONY: help install test test-unit test-integration test-basic coverage lint format clean dev-install pre-commit-install pre-commit-run pre-commit-update # Default target help: @@ -14,6 +14,9 @@ help: @echo " coverage Run tests with coverage report" @echo " lint Run linting with ruff" @echo " format Format code with ruff" + @echo " pre-commit-install Install pre-commit hooks" + @echo " pre-commit-run Run pre-commit on all files" + @echo " pre-commit-update Update pre-commit hook versions" @echo " clean Clean cache and build artifacts" # Installation @@ -58,6 +61,16 @@ lint-check: uv run ruff check src/ tests/ uv run ruff format --check src/ tests/ +# Pre-commit hooks +pre-commit-install: + uv run pre-commit install + +pre-commit-run: + uv run pre-commit run --all-files + +pre-commit-update: + uv run pre-commit autoupdate + # Cleanup clean: find . -type d -name __pycache__ -delete @@ -77,4 +90,4 @@ check: @echo "Running development checks..." $(MAKE) format $(MAKE) lint - $(MAKE) test-basic \ No newline at end of file + $(MAKE) test-basic diff --git a/README.md b/README.md index b315e55..dfb461a 100644 --- a/README.md +++ b/README.md @@ -224,17 +224,18 @@ uv run pytest tests/unit/test_handlers.py -v ### Code Quality ```bash -# Format code -uv run black src/ tests/ +# Format code and fix issues +make format -# Sort imports -uv run isort src/ tests/ +# Run linting checks +make lint -# Type checking -uv run mypy src/ +# Check code formatting without changes +make lint-check -# Run linting (if configured) -uv run ruff check src/ tests/ +# Or use Ruff directly +uv run ruff format src/ tests/ +uv run ruff check --fix src/ tests/ ``` ### Development Setup @@ -243,8 +244,39 @@ uv run ruff check src/ tests/ # Install development dependencies uv sync --group dev -# Install pre-commit hooks (if using) -pre-commit install +# Install pre-commit hooks (recommended) +make pre-commit-install + +# OR use pre-commit directly +uv run pre-commit install +``` + +### Pre-commit Hooks + +This project uses pre-commit hooks to automatically check code quality before commits: + +```bash +# Install pre-commit hooks +make pre-commit-install + +# Run pre-commit on all files +make pre-commit-run + +# Update hook versions +make pre-commit-update +``` + +The pre-commit hooks will automatically: +- Format code with Ruff +- Fix linting issues +- Check for trailing whitespace +- Validate YAML and TOML files +- Check for merge conflicts +- Remove debug statements + +If pre-commit hooks fail, fix the issues and commit again. To skip hooks in emergencies: +```bash +git commit --no-verify -m "Emergency commit" ``` ## Architecture diff --git a/docs/FUNCTIONAL_REQUIREMENTS.md b/docs/FUNCTIONAL_REQUIREMENTS.md index c7b14e5..8ddfe40 100644 --- a/docs/FUNCTIONAL_REQUIREMENTS.md +++ b/docs/FUNCTIONAL_REQUIREMENTS.md @@ -7,7 +7,7 @@ - **Input**: Natural language business problem description, context, constraints - **Processing**: Semantic parsing, constraint extraction, template matching - **Output**: Mathematical MIP model (variables, constraints, objective function) -- **Acceptance Criteria**: +- **Acceptance Criteria**: - Support for common business domains (supply chain, scheduling, resource allocation) - 90% accuracy for well-defined problems - Generate valid mathematical formulations @@ -212,4 +212,4 @@ ### Reliability - Graceful handling of edge cases - Fallback mechanisms for failures -- Consistent behavior across domains \ No newline at end of file +- Consistent behavior across domains diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 5590032..b59959b 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -32,7 +32,7 @@ Transform optimization from a specialist tool to an accessible business solution - Mathematical constraint extraction - Variable and objective function definition -2. **validate_and_debug_model** +2. **validate_and_debug_model** - Mathematical formulation validation - Infeasibility detection and analysis - Error interpretation and suggestions @@ -402,4 +402,4 @@ mip-mcp/ This implementation plan provides a comprehensive roadmap for building an LLM-Enhanced MIP system that addresses real-world optimization barriers. Through a phased approach focusing on core value delivery, technical excellence, and user experience, the system will transform optimization from a specialist tool to an accessible business capability. -The plan balances innovation with practical implementation considerations, ensuring deliverable value at each phase while building toward transformative AI-enhanced optimization capabilities. Success depends on disciplined execution, continuous user feedback, and maintaining focus on solving real business problems through intelligent automation. \ No newline at end of file +The plan balances innovation with practical implementation considerations, ensuring deliverable value at each phase while building toward transformative AI-enhanced optimization capabilities. Success depends on disciplined execution, continuous user feedback, and maintaining focus on solving real business problems through intelligent automation. diff --git a/docs/LLM_ENHANCED_MIP_RESEARCH.md b/docs/LLM_ENHANCED_MIP_RESEARCH.md index ba0fac9..5b596d4 100644 --- a/docs/LLM_ENHANCED_MIP_RESEARCH.md +++ b/docs/LLM_ENHANCED_MIP_RESEARCH.md @@ -255,4 +255,4 @@ Mixed Integer Programming (MIP) remains underutilized in real-world applications LLM-enhanced MIP represents a paradigm shift in optimization accessibility and effectiveness. By addressing the fundamental barriers to MIP adoption through intelligent automation, natural language interfaces, and seamless integration capabilities, this approach can unlock the value of optimization for a much broader range of organizations and applications. -The proposed MCP functions provide a comprehensive framework for implementing this vision, with a clear implementation roadmap that delivers value incrementally while building toward transformative capabilities. Success depends on careful attention to validation, user experience, and the balance between automation and human expertise. \ No newline at end of file +The proposed MCP functions provide a comprehensive framework for implementing this vision, with a clear implementation roadmap that delivers value incrementally while building toward transformative capabilities. Success depends on careful attention to validation, user experience, and the balance between automation and human expertise. diff --git a/docs/SYSTEM_REQUIREMENTS.md b/docs/SYSTEM_REQUIREMENTS.md index 8a6a7f6..376cc03 100644 --- a/docs/SYSTEM_REQUIREMENTS.md +++ b/docs/SYSTEM_REQUIREMENTS.md @@ -362,4 +362,4 @@ The LLM-Enhanced MIP system is a distributed, cloud-native application that inte - **Service Dependencies**: Third-party service risk assessment - **Single Points of Failure**: Redundancy for critical components - **Capacity Planning**: Proactive capacity management -- **Performance Degradation**: Performance monitoring and alerting \ No newline at end of file +- **Performance Degradation**: Performance monitoring and alerting diff --git a/docs/TECHNICAL_REQUIREMENTS.md b/docs/TECHNICAL_REQUIREMENTS.md index 16329cb..2cf6f0c 100644 --- a/docs/TECHNICAL_REQUIREMENTS.md +++ b/docs/TECHNICAL_REQUIREMENTS.md @@ -269,4 +269,4 @@ - Modular architecture with clear interfaces - Comprehensive unit and integration tests - Automated deployment pipelines -- Clear documentation and code comments \ No newline at end of file +- Clear documentation and code comments diff --git a/docs/USER_REQUIREMENTS.md b/docs/USER_REQUIREMENTS.md index b3ed0b4..64bbd43 100644 --- a/docs/USER_REQUIREMENTS.md +++ b/docs/USER_REQUIREMENTS.md @@ -45,8 +45,8 @@ ### Business Analyst Use Cases #### UC-001: Supply Chain Optimization -**As a** supply chain analyst -**I want to** optimize inventory distribution across warehouses +**As a** supply chain analyst +**I want to** optimize inventory distribution across warehouses **So that** I can minimize costs while meeting demand requirements **Acceptance Criteria:** @@ -56,8 +56,8 @@ - Export results to Excel for presentation #### UC-002: Resource Allocation -**As a** project manager -**I want to** allocate team members to projects optimally +**As a** project manager +**I want to** allocate team members to projects optimally **So that** I can maximize productivity while respecting skill requirements **Acceptance Criteria:** @@ -69,8 +69,8 @@ ### Operations Research Practitioner Use Cases #### UC-003: Model Development Acceleration -**As an** OR analyst -**I want to** quickly formulate complex optimization models +**As an** OR analyst +**I want to** quickly formulate complex optimization models **So that** I can focus on analysis rather than implementation **Acceptance Criteria:** @@ -80,8 +80,8 @@ - Generate code for multiple solver platforms #### UC-004: Performance Optimization -**As an** optimization engineer -**I want to** improve solver performance on large models +**As an** optimization engineer +**I want to** improve solver performance on large models **So that** I can solve problems within acceptable timeframes **Acceptance Criteria:** @@ -93,8 +93,8 @@ ### Data Scientist Use Cases #### UC-005: Workflow Integration -**As a** data scientist -**I want to** integrate optimization into my ML pipeline +**As a** data scientist +**I want to** integrate optimization into my ML pipeline **So that** I can create end-to-end decision support systems **Acceptance Criteria:** @@ -104,8 +104,8 @@ - Automate recurring optimization workflows #### UC-006: Experiment Management -**As a** research scientist -**I want to** run multiple optimization experiments +**As a** research scientist +**I want to** run multiple optimization experiments **So that** I can compare different approaches systematically **Acceptance Criteria:** @@ -117,8 +117,8 @@ ### Executive Use Cases #### UC-007: Strategic Decision Support -**As an** executive -**I want to** understand optimization recommendations +**As an** executive +**I want to** understand optimization recommendations **So that** I can make informed strategic decisions **Acceptance Criteria:** @@ -128,8 +128,8 @@ - Access results on mobile devices #### UC-008: Performance Monitoring -**As a** department head -**I want to** monitor optimization system performance +**As a** department head +**I want to** monitor optimization system performance **So that** I can ensure business objectives are met **Acceptance Criteria:** @@ -340,4 +340,4 @@ - 20-40% improvement in optimization quality - 60% reduction in model development time - 90% reduction in debugging time -- 85% accuracy in automated model generation \ No newline at end of file +- 85% accuracy in automated model generation diff --git a/pyproject.toml b/pyproject.toml index 1893aa1..2a711e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dev = [ "pytest-asyncio>=1.1.0", "pytest-cov>=6.2.1", "ruff>=0.12.7", + "pre-commit>=4.0.0", ] [tool.ruff] @@ -83,6 +84,9 @@ ignore = [ "S101", # Use of assert detected "PLR2004", # Magic value used in comparison ] +"src/mip_mcp/utils/library_detector.py" = [ + "N802", # Function name should be lowercase (AST visitor methods exception) +] [tool.ruff.format] # コードフォーマット設定 @@ -90,4 +94,3 @@ quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" - diff --git a/pytest.ini b/pytest.ini index 26786e6..fd74ad5 100644 --- a/pytest.ini +++ b/pytest.ini @@ -20,18 +20,18 @@ markers = e2e: End-to-end tests # Test discovery and execution options -addopts = +addopts = --strict-markers --strict-config --verbose --tb=short --color=yes --durations=10 - + # Coverage configuration [tool:coverage:run] source = src/mip_mcp -omit = +omit = */tests/* */test_* */__pycache__/* @@ -55,4 +55,4 @@ show_missing = true precision = 2 [tool:coverage:html] -directory = htmlcov \ No newline at end of file +directory = htmlcov diff --git a/src/mip_mcp/config/default.yaml b/src/mip_mcp/config/default.yaml index 54bf0ba..e80b1cb 100644 --- a/src/mip_mcp/config/default.yaml +++ b/src/mip_mcp/config/default.yaml @@ -1,21 +1,21 @@ server: name: "mip-mcp" version: "0.1.0" - + logging: level: "INFO" format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - + executor: enabled: true timeout: 300 # seconds for code execution memory_limit: "1GB" - + solvers: default: "scip" timeout: 3600 # seconds - + validation: max_variables: 100000 max_constraints: 100000 - max_code_length: 10000 # characters \ No newline at end of file + max_code_length: 10000 # characters diff --git a/tests/README.md b/tests/README.md index 54d5027..f270668 100644 --- a/tests/README.md +++ b/tests/README.md @@ -105,4 +105,4 @@ make coverage-all 2. 非同期テストのパフォーマンス最適化 3. 統合テスト環境の自動化 4. テストデータの管理改善 -5. セキュリティテストの強化 \ No newline at end of file +5. セキュリティテストの強化 diff --git a/uv.lock b/uv.lock index 7f2af75..f02514b 100644 --- a/uv.lock +++ b/uv.lock @@ -67,6 +67,15 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/2b/a8/050ab4f0c3d4c1b8aaa805f70e26e84d0e27004907c5b8ecc1d31815f92a/cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", size = 508501 } +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + [[package]] name = "click" version = "8.2.1" @@ -202,6 +211,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/1f/4b9f6986add9f6ff361c1bfffeb08fc2f2f6752f8adf8d4dcf0a988b6f28/cyclopts-3.22.3-py3-none-any.whl", hash = "sha256:771ae584868c8beeac74184a96e9fad3726c787b17e47a6f0d5f42cece1df57a", size = 84941 }, ] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047 }, +] + [[package]] name = "dnspython" version = "2.7.0" @@ -275,6 +293,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/05/4958cccbe862958d862b6a15f2d10d2f5ec3c411268dcb131a433e5e7a0d/fastmcp-2.10.6-py3-none-any.whl", hash = "sha256:9782416a8848cc0f4cfcc578e5c17834da620bef8ecf4d0daabf5dd1272411a2", size = 202613 }, ] +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, +] + [[package]] name = "h11" version = "0.16.0" @@ -321,6 +348,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054 }, ] +[[package]] +name = "identify" +version = "2.6.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145 }, +] + [[package]] name = "idna" version = "3.10" @@ -430,6 +466,7 @@ dev = [ [package.dev-dependencies] dev = [ + { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -450,12 +487,22 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "pre-commit", specifier = ">=4.0.0" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-asyncio", specifier = ">=1.1.0" }, { name = "pytest-cov", specifier = ">=6.2.1" }, { name = "ruff", specifier = ">=0.12.7" }, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + [[package]] name = "numpy" version = "2.3.2" @@ -540,6 +587,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, ] +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -549,6 +605,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] +[[package]] +name = "pre-commit" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, +] + [[package]] name = "pulp" version = "3.2.2" @@ -987,3 +1059,17 @@ sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8 wheels = [ { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406 }, ] + +[[package]] +name = "virtualenv" +version = "20.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/96/0834f30fa08dca3738614e6a9d42752b6420ee94e58971d702118f7cfd30/virtualenv-20.32.0.tar.gz", hash = "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0", size = 6076970 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/c6/f8f28009920a736d0df434b52e9feebfb4d702ba942f15338cb4a83eafc1/virtualenv-20.32.0-py3-none-any.whl", hash = "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56", size = 6057761 }, +] From 1ec6154d70ed2a4d4fdffed0c2c5031bc569d4fd Mon Sep 17 00:00:00 2001 From: ohtaman Date: Sun, 3 Aug 2025 08:59:02 +0900 Subject: [PATCH 2/4] Fix Issue #7: Implement automatic Pyodide download for uvx users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major enhancement to resolve Pyodide initialization failures: ## Problem Solved - uvx/pipx users experienced "Pyodide module not found" errors - No mechanism to install npm dependencies during Python package installation - Manual npm install requirement created friction for users ## Solution: Hybrid Runtime Download System ### Core Features - **Automatic fallback**: Downloads pyodide from npm registry if not found locally - **Smart caching**: Stores files in ~/.cache/mip-mcp/pyodide/ for fast subsequent runs - **Universal compatibility**: Works with uvx, pip, pipx, conda, any installation method - **No npm dependency**: Users don't need Node.js/npm installed - **Robust error handling**: Clear messages with manual installation fallback ### Technical Implementation - Add _download_pyodide() method for npm registry download (12MB) - Add _extract_pyodide_package() for selective file extraction - Add _find_or_download_pyodide_path() as unified entry point - Cross-platform cache directory handling (Windows/Unix) - Async download to prevent event loop blocking - Extract only required files: pyodide.js, pyodide.asm.wasm, python_stdlib.zip, etc. ### User Experience Improvements - Fast Python package installation (no build-time npm requirements) - One-time 12MB download cached for future use - Seamless experience: uvx mip-mcp "just works" - Graceful degradation if download fails with helpful error messages ### Architecture Benefits - Maintains existing local pyodide detection (backward compatible) - No changes to build system or dependencies required - No npm requirement at install time - Better than setuptools build hooks (uvx/pipx compatible) ### Testing Results - ✅ Download from npm registry successful - ✅ All required files extracted and cached properly - ✅ Pyodide initialization successful with downloaded files - ✅ Complete MCP workflow verified: Problem generation + SCIP solving - ✅ Cache reuse working for subsequent executions Resolves #7 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/mip_mcp/executor/pyodide_executor.py | 114 ++++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/src/mip_mcp/executor/pyodide_executor.py b/src/mip_mcp/executor/pyodide_executor.py index 0bda161..25af94a 100644 --- a/src/mip_mcp/executor/pyodide_executor.py +++ b/src/mip_mcp/executor/pyodide_executor.py @@ -4,11 +4,13 @@ import builtins import contextlib import json +import os import tempfile import time from collections.abc import Callable from pathlib import Path from typing import Any +from urllib.request import urlretrieve from ..models.responses import SolverProgress from ..utils.library_detector import MIPLibrary, MIPLibraryDetector @@ -156,11 +158,12 @@ async def _initialize_pyodide(self) -> None: try: logger.info("Initializing Pyodide environment...") - # Find pyodide path first - pyodide_path = await self._find_pyodide_path() + # Find pyodide path first (with automatic download fallback) + pyodide_path = await self._find_or_download_pyodide_path() if not pyodide_path: raise RuntimeError( - "Pyodide module not found. Please install: npm install pyodide" + "Pyodide initialization failed: Could not find or download Pyodide. " + "Please ensure internet access or manually install: npm install pyodide" ) logger.info(f"Found Pyodide at: {pyodide_path}") @@ -292,6 +295,111 @@ async def _find_pyodide_path(self) -> str | None: logger.error(f"Failed to find pyodide path: {e}") return None + def _get_pyodide_cache_dir(self) -> Path: + """Get the pyodide cache directory.""" + # Use user data directory for caching + if os.name == "nt": # Windows + cache_dir = Path(os.environ.get("APPDATA", "")) / "mip-mcp" / "pyodide" + else: # Unix-like + cache_dir = Path.home() / ".cache" / "mip-mcp" / "pyodide" + + cache_dir.mkdir(parents=True, exist_ok=True) + return cache_dir + + async def _download_pyodide(self) -> str | None: + """Download pyodide from npm registry and cache it.""" + try: + logger.info("Pyodide not found locally, downloading...") + + cache_dir = self._get_pyodide_cache_dir() + pyodide_js_path = cache_dir / "pyodide.js" + + # Check if already cached + if pyodide_js_path.exists(): + logger.info(f"Using cached pyodide at: {pyodide_js_path}") + return str(pyodide_js_path) + + # Download pyodide package from npm registry + npm_url = "https://registry.npmjs.org/pyodide/-/pyodide-0.28.0.tgz" + logger.info("Downloading pyodide package...") + + # Download to temporary file + with tempfile.NamedTemporaryFile(suffix=".tgz", delete=False) as tmp_file: + temp_path = tmp_file.name + + try: + # Run download in executor to avoid blocking event loop + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, urlretrieve, npm_url, temp_path) + + # Extract the package + logger.info("Extracting pyodide package...") + await loop.run_in_executor( + None, self._extract_pyodide_package, temp_path, cache_dir + ) + + # Verify extraction + if pyodide_js_path.exists(): + logger.info( + f"Successfully downloaded and cached pyodide at: {pyodide_js_path}" + ) + return str(pyodide_js_path) + else: + logger.error("Failed to extract pyodide.js from downloaded package") + return None + + finally: + # Clean up temporary file + with contextlib.suppress(OSError): + Path(temp_path).unlink() + + except Exception as e: + logger.error(f"Failed to download pyodide: {e}") + return None + + def _extract_pyodide_package(self, tar_path: str, extract_dir: Path) -> None: + """Extract pyodide package from tar.gz file.""" + import tarfile + + with tarfile.open(tar_path, "r:gz") as tar: + # Extract only the files we need + needed_files = [ + "package/pyodide.js", + "package/pyodide.asm.js", + "package/pyodide.asm.wasm", + "package/pyodide-lock.json", + "package/python_stdlib.zip", + ] + + for member in tar.getmembers(): + if member.name in needed_files: + # Extract to cache directory, removing "package/" prefix + target_name = member.name.replace("package/", "") + member.name = target_name + tar.extract(member, extract_dir) + + async def _find_or_download_pyodide_path(self) -> str | None: + """Find pyodide path, downloading if necessary.""" + # First try to find existing installation + path = await self._find_pyodide_path() + if path: + return path + + # If not found, try to download it + logger.info("Pyodide not found in local installations, attempting download...") + downloaded_path = await self._download_pyodide() + + if downloaded_path: + return downloaded_path + + # If download failed, provide helpful error message + logger.error( + "Could not find or download pyodide. Please install manually:\n" + " npm install pyodide\n" + "Or ensure you have internet access for automatic download." + ) + return None + def _get_pyodide_script(self, pyodide_path: str) -> str: """Get the Node.js script for Pyodide execution.""" script = """ From a9c6f43bbaf3a144343c97f245e0885ecccc6a3d Mon Sep 17 00:00:00 2001 From: ohtaman Date: Sun, 3 Aug 2025 09:28:57 +0900 Subject: [PATCH 3/4] Implement build-time pyodide bundling for PyPI distribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace runtime download approach with build-time bundling for better PyPI compatibility: ## Key Changes ### Build System (pyproject.toml) - Add hatchling shared-data configuration to bundle node_modules/pyodide files - Bundle 5 essential pyodide files (~12MB total) in wheel distribution: - pyodide.js, pyodide.asm.js, pyodide.asm.wasm - pyodide-lock.json, python_stdlib.zip ### Pyodide Path Detection (pyodide_executor.py) - Add _check_bundled_pyodide() method to detect wheel-bundled files - Update _find_pyodide_path() to check bundled location first - Maintain backward compatibility with npm install workflow ## Benefits for PyPI Distribution ✅ **Self-contained wheels**: Perfect for `uv tool run mip-mcp` ✅ **Instant execution**: No runtime downloads or network dependencies ✅ **Offline capable**: Works in restricted environments/firewalls ✅ **Standard practice**: Follows industry patterns (PuLP, pyscipopt) ✅ **Clean repository**: No binary files in git (bundled only in wheels) ## Workflow Support **Development**: `npm install` → local node_modules (unchanged) **Distribution**: `uv build` → bundles pyodide in wheel → PyPI ready ## Technical Implementation - Use pkg_resources.resource_filename() for bundled file access - Bundled files stored in `mip_mcp/pyodide/` within wheel - Graceful fallback to npm-installed pyodide for development - Build process automatically includes existing node_modules files This approach provides the optimal user experience for PyPI distribution while maintaining clean development workflow and following established packaging patterns. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- pyproject.toml | 13 ++- src/mip_mcp/executor/pyodide_executor.py | 141 +++++------------------ 2 files changed, 41 insertions(+), 113 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2a711e7..8ee354d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,5 +92,14 @@ ignore = [ # コードフォーマット設定 quote-style = "double" indent-style = "space" -skip-magic-trailing-comma = false -line-ending = "auto" + +# Package data configuration for bundled pyodide files +[tool.hatch.build.targets.wheel] +packages = ["src/mip_mcp"] + +[tool.hatch.build.targets.wheel.shared-data] +"node_modules/pyodide/pyodide.js" = "mip_mcp/pyodide/pyodide.js" +"node_modules/pyodide/pyodide.asm.js" = "mip_mcp/pyodide/pyodide.asm.js" +"node_modules/pyodide/pyodide.asm.wasm" = "mip_mcp/pyodide/pyodide.asm.wasm" +"node_modules/pyodide/pyodide-lock.json" = "mip_mcp/pyodide/pyodide-lock.json" +"node_modules/pyodide/python_stdlib.zip" = "mip_mcp/pyodide/python_stdlib.zip" diff --git a/src/mip_mcp/executor/pyodide_executor.py b/src/mip_mcp/executor/pyodide_executor.py index 25af94a..b21f07d 100644 --- a/src/mip_mcp/executor/pyodide_executor.py +++ b/src/mip_mcp/executor/pyodide_executor.py @@ -4,13 +4,11 @@ import builtins import contextlib import json -import os import tempfile import time from collections.abc import Callable from pathlib import Path from typing import Any -from urllib.request import urlretrieve from ..models.responses import SolverProgress from ..utils.library_detector import MIPLibrary, MIPLibraryDetector @@ -158,12 +156,11 @@ async def _initialize_pyodide(self) -> None: try: logger.info("Initializing Pyodide environment...") - # Find pyodide path first (with automatic download fallback) - pyodide_path = await self._find_or_download_pyodide_path() + # Find pyodide path first + pyodide_path = await self._find_pyodide_path() if not pyodide_path: raise RuntimeError( - "Pyodide initialization failed: Could not find or download Pyodide. " - "Please ensure internet access or manually install: npm install pyodide" + "Pyodide module not found. Please install: npm install pyodide" ) logger.info(f"Found Pyodide at: {pyodide_path}") @@ -244,9 +241,36 @@ async def _wait_for_process_ready(self) -> None: f"Error waiting for Pyodide process readiness: {e}" ) from e + def _check_bundled_pyodide(self) -> str | None: + """Check for bundled pyodide installation (from wheel).""" + try: + # Check if bundled pyodide files exist (installed from wheel) + import pkg_resources + + try: + pyodide_js_path = pkg_resources.resource_filename( + "mip_mcp", "pyodide/pyodide.js" + ) + if Path(pyodide_js_path).exists(): + return pyodide_js_path + except (ImportError, FileNotFoundError): + pass + + return None + + except Exception as e: + logger.debug(f"Error checking bundled pyodide: {e}") + return None + async def _find_pyodide_path(self) -> str | None: """Find pyodide installation path.""" try: + # First check for bundled pyodide (from wheel installation) + bundled_path = self._check_bundled_pyodide() + if bundled_path: + logger.info(f"Using bundled pyodide at: {bundled_path}") + return bundled_path + # Try to find pyodide using Node.js proc = await asyncio.create_subprocess_exec( "node", @@ -295,111 +319,6 @@ async def _find_pyodide_path(self) -> str | None: logger.error(f"Failed to find pyodide path: {e}") return None - def _get_pyodide_cache_dir(self) -> Path: - """Get the pyodide cache directory.""" - # Use user data directory for caching - if os.name == "nt": # Windows - cache_dir = Path(os.environ.get("APPDATA", "")) / "mip-mcp" / "pyodide" - else: # Unix-like - cache_dir = Path.home() / ".cache" / "mip-mcp" / "pyodide" - - cache_dir.mkdir(parents=True, exist_ok=True) - return cache_dir - - async def _download_pyodide(self) -> str | None: - """Download pyodide from npm registry and cache it.""" - try: - logger.info("Pyodide not found locally, downloading...") - - cache_dir = self._get_pyodide_cache_dir() - pyodide_js_path = cache_dir / "pyodide.js" - - # Check if already cached - if pyodide_js_path.exists(): - logger.info(f"Using cached pyodide at: {pyodide_js_path}") - return str(pyodide_js_path) - - # Download pyodide package from npm registry - npm_url = "https://registry.npmjs.org/pyodide/-/pyodide-0.28.0.tgz" - logger.info("Downloading pyodide package...") - - # Download to temporary file - with tempfile.NamedTemporaryFile(suffix=".tgz", delete=False) as tmp_file: - temp_path = tmp_file.name - - try: - # Run download in executor to avoid blocking event loop - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, urlretrieve, npm_url, temp_path) - - # Extract the package - logger.info("Extracting pyodide package...") - await loop.run_in_executor( - None, self._extract_pyodide_package, temp_path, cache_dir - ) - - # Verify extraction - if pyodide_js_path.exists(): - logger.info( - f"Successfully downloaded and cached pyodide at: {pyodide_js_path}" - ) - return str(pyodide_js_path) - else: - logger.error("Failed to extract pyodide.js from downloaded package") - return None - - finally: - # Clean up temporary file - with contextlib.suppress(OSError): - Path(temp_path).unlink() - - except Exception as e: - logger.error(f"Failed to download pyodide: {e}") - return None - - def _extract_pyodide_package(self, tar_path: str, extract_dir: Path) -> None: - """Extract pyodide package from tar.gz file.""" - import tarfile - - with tarfile.open(tar_path, "r:gz") as tar: - # Extract only the files we need - needed_files = [ - "package/pyodide.js", - "package/pyodide.asm.js", - "package/pyodide.asm.wasm", - "package/pyodide-lock.json", - "package/python_stdlib.zip", - ] - - for member in tar.getmembers(): - if member.name in needed_files: - # Extract to cache directory, removing "package/" prefix - target_name = member.name.replace("package/", "") - member.name = target_name - tar.extract(member, extract_dir) - - async def _find_or_download_pyodide_path(self) -> str | None: - """Find pyodide path, downloading if necessary.""" - # First try to find existing installation - path = await self._find_pyodide_path() - if path: - return path - - # If not found, try to download it - logger.info("Pyodide not found in local installations, attempting download...") - downloaded_path = await self._download_pyodide() - - if downloaded_path: - return downloaded_path - - # If download failed, provide helpful error message - logger.error( - "Could not find or download pyodide. Please install manually:\n" - " npm install pyodide\n" - "Or ensure you have internet access for automatic download." - ) - return None - def _get_pyodide_script(self, pyodide_path: str) -> str: """Get the Node.js script for Pyodide execution.""" script = """ From 49d55115792ae4513ea92bfe3d294af552c351a9 Mon Sep 17 00:00:00 2001 From: ohtaman Date: Sun, 3 Aug 2025 09:33:22 +0900 Subject: [PATCH 4/4] Remove tmp files --- .gitignore | 3 + .tmp/design.md | 1002 ------------------------------- .tmp/issue2-solution-summary.md | 94 --- .tmp/pyodide_architecture.md | 227 ------- .tmp/requirements.md | 173 ------ .tmp/solver_selection_design.md | 37 -- .tmp/tasks.md | 380 ------------ 7 files changed, 3 insertions(+), 1913 deletions(-) delete mode 100644 .tmp/design.md delete mode 100644 .tmp/issue2-solution-summary.md delete mode 100644 .tmp/pyodide_architecture.md delete mode 100644 .tmp/requirements.md delete mode 100644 .tmp/solver_selection_design.md delete mode 100644 .tmp/tasks.md diff --git a/.gitignore b/.gitignore index 505a3b1..3152e68 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ wheels/ # Virtual environments .venv + +# tmp files +.tmp diff --git a/.tmp/design.md b/.tmp/design.md deleted file mode 100644 index a605bcf..0000000 --- a/.tmp/design.md +++ /dev/null @@ -1,1002 +0,0 @@ -# 技術設計書 - MIP MCP Server - -## 1. システムアーキテクチャ - -### 1.1 全体構成 - -``` -┌─────────────────┐ streamable HTTP ┌─────────────────┐ -│ LLM Client │ ◄──────────────────► │ MIP MCP │ -│ (Claude/GPT) │ │ Server │ -└─────────────────┘ └─────────────────┘ - │ - ▼ - ┌─────────────────┐ - │ Python Executor │ - │ (PuLP/pyomo/ │ - │ pyscipopt) │ - └─────────────────┘ - │ - ▼ - ┌─────────────────┐ - │ Solver Layer │ - │ (SCIP/Gurobi/ │ - │ CPLEX/CBC) │ - └─────────────────┘ -``` - -### 1.2 モジュール構成 - -``` -src/mip_mcp/ -├── __init__.py -├── server.py # MCPサーバーメイン -├── handlers/ # MCPリクエストハンドラー -│ ├── __init__.py -│ ├── solve.py # 最適化実行ハンドラー -│ ├── execute_code.py # Pythonコード実行ハンドラー -│ ├── validate.py # モデル検証ハンドラー -│ └── status.py # ステータス確認ハンドラー -├── solvers/ # ソルバー抽象化層 -│ ├── __init__.py -│ ├── base.py # ベースソルバークラス -│ ├── scip_solver.py # pyscipopt実装 -│ ├── gurobi_solver.py # Gurobi実装(オプション) -│ └── cplex_solver.py # CPLEX実装(オプション) -├── models/ # データモデル定義 -│ ├── __init__.py -│ ├── problem.py # 最適化問題定義 -│ ├── solution.py # ソリューション定義 -│ └── config.py # 設定データモデル -├── parsers/ # 問題形式パーサー -│ ├── __init__.py -│ ├── json_parser.py # JSON形式パーサー -│ ├── lp_parser.py # LP形式パーサー -│ └── mps_parser.py # MPS形式パーサー -├── executor/ # Pythonコード実行エンジン -│ ├── __init__.py -│ ├── code_executor.py # Pythonコード実行器 -│ ├── sandbox.py # サンドボックス環境 -│ └── libraries.py # 許可されたライブラリ管理 -├── utils/ # ユーティリティ -│ ├── __init__.py -│ ├── logger.py # ログ設定 -│ ├── config_manager.py # 設定管理 -│ └── validators.py # 入力検証 -└── config/ # 設定ファイル - ├── default.yaml # デフォルト設定 - └── solvers.yaml # ソルバー設定 -``` - -## 2. コアコンポーネント設計 - -### 2.1 MCPサーバー (server.py) - -```python -from fastmcp import FastMCP -from typing import Dict, Any, Optional - -class MIPMCPServer: - def __init__(self, config_path: Optional[str] = None): - self.config = ConfigManager(config_path) - self.solver_factory = SolverFactory(self.config) - self.app = FastMCP("mip-mcp") - self._register_handlers() - - def _register_handlers(self): - # MCPツール登録 - self.app.add_tool("solve_optimization", solve_handler) - self.app.add_tool("execute_python_code", execute_code_handler) - self.app.add_tool("get_library_examples", get_library_examples_handler) - self.app.add_tool("validate_model", validate_handler) - self.app.add_tool("get_solver_status", status_handler) - - async def run(self): - await self.app.run() -``` - -### 2.2 ソルバー抽象化層 - -#### ベースソルバークラス (solvers/base.py) - -```python -from abc import ABC, abstractmethod -from typing import Dict, Any, Optional -from ..models.problem import OptimizationProblem -from ..models.solution import OptimizationSolution - -class BaseSolver(ABC): - def __init__(self, config: Dict[str, Any]): - self.config = config - self.model = None - - @abstractmethod - async def solve(self, problem: OptimizationProblem) -> OptimizationSolution: - """最適化問題を解決する""" - pass - - @abstractmethod - def validate_problem(self, problem: OptimizationProblem) -> Dict[str, Any]: - """問題の妥当性を検証する""" - pass - - @abstractmethod - def get_solver_info(self) -> Dict[str, Any]: - """ソルバー情報を取得する""" - pass - - @abstractmethod - def set_parameters(self, params: Dict[str, Any]) -> None: - """ソルバーパラメータを設定する""" - pass -``` - -#### SCIPソルバー実装 (solvers/scip_solver.py) - -```python -import pyscipopt -from .base import BaseSolver -from ..models.problem import OptimizationProblem -from ..models.solution import OptimizationSolution - -class SCIPSolver(BaseSolver): - def __init__(self, config: Dict[str, Any]): - super().__init__(config) - self.model = pyscipopt.Model() - - async def solve(self, problem: OptimizationProblem) -> OptimizationSolution: - try: - # 変数定義 - variables = {} - for var_def in problem.variables: - variables[var_def.name] = self.model.addVar( - name=var_def.name, - vtype=var_def.type, - lb=var_def.lower_bound, - ub=var_def.upper_bound - ) - - # 制約条件追加 - for constraint in problem.constraints: - expr = self._build_expression(constraint.expression, variables) - self.model.addCons(expr <= constraint.rhs, name=constraint.name) - - # 目的関数設定 - obj_expr = self._build_expression(problem.objective.expression, variables) - self.model.setObjective(obj_expr, sense=problem.objective.sense) - - # 最適化実行 - self.model.optimize() - - return self._extract_solution(variables) - - except Exception as e: - return OptimizationSolution( - status="error", - message=str(e), - objective_value=None, - variables={} - ) - -### 2.3 Pythonコード実行エンジン - -#### コード実行器 (executor/code_executor.py) - -```python -import ast -import sys -import io -import contextlib -from typing import Dict, Any, Optional, List -from ..models.solution import OptimizationSolution -from .sandbox import SecurityChecker -from .libraries import get_allowed_imports - -class PythonCodeExecutor: - def __init__(self, config: Dict[str, Any]): - self.config = config - self.security_checker = SecurityChecker() - self.allowed_imports = get_allowed_imports() - - async def execute_optimization_code(self, code: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - """ - Pythonコードを実行して最適化問題を解決する - - Args: - code: 実行するPythonコード(PuLP/pyomo/pyscipopt等を使用) - data: コードで使用するデータ - - Returns: - 最適化結果 - """ - try: - # セキュリティチェック - self.security_checker.validate_code(code) - - # 実行環境の準備 - namespace = self._prepare_namespace(data) - - # コード実行 - output = io.StringIO() - with contextlib.redirect_stdout(output): - exec(code, namespace) - - # 結果の抽出 - result = self._extract_result(namespace, output.getvalue()) - - return result - - except Exception as e: - return { - "status": "error", - "message": f"Code execution failed: {str(e)}", - "objective_value": None, - "variables": {} - } - - def _prepare_namespace(self, data: Optional[Dict[str, Any]]) -> Dict[str, Any]: - """実行環境の名前空間を準備""" - namespace = { - '__builtins__': self._get_safe_builtins(), - 'data': data or {}, - } - - # 許可されたライブラリをインポート - for lib_name, lib_module in self.allowed_imports.items(): - namespace[lib_name] = lib_module - - return namespace - - def _get_safe_builtins(self) -> Dict[str, Any]: - """安全なbuiltins関数のみを提供""" - safe_builtins = { - 'len', 'range', 'enumerate', 'zip', 'sum', 'min', 'max', - 'abs', 'round', 'int', 'float', 'str', 'list', 'dict', - 'tuple', 'set', 'bool', 'print' - } - - return {name: getattr(__builtins__, name) for name in safe_builtins if hasattr(__builtins__, name)} - - def _extract_result(self, namespace: Dict[str, Any], output: str) -> Dict[str, Any]: - """実行結果から最適化結果を抽出""" - # PuLPの場合 - if 'pulp' in output.lower() or any('pulp' in str(v) for v in namespace.values()): - return self._extract_pulp_result(namespace) - - # pyomoの場合 - if 'pyomo' in output.lower() or any('pyomo' in str(type(v)) for v in namespace.values()): - return self._extract_pyomo_result(namespace) - - # pyscipoptの場合 - if 'scip' in output.lower() or any('scip' in str(type(v)) for v in namespace.values()): - return self._extract_scip_result(namespace) - - # 汎用的な結果抽出 - return self._extract_generic_result(namespace, output) -``` - -#### セキュリティチェッカー (executor/sandbox.py) - -```python -import ast -from typing import Set, List - -class SecurityChecker: - """コードのセキュリティチェックを行う""" - - DANGEROUS_FUNCTIONS = { - 'eval', 'exec', 'compile', '__import__', 'open', 'file', - 'input', 'raw_input', 'reload', 'vars', 'globals', 'locals', - 'dir', 'getattr', 'setattr', 'delattr', 'hasattr' - } - - DANGEROUS_MODULES = { - 'os', 'sys', 'subprocess', 'shutil', 'glob', 'socket', - 'urllib', 'http', 'ftplib', 'smtplib', 'multiprocessing', - 'threading', 'pickle', 'marshal', 'shelve' - } - - def validate_code(self, code: str) -> bool: - """コードの安全性を検証""" - try: - tree = ast.parse(code) - checker = DangerousNodeVisitor() - checker.visit(tree) - - if checker.dangerous_nodes: - raise SecurityError(f"Dangerous operations detected: {checker.dangerous_nodes}") - - return True - - except SyntaxError as e: - raise SecurityError(f"Syntax error in code: {str(e)}") - -class DangerousNodeVisitor(ast.NodeVisitor): - """危険なASTノードを検出する""" - - def __init__(self): - self.dangerous_nodes = [] - - def visit_Call(self, node): - # 危険な関数呼び出しをチェック - if isinstance(node.func, ast.Name) and node.func.id in SecurityChecker.DANGEROUS_FUNCTIONS: - self.dangerous_nodes.append(f"Function call: {node.func.id}") - self.generic_visit(node) - - def visit_Import(self, node): - # 危険なモジュールのインポートをチェック - for alias in node.names: - if alias.name in SecurityChecker.DANGEROUS_MODULES: - self.dangerous_nodes.append(f"Import: {alias.name}") - self.generic_visit(node) - - def visit_ImportFrom(self, node): - # from文での危険なインポートをチェック - if node.module in SecurityChecker.DANGEROUS_MODULES: - self.dangerous_nodes.append(f"Import from: {node.module}") - self.generic_visit(node) - -class SecurityError(Exception): - """セキュリティ関連のエラー""" - pass -``` - -#### 許可ライブラリ管理 (executor/libraries.py) - -```python -from typing import Dict, Any -import importlib - -def get_allowed_imports() -> Dict[str, Any]: - """許可されたライブラリのマッピングを返す""" - allowed_libs = {} - - # 最適化ライブラリ - optimization_libs = [ - 'pulp', - 'pyomo', - 'pyscipopt', - 'cvxpy', - 'ortools' - ] - - # 数値計算ライブラリ - numeric_libs = [ - 'numpy', - 'pandas', - 'scipy', - 'math', - 'statistics' - ] - - # 全てのライブラリを試行してインポート - for lib_name in optimization_libs + numeric_libs: - try: - lib_module = importlib.import_module(lib_name) - allowed_libs[lib_name] = lib_module - except ImportError: - # ライブラリが利用できない場合はスキップ - continue - - return allowed_libs - -def get_library_info() -> Dict[str, Dict[str, Any]]: - """利用可能なライブラリの情報を返す""" - return { - 'pulp': { - 'description': 'Linear Programming library for Python', - 'example': ''' -import pulp - -# 問題定義 -prob = pulp.LpProblem("Example", pulp.LpMaximize) - -# 変数定義 -x = pulp.LpVariable("x", 0, None) -y = pulp.LpVariable("y", 0, None) - -# 目的関数 -prob += 3*x + 2*y - -# 制約条件 -prob += 2*x + y <= 100 -prob += x + y <= 80 - -# 求解 -prob.solve() - -# 結果出力 -result = { - "status": pulp.LpStatus[prob.status], - "objective": pulp.value(prob.objective), - "variables": {v.name: v.varValue for v in prob.variables()} -} -''' - }, - 'pyomo': { - 'description': 'Python Optimization Modeling Objects', - 'example': ''' -import pyomo.environ as pyo -from pyomo.opt import SolverFactory - -# モデル作成 -model = pyo.ConcreteModel() - -# 変数定義 -model.x = pyo.Var(bounds=(0, None)) -model.y = pyo.Var(bounds=(0, None)) - -# 目的関数 -model.obj = pyo.Objective(expr=3*model.x + 2*model.y, sense=pyo.maximize) - -# 制約条件 -model.constraint1 = pyo.Constraint(expr=2*model.x + model.y <= 100) -model.constraint2 = pyo.Constraint(expr=model.x + model.y <= 80) - -# 求解 -solver = SolverFactory('cbc') -results = solver.solve(model) - -# 結果出力 -result = { - "status": str(results.solver.termination_condition), - "objective": pyo.value(model.obj), - "variables": { - "x": pyo.value(model.x), - "y": pyo.value(model.y) - } -} -''' - } - } -``` - -### 2.4 Pythonコード実行ハンドラー (handlers/execute_code.py) - -```python -from fastmcp import Context -from typing import Dict, Any, Optional -from ..executor.code_executor import PythonCodeExecutor -from ..utils.logger import get_logger - -logger = get_logger(__name__) - -async def execute_code_handler( - context: Context, - code: str, - data: Optional[Dict[str, Any]] = None, - library: str = "pulp" -) -> Dict[str, Any]: - """ - Pythonコードを実行して最適化問題を解決する - - Args: - code: 実行するPythonコード - data: コードで使用するデータ - library: 使用する最適化ライブラリのヒント - - Returns: - 最適化結果 - """ - try: - executor = PythonCodeExecutor(context.config) - result = await executor.execute_optimization_code(code, data) - - logger.info(f"Code execution completed with library {library}") - return result - - except Exception as e: - logger.error(f"Code execution failed: {str(e)}") - return { - "status": "error", - "message": str(e), - "objective_value": None, - "variables": {} - } - -async def get_library_examples_handler(context: Context) -> Dict[str, Any]: - """ - 利用可能なライブラリとサンプルコードを返す - """ - from ..executor.libraries import get_library_info - - return { - "libraries": get_library_info(), - "supported_formats": ["pulp", "pyomo", "pyscipopt", "cvxpy", "ortools"] - } -``` - -### 2.5 データモデル定義 - -#### 最適化問題モデル (models/problem.py) - -```python -from pydantic import BaseModel, Field -from typing import List, Dict, Any, Optional, Literal -from enum import Enum - -class VariableType(str, Enum): - CONTINUOUS = "C" - INTEGER = "I" - BINARY = "B" - -class ObjectiveSense(str, Enum): - MINIMIZE = "minimize" - MAXIMIZE = "maximize" - -class Variable(BaseModel): - name: str - type: VariableType = VariableType.CONTINUOUS - lower_bound: Optional[float] = None - upper_bound: Optional[float] = None - -class Constraint(BaseModel): - name: str - expression: Dict[str, float] # {variable_name: coefficient} - sense: Literal["<=", ">=", "="] = "<=" - rhs: float - -class Objective(BaseModel): - sense: ObjectiveSense - expression: Dict[str, float] # {variable_name: coefficient} - -class OptimizationProblem(BaseModel): - name: str - variables: List[Variable] - constraints: List[Constraint] - objective: Objective - parameters: Optional[Dict[str, Any]] = None -``` - -#### ソリューションモデル (models/solution.py) - -```python -from pydantic import BaseModel -from typing import Dict, Any, Optional, List - -class SolutionVariable(BaseModel): - name: str - value: float - reduced_cost: Optional[float] = None - -class SolutionConstraint(BaseModel): - name: str - slack: Optional[float] = None - dual_value: Optional[float] = None - -class OptimizationSolution(BaseModel): - status: str # optimal, infeasible, unbounded, error, etc. - objective_value: Optional[float] = None - variables: Dict[str, float] = {} - constraints: List[SolutionConstraint] = [] - solve_time: Optional[float] = None - iterations: Optional[int] = None - message: Optional[str] = None - solver_info: Optional[Dict[str, Any]] = None -``` - -### 2.6 MCPハンドラー実装 - -#### 最適化実行ハンドラー (handlers/solve.py) - -```python -from fastmcp import Context -from typing import Dict, Any -from ..models.problem import OptimizationProblem -from ..parsers import get_parser -from ..utils.logger import get_logger - -logger = get_logger(__name__) - -async def solve_handler( - context: Context, - problem_data: Dict[str, Any], - solver_name: str = "scip", - solver_params: Dict[str, Any] = None -) -> Dict[str, Any]: - """ - 最適化問題を解決する - - Args: - problem_data: 最適化問題データ(JSON/LP/MPS形式) - solver_name: 使用するソルバー名 - solver_params: ソルバーパラメータ - - Returns: - 最適化結果 - """ - try: - # 問題データのパース - parser = get_parser(problem_data) - problem = parser.parse(problem_data) - - # ソルバー取得 - solver = context.solver_factory.get_solver(solver_name) - - # パラメータ設定 - if solver_params: - solver.set_parameters(solver_params) - - # 最適化実行 - solution = await solver.solve(problem) - - logger.info(f"Optimization completed: {solution.status}") - - return solution.dict() - - except Exception as e: - logger.error(f"Optimization failed: {str(e)}") - return { - "status": "error", - "message": str(e), - "objective_value": None, - "variables": {} - } -``` - -## 3. 設定管理システム - -### 3.1 設定ファイル構造 - -#### default.yaml -```yaml -server: - name: "mip-mcp" - version: "0.1.0" - -logging: - level: "INFO" - format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - -solvers: - default: "scip" - timeout: 3600 # seconds - -executor: - enabled: true - timeout: 300 # seconds for code execution - memory_limit: "1GB" - -parsers: - supported_formats: ["json", "lp", "mps", "python"] - -validation: - max_variables: 100000 - max_constraints: 100000 - max_code_length: 10000 # characters -``` - -#### solvers.yaml -```yaml -scip: - class: "SCIPSolver" - enabled: true - parameters: - limits/time: 3600 - display/verblevel: 1 - -gurobi: - class: "GurobiSolver" - enabled: false - parameters: - TimeLimit: 3600 - OutputFlag: 1 - -cplex: - class: "CPLEXSolver" - enabled: false - parameters: - timelimit: 3600 - output.clonelog: 1 -``` - -### 3.2 設定管理クラス (utils/config_manager.py) - -```python -import yaml -from pathlib import Path -from typing import Dict, Any, Optional - -class ConfigManager: - def __init__(self, config_path: Optional[str] = None): - self.config_dir = Path(config_path) if config_path else Path(__file__).parent.parent / "config" - self.config = self._load_config() - - def _load_config(self) -> Dict[str, Any]: - """設定ファイルを読み込む""" - default_config = self._load_yaml("default.yaml") - solver_config = self._load_yaml("solvers.yaml") - - default_config["solvers_config"] = solver_config - return default_config - - def _load_yaml(self, filename: str) -> Dict[str, Any]: - """YAMLファイルを読み込む""" - config_path = self.config_dir / filename - if config_path.exists(): - with open(config_path, 'r', encoding='utf-8') as f: - return yaml.safe_load(f) or {} - return {} - - def get(self, key: str, default: Any = None) -> Any: - """設定値を取得する""" - keys = key.split('.') - value = self.config - for k in keys: - if isinstance(value, dict) and k in value: - value = value[k] - else: - return default - return value -``` - -## 4. 入力形式対応 - -### 4.1 入力形式の拡張 - -MCPサーバーは以下の入力形式に対応: - -1. **Pythonコード実行** (推奨) - - PuLP, pyomo, pyscipopt等のライブラリを使用 - - 複雑なデータ処理と最適化を組み合わせ可能 - - 柔軟なモデリングが可能 - -2. **構造化データ形式** - - JSON: プログラマティックな問題定義 - - LP: 標準線形計画形式 - - MPS: 業界標準形式 - -### 4.2 パーサーシステム - -### 4.3 パーサーファクトリー (parsers/__init__.py) - -```python -from typing import Dict, Any, Union -from .json_parser import JSONParser -from .lp_parser import LPParser -from .mps_parser import MPSParser - -def get_parser(problem_data: Union[Dict[str, Any], str]): - """ - 問題データの形式に応じて適切なパーサーを返す - """ - if isinstance(problem_data, dict): - return JSONParser() - elif isinstance(problem_data, str): - if problem_data.strip().startswith("Minimize") or problem_data.strip().startswith("Maximize"): - return LPParser() - elif "ROWS" in problem_data and "COLUMNS" in problem_data: - return MPSParser() - else: - # JSON文字列として試行 - try: - import json - json.loads(problem_data) - return JSONParser() - except: - raise ValueError("Unsupported problem format") - else: - raise ValueError("Invalid problem data type") -``` - -### 4.4 JSONパーサー (parsers/json_parser.py) - -```python -from typing import Dict, Any, Union -from ..models.problem import OptimizationProblem -import json - -class JSONParser: - def parse(self, data: Union[Dict[str, Any], str]) -> OptimizationProblem: - """ - JSON形式の最適化問題をパースする - - Expected format: - { - "name": "problem_name", - "variables": [ - {"name": "x1", "type": "C", "lower_bound": 0, "upper_bound": 10} - ], - "constraints": [ - {"name": "c1", "expression": {"x1": 2, "x2": 3}, "sense": "<=", "rhs": 10} - ], - "objective": { - "sense": "minimize", - "expression": {"x1": 1, "x2": 2} - } - } - """ - if isinstance(data, str): - data = json.loads(data) - - return OptimizationProblem.parse_obj(data) -``` - -## 5. エラーハンドリング戦略 - -### 5.1 エラー分類 - -```python -class MIPMCPError(Exception): - """MIP MCP基底例外クラス""" - pass - -class SolverError(MIPMCPError): - """ソルバー関連エラー""" - pass - -class ParseError(MIPMCPError): - """パース関連エラー""" - pass - -class ValidationError(MIPMCPError): - """バリデーション関連エラー""" - pass - -class ConfigError(MIPMCPError): - """設定関連エラー""" - pass -``` - -### 5.2 エラーハンドリング戦略 - -1. **入力検証段階**: `ValidationError`でクライアントに詳細な修正指示 -2. **パース段階**: `ParseError`で形式エラーを報告 -3. **ソルバー実行段階**: `SolverError`で実行エラーを処理 -4. **内部エラー**: ログ出力と一般的なエラーメッセージ - -## 6. テスト戦略 - -### 6.1 テスト構造 - -``` -tests/ -├── unit/ -│ ├── test_solvers/ -│ ├── test_parsers/ -│ ├── test_handlers/ -│ └── test_models/ -├── integration/ -│ ├── test_mcp_communication/ -│ └── test_solver_integration/ -└── fixtures/ - ├── sample_problems/ - └── expected_solutions/ -``` - -### 6.2 テストデータ - -- 小規模線形計画問題 -- 整数計画問題 -- 実行不可能問題 -- 非有界問題 -- エラーケース用の不正データ - -## 7. パフォーマンス考慮事項 - -### 7.1 メモリ管理 - -- 大規模問題でのストリーミング処理 -- ソルバーインスタンスの適切な破棄 -- 中間結果のメモリ効率 - -### 7.2 非同期処理 - -- 長時間実行される最適化の非同期対応 -- プログレス報告機能 -- キャンセル機能 - -## 8. セキュリティ考慮事項 - -### 8.1 入力検証 - -- 問題サイズ制限 -- ファイル形式検証 -- SQLインジェクション対策(外部DB使用時) - -### 8.2 リソース制限 - -- CPU使用時間制限 -- メモリ使用量制限 -- 同時実行数制限 - -## 9. 依存関係とインストール - -### 9.1 pyproject.tomlの更新 - -```toml -[project] -name = "mip-mcp" -version = "0.1.0" -description = "MIP optimization server using Model Context Protocol" -readme = "README.md" -authors = [ - { name = "ohtaman", email = "ohtamans@gmail.com" } -] -requires-python = ">=3.12" -dependencies = [ - "fastmcp>=2.10.6", - "pyscipopt>=5.5.0", - "pulp>=2.7.0", - "pydantic>=2.0.0", - "pyyaml>=6.0", -] - -[project.optional-dependencies] -extra = [ - "pyomo>=6.0.0", - "cvxpy>=1.3.0", - "gurobipy>=10.0.0", # 商用ライセンス必要 - "cplex>=22.1.0", # 商用ライセンス必要 - "numpy>=1.21.0", - "pandas>=1.3.0", - "ortools>=9.0.0", -] - -[project.scripts] -mip-mcp = "mip_mcp:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" -``` - -### 9.2 使用例 - -#### Pythonコード実行による最適化 - -```python -# LLMクライアントから送信されるリクエスト例 -{ - "tool": "execute_python_code", - "arguments": { - "code": """ -import pulp - -# 生産計画問題 -prob = pulp.LpProblem("Production", pulp.LpMaximize) - -# 変数定義 -x1 = pulp.LpVariable("Product_A", 0, None) -x2 = pulp.LpVariable("Product_B", 0, None) - -# 目的関数(利益最大化) -prob += 40*x1 + 30*x2 - -# 制約条件 -prob += 2*x1 + x2 <= 100 # 労働時間制約 -prob += x1 + 2*x2 <= 80 # 原材料制約 - -# 求解 -prob.solve() - -# 結果をグローバル変数として設定 -result = { - "status": pulp.LpStatus[prob.status], - "objective": pulp.value(prob.objective), - "variables": {v.name: v.varValue for v in prob.variables()} -} -""", - "data": { - "max_labor_hours": 100, - "max_materials": 80 - } - } -} -``` - -## 10. 将来拡張性 - -### 10.1 プラグインアーキテクチャ - -- 新しいソルバーの動的追加 -- カスタムパーサーの追加 -- カスタムバリデーターの追加 -- 新しい最適化ライブラリのサポート - -### 10.2 スケーラビリティ - -- 分散処理対応(将来) -- キューイングシステム統合 -- 結果キャッシュ機能 -- Docker化とKubernetes対応 diff --git a/.tmp/issue2-solution-summary.md b/.tmp/issue2-solution-summary.md deleted file mode 100644 index 8de47ab..0000000 --- a/.tmp/issue2-solution-summary.md +++ /dev/null @@ -1,94 +0,0 @@ -# Issue #2 Solution Summary: Ghost Process Prevention - -## Overview -Issue #2 identified potential ghost process (orphaned subprocess) problems in the MIP-MCP server. A comprehensive solution has been implemented that addresses all the identified concerns. - -## Problems Solved - -### ✅ 1. PyodideExecutor Process Cleanup Issues -**Location**: `src/mip_mcp/executor/pyodide_executor.py:727-775` - -**Solution Implemented**: -- **Graceful shutdown cascade**: Try exit command → SIGTERM → SIGKILL with proper timeouts -- **Proper timeout handling**: 2s for graceful exit, 5s for SIGTERM, 2s for SIGKILL -- **Idempotent cleanup**: Multiple cleanup calls are safe -- **Improved `__del__` method**: Synchronous fallback cleanup for destruction scenarios - -### ✅ 2. Process Group Management -**Location**: `src/mip_mcp/executor/pyodide_executor.py:184` - -**Solution Implemented**: -- **Process groups**: `start_new_session=True` creates new process group -- **Child process cleanup**: Entire process tree is terminated together -- **Signal isolation**: Subprocesses don't inherit parent signal handlers - -### ✅ 3. Executor Registry for Tracking -**Location**: `src/mip_mcp/utils/executor_registry.py` - -**Solution Implemented**: -- **Weak reference tracking**: Avoids circular references, automatic cleanup of dead refs -- **Thread-safe operations**: AsyncLock for concurrent access -- **Centralized cleanup**: `cleanup_all()` with 15s timeout for all active executors -- **Active count tracking**: Real-time monitoring of executor instances - -### ✅ 4. Server-Level Signal Handling -**Location**: `src/mip_mcp/server.py:57-93` - -**Solution Implemented**: -- **Atexit handlers**: Natural cleanup during process termination -- **FastMCP integration**: Works with FastMCP's signal handling without conflicts -- **Event loop management**: Creates new loop for cleanup during shutdown -- **Exception suppression**: Prevents ugly tracebacks during shutdown - -### ✅ 5. Exception Path Cleanup -**Location**: `src/mip_mcp/handlers/execute_code.py:619-628` - -**Solution Implemented**: -- **Finally blocks**: Ensure executor cleanup in all execution paths -- **Registry integration**: Automatic registration/unregistration -- **Error handling**: Graceful degradation when cleanup fails - -## Key Features - -### Security & Reliability -- **Complete process isolation**: WebAssembly + process groups prevent orphans -- **Timeout protection**: No hanging processes, all operations have timeouts -- **Resource leak prevention**: Weak references prevent memory leaks - -### Robustness -- **Multiple cleanup strategies**: Graceful → terminate → kill escalation -- **Failure tolerance**: Continues cleanup even if individual executors fail -- **Idempotent operations**: Safe to call cleanup multiple times - -### Testing Coverage -- **14 comprehensive test cases**: Cover all cleanup scenarios -- **Integration testing**: Real-world execution path testing -- **Mock-based testing**: Fast, reliable test execution -- **Edge case coverage**: Timeouts, failures, concurrent operations - -## Implementation Statistics - -- **Files Modified**: 4 core files -- **New Files Added**: 1 (executor_registry.py) -- **Test Files Added**: 1 (test_subprocess_cleanup.py) -- **Test Cases**: 14 tests covering all scenarios -- **Test Pass Rate**: 100% (88 passed, 7 skipped) - -## Acceptance Criteria Status - -- ✅ No ghost processes remain after server shutdown -- ✅ Signal handlers properly clean up all subprocesses -- ✅ Process cleanup works under all exception conditions -- ✅ Subprocess cleanup completes within reasonable timeouts -- ✅ Process groups prevent orphaned child processes -- ✅ Comprehensive logging for debugging process issues - -## Manual Testing Verified - -1. **Signal handling**: Server responds properly to SIGINT/SIGTERM -2. **Process monitoring**: No processes remain after shutdown -3. **Timeout scenarios**: Cleanup completes within expected timeframes -4. **Concurrent execution**: Multiple executors cleanup properly -5. **Error conditions**: Cleanup works even when operations fail - -The implementation successfully addresses all concerns raised in Issue #2 and provides a robust, production-ready solution for subprocess management. diff --git a/.tmp/pyodide_architecture.md b/.tmp/pyodide_architecture.md deleted file mode 100644 index 15e1be4..0000000 --- a/.tmp/pyodide_architecture.md +++ /dev/null @@ -1,227 +0,0 @@ -# Pyodideベース MIP-MCP サーバー アーキテクチャ設計 - -## 概要 - -既存のカスタムサンドボックスをPyodideベースの安全なWebAssembly実行環境に置き換える。ツール名は汎用的なMIP対応のまま維持し、現在はPuLPのみサポートする。 - -## アーキテクチャ - -### 1. **Pyodide実行エンジン** (`PyodideExecutor`) - -```typescript -interface PyodideExecutor { - // Pyodide環境の初期化(遅延読み込み) - initialize(): Promise - - // MIPコード実行(PuLP対応) - executeMIPCode(code: string, options: ExecutionOptions): Promise - - // ライブラリ検出 - detectLibrary(code: string): MIPLibrary - - // パフォーマンス最適化 - warmup(): Promise - dispose(): void -} -``` - -### 2. **ライブラリ検出システム** (`LibraryDetector`) - -```python -class LibraryDetector: - def detect_library(self, code: str) -> MIPLibrary: - """コードからMIPライブラリを自動検出""" - if 'import pulp' in code or 'from pulp' in code: - return MIPLibrary.PULP - elif 'import mip' in code or 'from mip' in code: - return MIPLibrary.PYTHON_MIP # 将来対応 - else: - return MIPLibrary.UNKNOWN -``` - -### 3. **問題情報抽出** (`ProblemExtractor`) - -```python -class ProblemExtractor: - def extract_problem_info(self, code: str, pyodide_globals: dict) -> ProblemInfo: - """実行後のグローバル変数から問題情報を抽出""" - # 1. 変数方式(推奨) - if '__mps_content__' in pyodide_globals: - return ProblemInfo(format='mps', content=pyodide_globals['__mps_content__']) - elif '__lp_content__' in pyodide_globals: - return ProblemInfo(format='lp', content=pyodide_globals['__lp_content__']) - - # 2. 自動検出方式 - problems = self._detect_problems(pyodide_globals) - if problems: - return self._generate_format(problems[0]) - - return ProblemInfo(format=None, content=None) -``` - -### 4. **MCPツール統合** - -既存のツール名を維持しつつ、内部実装をPyodideに変更: - -```python -class MIPMCPTools: - # ツール名: execute_mip_code (汎用的) - # 実装: PuLPサポート、将来的に他ライブラリ対応可能 - async def execute_mip_code(self, code: str) -> ExecutionResult - - # ツール名: validate_mip_code (汎用的) - # 実装: 現在はPuLP構文検証 - async def validate_mip_code(self, code: str) -> ValidationResult - - # その他既存ツールも同様に汎用的な名前を維持 -``` - -## 実装詳細 - -### Phase 1: Pyodide実行エンジン - -1. **Node.js + Pyodide統合** - ```typescript - class PyodideExecutor { - private pyodide: PyodideInterface | null = null - - async initialize() { - if (!this.pyodide) { - this.pyodide = await loadPyodide() - await this.pyodide.loadPackage("micropip") - await this.installMIPLibraries() - } - } - - private async installMIPLibraries() { - await this.pyodide.runPythonAsync(` - import micropip - await micropip.install('pulp') - `) - } - } - ``` - -2. **セキュアな実行環境** - ```python - # Pyodide内部で実行される安全なコード実行ラッパー - def secure_execute_mip_code(user_code: str) -> dict: - # グローバル名前空間を制限 - safe_globals = { - '__builtins__': {}, - 'pulp': pulp, - # その他必要最小限のみ - } - - try: - exec(user_code, safe_globals) - return { - 'success': True, - 'globals': safe_globals, - 'error': None - } - except Exception as e: - return { - 'success': False, - 'globals': {}, - 'error': str(e) - } - ``` - -### Phase 2: 問題情報抽出 - -1. **変数ベース抽出(推奨)** - ```python - # ユーザーコード例 - import pulp - prob = pulp.LpProblem("example", pulp.LpMaximize) - # ... 問題定義 ... - - # LP形式を変数に設定(推奨方式) - prob.writeLP("/tmp/problem.lp") - with open("/tmp/problem.lp", "r") as f: - __lp_content__ = f.read() - ``` - -2. **自動検出(フォールバック)** - ```python - def auto_detect_problems(globals_dict: dict) -> List[ProblemInfo]: - problems = [] - for name, obj in globals_dict.items(): - if hasattr(obj, 'writeLP'): # PuLP問題オブジェクト - problems.append(extract_from_pulp_problem(obj)) - return problems - ``` - -### Phase 3: パフォーマンス最適化 - -1. **Pyodide事前ウォームアップ** - ```typescript - class PyodidePool { - private instances: PyodideExecutor[] = [] - - async warmup(count: number = 3) { - for (let i = 0; i < count; i++) { - const executor = new PyodideExecutor() - await executor.initialize() - this.instances.push(executor) - } - } - - async getExecutor(): Promise { - return this.instances.pop() || new PyodideExecutor() - } - } - ``` - -2. **メモリ効率化** - - 不要なライブラリの遅延読み込み - - 実行後のクリーンアップ - - インスタンス再利用 - -## 移行計画 - -### Step 1: 新しいPyodide実行エンジン実装 -- `src/mip_mcp/executor/pyodide_executor.py` -- Node.js統合とPyodide初期化 - -### Step 2: 既存ハンドラーの更新 -- `src/mip_mcp/handlers/execute_code.py`の内部実装変更 -- MCPツール名とインターフェースは維持 - -### Step 3: ライブラリ検出の統合 -- `src/mip_mcp/utils/library_detector.py`をPyodide対応に更新 - -### Step 4: テストとパフォーマンス調整 -- セキュリティテスト -- パフォーマンステスト -- エラーハンドリング強化 - -## 期待される効果 - -### セキュリティ -- ✅ **完全分離**: WebAssembly環境での実行 -- ✅ **ゼロ脆弱性**: ホストシステムアクセス不可 -- ✅ **検証済み**: Pyodideは本番環境で広く使用 - -### 機能性 -- ✅ **PuLP完全サポート**: LP/MPS生成確認済み -- ✅ **拡張性**: 将来的にpython-mip等追加可能 -- ✅ **互換性**: 既存MCPツール名・インターフェース維持 - -### パフォーマンス -- ✅ **高速起動**: 数百ms程度 -- ✅ **軽量**: メモリ使用量最適化 -- ✅ **スケーラブル**: Cloud Run環境での運用 - -## リスク軽減 - -### 潜在的リスク -1. **Pyodide起動時間**: ウォームアップで解決 -2. **メモリ使用量**: プールとクリーンアップで管理 -3. **ライブラリ制限**: PuLPサポート確認済み - -### 対策 -- インスタンスプール -- メモリ監視 -- グレースフルフォールバック(RestrictedPython) diff --git a/.tmp/requirements.md b/.tmp/requirements.md deleted file mode 100644 index ecc3100..0000000 --- a/.tmp/requirements.md +++ /dev/null @@ -1,173 +0,0 @@ -# 要件定義書 - MIP MCP Server - -## 1. 目的 - -LLMからのPuLPコードを受信し、MPS/LPファイルを生成してMIP最適化を実行するMCP(Model Context Protocol)サーバーを構築する。LLMクライアントはPuLPライブラリを使用してPythonコードを生成し、MCPサーバーがそのコードを実行してMPS/LPファイルを出力、pyscipoptで解決して結果を返却する。 - -## 2. 機能要件 - -### 2.1 必須機能 - -#### 基本MCP機能 -- [ ] MCP対応のサーバー実装(fastmcpベース) -- [ ] streamable HTTPトランスポート対応(fastmcp標準機能) -- [ ] LLMクライアントとの双方向通信インターフェース -- [ ] LP/MPS形式での問題ファイル受信・処理 - -#### 最適化ソルバー統合 -- [ ] pyscipopt(デフォルト)による最適化実行 -- [ ] 設定ファイルによるソルバー切り替え機能(SCIP、Gurobi、CPLEX等) -- [ ] ソルバーパラメータの動的調整 -- [ ] 最適化結果の構造化出力 - -#### Pythonコード実行機能(PuLP特化) -- [ ] PuLPライブラリを使用したPythonコード実行 -- [ ] MPS/LPファイル生成機能 -- [ ] 生成されたファイルの妥当性検証 -- [ ] PuLPモデルからソルバー対応形式への変換 - -#### 問題検証・デバッグ支援 -- [ ] 制約条件の数学的検証 -- [ ] 実行不可能モデルの診断 -- [ ] 制約緩和提案機能 -- [ ] エラーメッセージの自然言語説明 - -### 2.2 オプション機能(将来実装) - -- [ ] 複数最適化ライブラリ対応(pyomo、cvxpy等) -- [ ] 複数ソルバーでの結果比較 -- [ ] ソリューションの感度分析 -- [ ] 最適化プロセスの可視化 - -## 3. 非機能要件 - -### 3.1 パフォーマンス - -- 処理性能はソルバーとマシン性能に依存(具体的な時間制約なし) -- ソルバー実行のプログレス情報提供 -- メモリ使用量の適切な管理 -- streamable HTTPによる効率的なデータ転送 - -### 3.2 セキュリティ - -- 入力データの検証とサニタイゼーション -- ソルバー実行時のサンドボックス化 -- 機密性の高い最適化データの保護 -- ログ情報の適切な管理 - -### 3.3 保守性 - -- モジュラー設計による機能拡張の容易性 -- 設定ファイルによる動作カスタマイズ -- 詳細なログ出力とデバッグ情報 -- 包括的なユニットテストカバレージ - -### 3.4 互換性 - -- 主要MIPソルバーとの互換性維持 -- 既存のMCP実装との相互運用性 -- Python 3.12以上での動作保証 -- クロスプラットフォーム対応 - -### 3.5 クラウドデプロイメント - -- Google Cloud Run環境での実行対応 -- コンテナ化による可搬性確保 -- ステートレス設計によるスケーラビリティ -- 環境変数による設定管理 - -## 4. 制約事項 - -### 4.1 技術的制約 - -- fastmcp v2.10.6以上の使用 -- pyscipopt v5.5.0以上の使用 -- Pythonベースの実装 -- MCP仕様への準拠 - -### 4.2 ビジネス制約 - -- オープンソースライセンスでの提供 -- 商用ソルバーは別途ライセンス必要 -- 初期段階では基本機能の実装を優先 - -### 4.3 デプロイメント制約 - -- Cloud Run環境のメモリ・CPU制限への対応 -- コールドスタート時間の最小化 -- リクエストタイムアウト(最大60分)内での処理 -- 永続ストレージなしでの動作 - -## 5. 成功基準 - -### 5.1 完了の定義 - -- [ ] MCP経由でLLMクライアントがPuLPコードを送信可能 -- [ ] PuLPコードからMPS/LPファイルを生成 -- [ ] 生成されたファイルをpyscipoptで解決 -- [ ] 最適化結果(変数値、目的関数値、ステータス)を返却 -- [ ] エラー処理と適切なフィードバックの提供 -- [ ] streamable HTTPトランスポートでの通信確立 -- [ ] Cloud Run環境での正常動作確認 - -### 5.2 受け入れテスト - -- PuLPコードによるサンプル最適化問題の解決 -- 線形計画・整数計画問題でのMPS/LP生成テスト -- 生成ファイルの妥当性検証テスト -- エラーケースでの適切な診断メッセージ表示 -- MCP streamable HTTPでの通信テスト -- Cloud Run環境でのデプロイメントテスト -- コンテナ環境でのPuLP→MPS/LP→解決フローテスト - -## 6. 想定されるリスク - -### 技術リスク -- 商用ソルバーのライセンス・インストール複雑性 -- MCP仕様の進化に伴う互換性問題 -- 大規模問題でのメモリ・処理時間制約 - -### 運用リスク -- ソルバー依存関係の管理 -- LLMとの通信エラー処理 -- 不正な最適化問題入力の処理 - -### クラウド運用リスク -- Cloud Runのコールドスタート遅延 -- メモリ・CPU制限による大規模問題の制約 -- ネットワーク接続の不安定性 -- 商用ソルバーライセンスのクラウド対応 - -### 対策 -- 複数ソルバーでの代替実装 -- 詳細なエラーハンドリング -- 入力検証の強化 -- コンテナイメージの最適化 -- ヘルスチェック機能の実装 -- 適切なタイムアウト設定 - -## 7. 今後の検討事項 - -### 設計フェーズで詳細化すべき事項 -- MCPサーバーのアーキテクチャ設計 -- ソルバー抽象化層の設計 -- Pythonコード実行環境の設計 -- 設定管理システムの設計 -- エラーハンドリング戦略 -- テスト戦略とカバレージ計画 -- Cloud Run デプロイメント戦略 -- Dockerコンテナ設計 - -### 実装優先度の検討 -- Phase 1: 基本MCP機能 + PuLP + pyscipopt統合 -- Phase 2: MPS/LPファイル生成・処理機能 -- Phase 3: Cloud Run対応 + コンテナ化 -- Phase 4: 他最適化ライブラリ対応(pyomo、cvxpy等) -- Phase 5: 複数ソルバー対応 - -### 外部依存関係の調査 -- PuLPライブラリの機能とMPS/LP出力形式 -- pyscipoptとの互換性確認 -- MCPクライアントとの連携テスト -- Cloud Runの制限事項とベストプラクティス -- コンテナ環境でのPuLP + pyscipoptの動作確認 diff --git a/.tmp/solver_selection_design.md b/.tmp/solver_selection_design.md deleted file mode 100644 index 4f821c1..0000000 --- a/.tmp/solver_selection_design.md +++ /dev/null @@ -1,37 +0,0 @@ -# Solver Selection Design - -## Problem -現在、ユーザーはソルバーパラメータ(solver_params)を設定できるが、どのソルバーを使用するかを指定できない。execute_mip_codeツールはハードコードでSCIPソルバーのみを使用している。 - -## Solution Design - -### 1. APIパラメータの追加 -`execute_mip_code`ツールに`solver`パラメータを追加: -- Type: Optional[str] = None -- Default: None (設定ファイルのdefaultを使用) -- Options: "scip" (現在は1つのみサポート) - -### 2. SolverFactoryパターンの実装 -ソルバー選択のためのファクトリークラスを作成: -```python -class SolverFactory: - @staticmethod - def create_solver(solver_name: str, config: Dict[str, Any]) -> BaseSolver: - if solver_name.lower() == "scip": - return SCIPSolver(config) - else: - raise ValueError(f"Unsupported solver: {solver_name}") -``` - -### 3. 設定からのデフォルトソルバー取得 -設定ファイル(default.yaml)の`solvers.default`を使用してデフォルトソルバーを決定 - -### 4. バックワード互換性 -- 既存のコードは変更なしで動作し続ける -- solver パラメータが未指定の場合はSCIPを使用 - -## Implementation Steps -1. SolverFactoryクラスの実装 -2. execute_mip_code_handlerの更新(solverパラメータ追加) -3. MCP tool定義の更新 -4. テストの追加 diff --git a/.tmp/tasks.md b/.tmp/tasks.md deleted file mode 100644 index ab9e09c..0000000 --- a/.tmp/tasks.md +++ /dev/null @@ -1,380 +0,0 @@ -# タスク分解書 - MIP MCP Server - -## 1. 実装タスク概要 - -要件・設計に基づいて、PuLP特化のMIP MCP Serverを実装する。 - -### ワークフロー -``` -LLM Client → PuLP Code → MCP Server → MPS/LP File → pyscipopt → Results → LLM Client -``` - -## 2. Phase 1: 基本MCP機能 + PuLP + pyscipopt統合 - -### 2.1 プロジェクト基盤セットアップ - -#### T001: 依存関係の更新 -- [ ] pyproject.tomlにPuLP依存関係追加 -- [ ] 必要な依存関係のインストール確認 -- [ ] 開発環境のセットアップ - -**優先度**: High -**見積時間**: 0.5h -**成果物**: 更新されたpyproject.toml - -#### T002: プロジェクト構造の作成 -- [ ] src/mip_mcp/配下のディレクトリ構造作成 -- [ ] 基本的な__init__.pyファイル作成 -- [ ] 設定ファイルディレクトリの作成 - -**優先度**: High -**見積時間**: 0.5h -**成果物**: 基本プロジェクト構造 - -### 2.2 データモデル定義 - -#### T003: 基本データモデル実装 -- [ ] models/solution.py: OptimizationSolution実装 -- [ ] models/config.py: 設定データモデル実装 -- [ ] Pydanticモデルの基本検証 - -**優先度**: High -**見積時間**: 1h -**成果物**: データモデルクラス - -### 2.3 設定管理システム - -#### T004: 設定管理実装 -- [ ] utils/config_manager.py実装 -- [ ] config/default.yaml作成 -- [ ] 環境変数サポート実装 - -**優先度**: High -**見積時間**: 1h -**成果物**: 設定管理システム - -#### T005: ログシステム実装 -- [ ] utils/logger.py実装 -- [ ] 構造化ログ出力設定 -- [ ] ログレベル設定 - -**優先度**: Medium -**見積時間**: 0.5h -**成果物**: ログシステム - -### 2.4 PuLPコード実行エンジン - -#### T006: セキュリティチェッカー実装 -- [ ] executor/sandbox.py実装 -- [ ] ASTベースの危険コード検出 -- [ ] SecurityError例外クラス定義 - -**優先度**: High -**見積時間**: 2h -**成果物**: セキュアなコード検証機能 - -#### T007: PuLP実行環境実装 -- [ ] executor/code_executor.py実装 -- [ ] PuLPライブラリの安全な実行環境 -- [ ] 実行タイムアウト制御 - -**優先度**: High -**見積時間**: 2h -**成果物**: PuLP実行エンジン - -#### T008: MPS/LP生成機能実装 -- [ ] PuLPモデルからMPS形式生成 -- [ ] PuLPモデルからLP形式生成 -- [ ] 生成ファイルの妥当性検証 - -**優先度**: High -**見積時間**: 1.5h -**成果物**: ファイル生成機能 - -### 2.5 ソルバー統合 - -#### T009: ベースソルバークラス実装 -- [ ] solvers/base.py実装 -- [ ] 抽象ソルバーインターフェース定義 -- [ ] エラーハンドリング基盤 - -**優先度**: High -**見積時間**: 1h -**成果物**: ソルバー抽象化基盤 - -#### T010: SCIPソルバー実装 -- [ ] solvers/scip_solver.py実装 -- [ ] MPS/LPファイル読み込み機能 -- [ ] pyscipoptによる最適化実行 -- [ ] 結果抽出機能 - -**優先度**: High -**見積時間**: 2h -**成果物**: SCIP統合機能 - -### 2.6 MCPサーバー実装 - -#### T011: MCPハンドラー実装 -- [ ] handlers/execute_code.py実装 -- [ ] PuLPコード実行ハンドラー -- [ ] エラーハンドリングとレスポンス形成 - -**優先度**: High -**見積時間**: 1.5h -**成果物**: MCP実行ハンドラー - -#### T012: MCPサーバーメイン実装 -- [ ] server.py実装 -- [ ] FastMCPサーバー設定 -- [ ] ハンドラー登録とルーティング - -**優先度**: High -**見積時間**: 1h -**成果物**: MCPサーバー本体 - -#### T013: アプリケーションエントリーポイント -- [ ] __init__.py実装 -- [ ] main()関数実装 -- [ ] コマンドライン起動機能 - -**優先度**: High -**見積時間**: 0.5h -**成果物**: 実行可能アプリケーション - -## 3. Phase 2: MPS/LPファイル生成・処理機能強化 - -### 3.1 ファイル処理機能拡張 - -#### T014: ファイル検証機能強化 -- [ ] 生成されたMPS/LPファイルの構文検証 -- [ ] ソルバー固有の制限事項チェック -- [ ] 詳細なエラーレポート機能 - -**優先度**: Medium -**見積時間**: 1.5h -**成果物**: 強化された検証機能 - -#### T015: ファイル最適化機能 -- [ ] MPS/LPファイルの最適化(サイズ削減) -- [ ] 冗長な制約の検出と削除 -- [ ] 変数名の正規化 - -**優先度**: Low -**見積時間**: 2h -**成果物**: ファイル最適化機能 - -### 3.2 エラーハンドリング強化 - -#### T016: 詳細エラー診断 -- [ ] PuLPコードエラーの詳細分析 -- [ ] ソルバーエラーの分類と説明 -- [ ] 修正提案機能 - -**優先度**: Medium -**見積時間**: 2h -**成果物**: 高度なエラー診断機能 - -#### T017: カスタム例外クラス -- [ ] MIPMCPError基底クラス実装 -- [ ] SolverError, ParseError等の専用例外 -- [ ] 例外階層の整理 - -**優先度**: Medium -**見積時間**: 1h -**成果物**: 例外クラス体系 - -## 4. Phase 3: Cloud Run対応 + コンテナ化 - -### 4.1 コンテナ化 - -#### T018: Dockerfile作成 -- [ ] マルチステージビルドDockerfile -- [ ] Python環境とソルバー依存関係 -- [ ] セキュリティ最適化 - -**優先度**: High -**見積時間**: 2h -**成果物**: Dockerfile - -#### T019: Docker環境テスト -- [ ] ローカルDockerビルドテスト -- [ ] コンテナ内でのPuLP + pyscipopt動作確認 -- [ ] メモリ・CPU使用量測定 - -**優先度**: High -**見積時間**: 1h -**成果物**: 動作確認済みコンテナ - -### 4.2 Cloud Run対応 - -#### T020: Cloud Run設定 -- [ ] service.yaml作成 -- [ ] 環境変数設定 -- [ ] リソース制限設定 - -**優先度**: High -**見積時間**: 1h -**成果物**: Cloud Run設定ファイル - -#### T021: ヘルスチェック実装 -- [ ] /health エンドポイント実装 -- [ ] システム状態監視機能 -- [ ] 依存関係チェック - -**優先度**: High -**見積時間**: 1h -**成果物**: ヘルスチェック機能 - -#### T022: Cloud Runデプロイメント -- [ ] gcloud CLIによるデプロイ -- [ ] 動作確認テスト -- [ ] パフォーマンス測定 - -**優先度**: High -**見積時間**: 1h -**成果物**: デプロイ済みサービス - -## 5. テスト実装 - -### 5.1 ユニットテスト - -#### T023: コアコンポーネントテスト -- [ ] PuLP実行エンジンテスト -- [ ] ソルバー統合テスト -- [ ] データモデルテスト - -**優先度**: High -**見積時間**: 3h -**成果物**: ユニットテストスイート - -#### T024: セキュリティテスト -- [ ] 危険なコードの検出テスト -- [ ] サンドボックス機能テスト -- [ ] 入力検証テスト - -**優先度**: High -**見積時間**: 2h -**成果物**: セキュリティテストスイート - -### 5.2 統合テスト - -#### T025: エンドツーエンドテスト -- [ ] PuLP → MPS/LP → 解決フローテスト -- [ ] MCPクライアント統合テスト -- [ ] エラーケーステスト - -**優先度**: High -**見積時間**: 2h -**成果物**: 統合テストスイート - -#### T026: Cloud Run統合テスト -- [ ] コンテナ環境でのテスト -- [ ] Cloud Run環境でのテスト -- [ ] パフォーマンステスト - -**優先度**: Medium -**見積時間**: 1.5h -**成果物**: クラウド統合テスト - -## 6. ドキュメント作成 - -### 6.1 開発者向けドキュメント - -#### T027: API仕様書 -- [ ] MCPツール仕様 -- [ ] リクエスト・レスポンス形式 -- [ ] エラーコード一覧 - -**優先度**: Medium -**見積時間**: 2h -**成果物**: API仕様書 - -#### T028: 開発環境セットアップガイド -- [ ] 依存関係インストール手順 -- [ ] 開発環境構築手順 -- [ ] テスト実行方法 - -**優先度**: Medium -**見積時間**: 1h -**成果物**: 開発者ガイド - -### 6.2 運用ドキュメント - -#### T029: デプロイメントガイド -- [ ] Cloud Runデプロイ手順 -- [ ] 設定オプション説明 -- [ ] トラブルシューティング - -**優先度**: Medium -**見積時間**: 1.5h -**成果物**: デプロイメントガイド - -#### T030: 使用例とチュートリアル -- [ ] PuLPコード例 -- [ ] 典型的な最適化問題の解決例 -- [ ] トラブルシューティング事例 - -**優先度**: Low -**見積時間**: 2h -**成果物**: チュートリアル - -## 7. 実装順序とマイルストーン - -### Milestone 1: 基本機能完成 (Phase 1) -**期間**: 2-3日 -**成果物**: 動作するMCP Server + PuLP + pyscipopt統合 - -**クリティカルパス**: -T001 → T002 → T003 → T006 → T007 → T008 → T010 → T011 → T012 → T013 - -### Milestone 2: 機能強化 (Phase 2) -**期間**: 1-2日 -**成果物**: エラーハンドリング強化、ファイル処理改善 - -**並行実装可能**: -- T014, T015 (ファイル処理) -- T016, T017 (エラーハンドリング) - -### Milestone 3: クラウド対応 (Phase 3) -**期間**: 1-2日 -**成果物**: Cloud Run対応完了 - -**実装順序**: -T018 → T019 → T020 → T021 → T022 - -### Milestone 4: 品質保証 -**期間**: 2-3日 -**成果物**: テスト完備、ドキュメント整備 - -**並行実装可能**: -- T023, T024, T025 (テスト) -- T027, T028, T029 (ドキュメント) - -## 8. リスク対応 - -### 高リスクタスク -- **T006**: セキュリティチェッカー - 複雑なAST解析 -- **T008**: MPS/LP生成 - PuLPの出力形式への依存 -- **T018**: Dockerfile - ソルバー依存関係の複雑性 - -### 代替案 -- セキュリティチェッカーが複雑な場合 → 基本的な文字列チェックで代替 -- MPS/LP生成に問題がある場合 → JSON形式での中間表現を使用 -- Docker化に問題がある場合 → 標準Python環境での実行を優先 - -## 9. 品質基準 - -### コード品質 -- テストカバレージ > 80% -- 型ヒント完備 -- ドキュメント文字列完備 - -### パフォーマンス -- PuLPコード実行 < 30秒 -- MPS/LP生成 < 5秒 -- MCPレスポンス < 1秒 (ソルバー実行除く) - -### セキュリティ -- 危険なコードの検出率 > 95% -- サンドボックス環境での隔離 -- 入力検証完備