runtime.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. from typing import Any, Optional
  2. from opendevin.core.config import AppConfig
  3. from opendevin.core.exceptions import BrowserInitException
  4. from opendevin.core.logger import opendevin_logger as logger
  5. from opendevin.events.action import (
  6. BrowseInteractiveAction,
  7. BrowseURLAction,
  8. CmdRunAction,
  9. FileReadAction,
  10. FileWriteAction,
  11. IPythonRunCellAction,
  12. )
  13. from opendevin.events.observation import (
  14. CmdOutputObservation,
  15. ErrorObservation,
  16. IPythonRunCellObservation,
  17. Observation,
  18. )
  19. from opendevin.events.stream import EventStream
  20. from opendevin.runtime import (
  21. DockerSSHBox,
  22. E2BBox,
  23. LocalBox,
  24. Sandbox,
  25. )
  26. from opendevin.runtime.browser.browser_env import BrowserEnv
  27. from opendevin.runtime.plugins import PluginRequirement
  28. from opendevin.runtime.runtime import Runtime
  29. from opendevin.runtime.tools import RuntimeTool
  30. from opendevin.storage.local import LocalFileStore
  31. from ..browser import browse
  32. from .files import read_file, write_file
  33. class ServerRuntime(Runtime):
  34. def __init__(
  35. self,
  36. config: AppConfig,
  37. event_stream: EventStream,
  38. sid: str = 'default',
  39. sandbox: Sandbox | None = None,
  40. ):
  41. super().__init__(config, event_stream, sid)
  42. self.file_store = LocalFileStore(config.workspace_base)
  43. if sandbox is None:
  44. self.sandbox = self.create_sandbox(sid, config.sandbox.box_type)
  45. self._is_external_sandbox = False
  46. else:
  47. self.sandbox = sandbox
  48. self._is_external_sandbox = True
  49. self.browser: BrowserEnv | None = None
  50. def create_sandbox(self, sid: str = 'default', box_type: str = 'ssh') -> Sandbox:
  51. if box_type == 'local':
  52. return LocalBox(
  53. config=self.config.sandbox, workspace_base=self.config.workspace_base
  54. )
  55. elif box_type == 'ssh':
  56. return DockerSSHBox(
  57. config=self.config.sandbox,
  58. persist_sandbox=self.config.persist_sandbox,
  59. workspace_mount_path=self.config.workspace_mount_path,
  60. sandbox_workspace_dir=self.config.workspace_mount_path_in_sandbox,
  61. cache_dir=self.config.cache_dir,
  62. run_as_devin=self.config.run_as_devin,
  63. ssh_hostname=self.config.ssh_hostname,
  64. ssh_password=self.config.ssh_password,
  65. ssh_port=self.config.ssh_port,
  66. sid=sid,
  67. )
  68. elif box_type == 'e2b':
  69. return E2BBox(
  70. config=self.config.sandbox,
  71. e2b_api_key=self.config.e2b_api_key,
  72. )
  73. else:
  74. raise ValueError(f'Invalid sandbox type: {box_type}')
  75. async def ainit(self, env_vars: dict[str, str] | None = None):
  76. # MUST call super().ainit() to initialize both default env vars
  77. # AND the ones in env vars!
  78. await super().ainit(env_vars)
  79. async def close(self):
  80. if hasattr(self, '_is_external_sandbox') and not self._is_external_sandbox:
  81. self.sandbox.close()
  82. if hasattr(self, 'browser') and self.browser is not None:
  83. self.browser.close()
  84. def init_sandbox_plugins(self, plugins: list[PluginRequirement]) -> None:
  85. self.sandbox.init_plugins(plugins)
  86. def init_runtime_tools(
  87. self,
  88. runtime_tools: list[RuntimeTool],
  89. runtime_tools_config: Optional[dict[RuntimeTool, Any]] = None,
  90. is_async: bool = True,
  91. ) -> None:
  92. # if browser in runtime_tools, init it
  93. if RuntimeTool.BROWSER in runtime_tools:
  94. if runtime_tools_config is None:
  95. runtime_tools_config = {}
  96. browser_env_config = runtime_tools_config.get(RuntimeTool.BROWSER, {})
  97. try:
  98. self.browser = BrowserEnv(is_async=is_async, **browser_env_config)
  99. except BrowserInitException:
  100. logger.warn(
  101. 'Failed to start browser environment, web browsing functionality will not work'
  102. )
  103. async def run(self, action: CmdRunAction) -> Observation:
  104. return self._run_command(action.command)
  105. async def run_ipython(self, action: IPythonRunCellAction) -> Observation:
  106. self._run_command(
  107. f"cat > /tmp/opendevin_jupyter_temp.py <<'EOL'\n{action.code}\nEOL"
  108. )
  109. # run the code
  110. obs = self._run_command('cat /tmp/opendevin_jupyter_temp.py | execute_cli')
  111. output = obs.content
  112. if 'pip install' in action.code:
  113. print(output)
  114. package_names = action.code.split(' ', 2)[-1]
  115. is_single_package = ' ' not in package_names
  116. if 'Successfully installed' in output:
  117. restart_kernel = 'import IPython\nIPython.Application.instance().kernel.do_shutdown(True)'
  118. if (
  119. 'Note: you may need to restart the kernel to use updated packages.'
  120. in output
  121. ):
  122. self._run_command(
  123. (
  124. "cat > /tmp/opendevin_jupyter_temp.py <<'EOL'\n"
  125. f'{restart_kernel}\n'
  126. 'EOL'
  127. )
  128. )
  129. obs = self._run_command(
  130. 'cat /tmp/opendevin_jupyter_temp.py | execute_cli'
  131. )
  132. output = '[Package installed successfully]'
  133. if "{'status': 'ok', 'restart': True}" != obs.content.strip():
  134. print(obs.content)
  135. output += (
  136. '\n[But failed to restart the kernel to load the package]'
  137. )
  138. else:
  139. output += (
  140. '\n[Kernel restarted successfully to load the package]'
  141. )
  142. # re-init the kernel after restart
  143. if action.kernel_init_code:
  144. self._run_command(
  145. (
  146. f"cat > /tmp/opendevin_jupyter_init.py <<'EOL'\n"
  147. f'{action.kernel_init_code}\n'
  148. 'EOL'
  149. ),
  150. )
  151. obs = self._run_command(
  152. 'cat /tmp/opendevin_jupyter_init.py | execute_cli',
  153. )
  154. elif (
  155. is_single_package
  156. and f'Requirement already satisfied: {package_names}' in output
  157. ):
  158. output = '[Package already installed]'
  159. return IPythonRunCellObservation(content=output, code=action.code)
  160. async def read(self, action: FileReadAction) -> Observation:
  161. # TODO: use self.file_store
  162. working_dir = self.sandbox.get_working_directory()
  163. return await read_file(
  164. action.path,
  165. working_dir,
  166. self.config.workspace_base,
  167. self.config.workspace_mount_path_in_sandbox,
  168. action.start,
  169. action.end,
  170. )
  171. async def write(self, action: FileWriteAction) -> Observation:
  172. # TODO: use self.file_store
  173. working_dir = self.sandbox.get_working_directory()
  174. return await write_file(
  175. action.path,
  176. working_dir,
  177. self.config.workspace_base,
  178. self.config.workspace_mount_path_in_sandbox,
  179. action.content,
  180. action.start,
  181. action.end,
  182. )
  183. async def browse(self, action: BrowseURLAction) -> Observation:
  184. return await browse(action, self.browser)
  185. async def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
  186. return await browse(action, self.browser)
  187. def _run_command(self, command: str) -> Observation:
  188. try:
  189. exit_code, output = self.sandbox.execute(command)
  190. if 'pip install' in command:
  191. package_names = command.split(' ', 2)[-1]
  192. is_single_package = ' ' not in package_names
  193. print(output)
  194. if 'Successfully installed' in output:
  195. output = '[Package installed successfully]'
  196. elif (
  197. is_single_package
  198. and f'Requirement already satisfied: {package_names}' in output
  199. ):
  200. output = '[Package already installed]'
  201. return CmdOutputObservation(
  202. command_id=-1, content=str(output), command=command, exit_code=exit_code
  203. )
  204. except UnicodeDecodeError:
  205. return ErrorObservation('Command output could not be decoded as utf-8')