runtime.py 9.1 KB

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