runner.py 3.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  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. )
  24. try:
  25. completed = subprocess.run(
  26. [self.settings.python_bin, str(script_file), str(input_file)],
  27. capture_output=True,
  28. text=True,
  29. encoding="utf-8",
  30. timeout=payload.timeout_seconds,
  31. check=False,
  32. )
  33. except subprocess.TimeoutExpired as exc:
  34. return CodeExecutionResponseContract(
  35. success=False,
  36. stderr=exc.stderr or "",
  37. error_message=f"code execution timed out after {payload.timeout_seconds} seconds",
  38. )
  39. except OSError as exc:
  40. raise CodeRunnerError(f"failed to start python runner: {exc}") from exc
  41. stdout = completed.stdout
  42. stderr = completed.stderr
  43. output_json = _extract_result_json(stdout)
  44. success = completed.returncode == 0
  45. error_message = None if success else f"python exited with code {completed.returncode}"
  46. return CodeExecutionResponseContract(
  47. success=success,
  48. stdout=stdout,
  49. stderr=stderr,
  50. output_json=output_json,
  51. error_message=error_message,
  52. )
  53. def _build_python_runner_script(user_code: str) -> str:
  54. escaped_code = json.dumps(user_code)
  55. return (
  56. "import json\n"
  57. "import pathlib\n"
  58. "import sys\n"
  59. "\n"
  60. "input_path = pathlib.Path(sys.argv[1])\n"
  61. "payload = json.loads(input_path.read_text(encoding='utf-8'))\n"
  62. "namespace = {\n"
  63. " 'payload': payload,\n"
  64. " 'result': None,\n"
  65. "}\n"
  66. f"exec({escaped_code}, namespace, namespace)\n"
  67. "print('\\n__RESULT_JSON__=' + json.dumps(namespace.get('result'), ensure_ascii=False))\n"
  68. )
  69. def _extract_result_json(stdout: str) -> dict[str, JSONValue]:
  70. marker = "__RESULT_JSON__="
  71. lines = stdout.splitlines()
  72. for index in range(len(lines) - 1, -1, -1):
  73. line = lines[index]
  74. if not line.startswith(marker):
  75. continue
  76. raw_payload = line[len(marker) :]
  77. try:
  78. payload = json.loads(raw_payload)
  79. except json.JSONDecodeError:
  80. return {}
  81. if isinstance(payload, dict):
  82. return {str(key): value for key, value in payload.items()}
  83. return {"result": payload}
  84. return {}