runtime.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import asyncio
  2. from abc import abstractmethod
  3. from typing import Any, Optional
  4. from opendevin.core.config import config
  5. from opendevin.core.exceptions import BrowserInitException
  6. from opendevin.core.logger import opendevin_logger as logger
  7. from opendevin.events import EventSource, EventStream, EventStreamSubscriber
  8. from opendevin.events.action import (
  9. Action,
  10. AgentRecallAction,
  11. BrowseInteractiveAction,
  12. BrowseURLAction,
  13. CmdKillAction,
  14. CmdRunAction,
  15. FileReadAction,
  16. FileWriteAction,
  17. IPythonRunCellAction,
  18. )
  19. from opendevin.events.event import Event
  20. from opendevin.events.observation import (
  21. CmdOutputObservation,
  22. ErrorObservation,
  23. NullObservation,
  24. Observation,
  25. )
  26. from opendevin.events.serialization.action import ACTION_TYPE_TO_CLASS
  27. from opendevin.runtime import (
  28. DockerSSHBox,
  29. E2BBox,
  30. LocalBox,
  31. Sandbox,
  32. )
  33. from opendevin.runtime.browser.browser_env import BrowserEnv
  34. from opendevin.runtime.plugins import PluginRequirement
  35. from opendevin.runtime.tools import RuntimeTool
  36. from opendevin.storage import FileStore, InMemoryFileStore
  37. def create_sandbox(sid: str = 'default', sandbox_type: str = 'ssh') -> Sandbox:
  38. if sandbox_type == 'local':
  39. return LocalBox()
  40. elif sandbox_type == 'ssh':
  41. return DockerSSHBox(sid=sid)
  42. elif sandbox_type == 'e2b':
  43. return E2BBox()
  44. else:
  45. raise ValueError(f'Invalid sandbox type: {sandbox_type}')
  46. class Runtime:
  47. """
  48. The runtime is how the agent interacts with the external environment.
  49. This includes a bash sandbox, a browser, and filesystem interactions.
  50. sid is the session id, which is used to identify the current user session.
  51. """
  52. sid: str
  53. file_store: FileStore
  54. def __init__(
  55. self,
  56. event_stream: EventStream,
  57. sid: str = 'default',
  58. sandbox: Sandbox | None = None,
  59. ):
  60. self.sid = sid
  61. if sandbox is None:
  62. self.sandbox = create_sandbox(sid, config.sandbox_type)
  63. self._is_external_sandbox = False
  64. else:
  65. self.sandbox = sandbox
  66. self._is_external_sandbox = True
  67. self.browser: BrowserEnv | None = None
  68. self.file_store = InMemoryFileStore()
  69. self.event_stream = event_stream
  70. self.event_stream.subscribe(EventStreamSubscriber.RUNTIME, self.on_event)
  71. self._bg_task = asyncio.create_task(self._start_background_observation_loop())
  72. def close(self):
  73. if not self._is_external_sandbox:
  74. self.sandbox.close()
  75. if self.browser is not None:
  76. self.browser.close()
  77. self._bg_task.cancel()
  78. def init_sandbox_plugins(self, plugins: list[PluginRequirement]) -> None:
  79. self.sandbox.init_plugins(plugins)
  80. def init_runtime_tools(
  81. self,
  82. runtime_tools: list[RuntimeTool],
  83. runtime_tools_config: Optional[dict[RuntimeTool, Any]] = None,
  84. is_async: bool = True,
  85. ) -> None:
  86. # if browser in runtime_tools, init it
  87. if RuntimeTool.BROWSER in runtime_tools:
  88. if runtime_tools_config is None:
  89. runtime_tools_config = {}
  90. browser_env_config = runtime_tools_config.get(RuntimeTool.BROWSER, {})
  91. try:
  92. self.browser = BrowserEnv(is_async=is_async, **browser_env_config)
  93. except BrowserInitException:
  94. logger.warn(
  95. 'Failed to start browser environment, web browsing functionality will not work'
  96. )
  97. async def on_event(self, event: Event) -> None:
  98. if isinstance(event, Action):
  99. observation = await self.run_action(event)
  100. observation._cause = event.id # type: ignore[attr-defined]
  101. source = event.source if event.source else EventSource.AGENT
  102. self.event_stream.add_event(observation, source)
  103. async def run_action(self, action: Action) -> Observation:
  104. """
  105. Run an action and return the resulting observation.
  106. If the action is not runnable in any runtime, a NullObservation is returned.
  107. If the action is not supported by the current runtime, an ErrorObservation is returned.
  108. """
  109. if not action.runnable:
  110. return NullObservation('')
  111. action_type = action.action # type: ignore[attr-defined]
  112. if action_type not in ACTION_TYPE_TO_CLASS:
  113. return ErrorObservation(f'Action {action_type} does not exist.')
  114. if not hasattr(self, action_type):
  115. return ErrorObservation(
  116. f'Action {action_type} is not supported in the current runtime.'
  117. )
  118. observation = await getattr(self, action_type)(action)
  119. observation._parent = action.id # type: ignore[attr-defined]
  120. return observation
  121. async def _start_background_observation_loop(self):
  122. while True:
  123. await self.submit_background_obs()
  124. await asyncio.sleep(1)
  125. async def submit_background_obs(self):
  126. """
  127. Returns all observations that have accumulated in the runtime's background.
  128. Right now, this is just background commands, but could include e.g. asynchronous
  129. events happening in the browser.
  130. """
  131. for _id, cmd in self.sandbox.background_commands.items():
  132. output = cmd.read_logs()
  133. if output:
  134. self.event_stream.add_event(
  135. CmdOutputObservation(
  136. content=output, command_id=_id, command=cmd.command
  137. ),
  138. EventSource.AGENT, # FIXME: use the original action's source
  139. )
  140. await asyncio.sleep(1)
  141. @abstractmethod
  142. async def run(self, action: CmdRunAction) -> Observation:
  143. pass
  144. @abstractmethod
  145. async def kill(self, action: CmdKillAction) -> Observation:
  146. pass
  147. @abstractmethod
  148. async def run_ipython(self, action: IPythonRunCellAction) -> Observation:
  149. pass
  150. @abstractmethod
  151. async def read(self, action: FileReadAction) -> Observation:
  152. pass
  153. @abstractmethod
  154. async def write(self, action: FileWriteAction) -> Observation:
  155. pass
  156. @abstractmethod
  157. async def browse(self, action: BrowseURLAction) -> Observation:
  158. pass
  159. @abstractmethod
  160. async def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
  161. pass
  162. @abstractmethod
  163. async def recall(self, action: AgentRecallAction) -> Observation:
  164. pass