runner.py 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import json
  2. import subprocess
  3. import tempfile
  4. from pathlib import Path
  5. from core_domain import CodeExecutionRequestContract, CodeExecutionResponseContract
  6. from core_shared import JSONValue
  7. from app.bootstrap.settings import CodeRunnerServiceSettings
  8. class CodeRunnerError(Exception):
  9. pass
  10. class PythonCodeRunner:
  11. def __init__(self, *, settings: CodeRunnerServiceSettings) -> None:
  12. self.settings = settings
  13. def execute(self, payload: CodeExecutionRequestContract) -> CodeExecutionResponseContract:
  14. script_content = _build_python_runner_script(payload.code)
  15. with tempfile.TemporaryDirectory(prefix="agent-platform-code-") as temp_dir:
  16. temp_path = Path(temp_dir)
  17. script_file = temp_path / "runner.py"
  18. input_file = temp_path / "input.json"
  19. script_file.write_text(script_content, encoding="utf-8")
  20. input_file.write_text(
  21. json.dumps(payload.input_json, ensure_ascii=False),
  22. encoding="utf-8")
  23. try:
  24. completed = subprocess.run(
  25. [self.settings.python_bin, str(script_file), str(input_file)],
  26. capture_output=True,
  27. text=True,
  28. encoding="utf-8",
  29. timeout=payload.timeout_seconds,
  30. check=False)
  31. except subprocess.TimeoutExpired as exc:
  32. return CodeExecutionResponseContract(
  33. success=False,
  34. stderr=exc.stderr or "",
  35. error_message=f"code execution timed out after {payload.timeout_seconds} seconds")
  36. except OSError as exc:
  37. raise CodeRunnerError(f"failed to start python runner: {exc}") from exc
  38. stdout = completed.stdout
  39. stderr = completed.stderr
  40. output_json = _extract_result_json(stdout)
  41. success = completed.returncode == 0
  42. error_message = None if success else f"python exited with code {completed.returncode}"
  43. return CodeExecutionResponseContract(
  44. success=success,
  45. stdout=stdout,
  46. stderr=stderr,
  47. output_json=output_json,
  48. error_message=error_message)
  49. def _build_python_runner_script(user_code: str) -> str:
  50. escaped_code = json.dumps(user_code)
  51. return (
  52. "import json\n"
  53. "import pathlib\n"
  54. "import sys\n"
  55. "\n"
  56. "input_path = pathlib.Path(sys.argv[1])\n"
  57. "payload = json.loads(input_path.read_text(encoding='utf-8'))\n"
  58. "namespace = {\n"
  59. " 'payload': payload,\n"
  60. " 'result': None,\n"
  61. "}\n"
  62. f"exec({escaped_code}, namespace, namespace)\n"
  63. "print('\\n__RESULT_JSON__=' + json.dumps(namespace.get('result'), ensure_ascii=False))\n"
  64. )
  65. def _extract_result_json(stdout: str) -> dict[str, JSONValue]:
  66. marker = "__RESULT_JSON__="
  67. lines = stdout.splitlines()
  68. for index in range(len(lines) - 1, -1, -1):
  69. line = lines[index]
  70. if not line.startswith(marker):
  71. continue
  72. raw_payload = line[len(marker) :]
  73. try:
  74. payload = json.loads(raw_payload)
  75. except json.JSONDecodeError:
  76. return {}
  77. if isinstance(payload, dict):
  78. return {str(key): value for key, value in payload.items()}
  79. return {"result": payload}
  80. return {}