runtime.py 5.5 KB

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