test_ipython.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. """Test the EventStreamRuntime, which connects to the RuntimeClient running in the sandbox."""
  2. import asyncio
  3. import pytest
  4. from conftest import _load_runtime
  5. from openhands.core.logger import openhands_logger as logger
  6. from openhands.events.action import (
  7. CmdRunAction,
  8. FileReadAction,
  9. FileWriteAction,
  10. IPythonRunCellAction,
  11. )
  12. from openhands.events.observation import (
  13. CmdOutputObservation,
  14. ErrorObservation,
  15. FileReadObservation,
  16. FileWriteObservation,
  17. IPythonRunCellObservation,
  18. )
  19. from openhands.runtime.client.runtime import EventStreamRuntime
  20. # ============================================================================================================================
  21. # ipython-specific tests
  22. # ============================================================================================================================
  23. @pytest.mark.asyncio
  24. async def test_simple_cmd_ipython_and_fileop(temp_dir, box_class, run_as_openhands):
  25. runtime = await _load_runtime(temp_dir, box_class, run_as_openhands)
  26. # Test run command
  27. action_cmd = CmdRunAction(command='ls -l')
  28. logger.info(action_cmd, extra={'msg_type': 'ACTION'})
  29. obs = await runtime.run_action(action_cmd)
  30. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  31. assert isinstance(obs, CmdOutputObservation)
  32. assert obs.exit_code == 0
  33. assert 'total 0' in obs.content
  34. # Test run ipython
  35. test_code = "print('Hello, `World`!\\n')"
  36. action_ipython = IPythonRunCellAction(code=test_code)
  37. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  38. obs = await runtime.run_action(action_ipython)
  39. assert isinstance(obs, IPythonRunCellObservation)
  40. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  41. assert obs.content.strip() == (
  42. 'Hello, `World`!\n'
  43. '[Jupyter current working directory: /workspace]\n'
  44. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  45. )
  46. # Test read file (file should not exist)
  47. action_read = FileReadAction(path='hello.sh')
  48. logger.info(action_read, extra={'msg_type': 'ACTION'})
  49. obs = await runtime.run_action(action_read)
  50. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  51. assert isinstance(obs, ErrorObservation)
  52. assert 'File not found' in obs.content
  53. # Test write file
  54. action_write = FileWriteAction(content='echo "Hello, World!"', path='hello.sh')
  55. logger.info(action_write, extra={'msg_type': 'ACTION'})
  56. obs = await runtime.run_action(action_write)
  57. assert isinstance(obs, FileWriteObservation)
  58. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  59. assert obs.content == ''
  60. # event stream runtime will always use absolute path
  61. assert obs.path == '/workspace/hello.sh'
  62. # Test read file (file should exist)
  63. action_read = FileReadAction(path='hello.sh')
  64. logger.info(action_read, extra={'msg_type': 'ACTION'})
  65. obs = await runtime.run_action(action_read)
  66. assert isinstance(
  67. obs, FileReadObservation
  68. ), 'The observation should be a FileReadObservation.'
  69. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  70. assert obs.content == 'echo "Hello, World!"\n'
  71. assert obs.path == '/workspace/hello.sh'
  72. # clean up
  73. action = CmdRunAction(command='rm -rf hello.sh')
  74. logger.info(action, extra={'msg_type': 'ACTION'})
  75. obs = await runtime.run_action(action)
  76. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  77. assert obs.exit_code == 0
  78. await runtime.close()
  79. await asyncio.sleep(1)
  80. @pytest.mark.asyncio
  81. async def test_ipython_multi_user(temp_dir, box_class, run_as_openhands):
  82. runtime = await _load_runtime(temp_dir, box_class, run_as_openhands)
  83. # Test run ipython
  84. # get username
  85. test_code = "import os; print(os.environ['USER'])"
  86. action_ipython = IPythonRunCellAction(code=test_code)
  87. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  88. obs = await runtime.run_action(action_ipython)
  89. assert isinstance(obs, IPythonRunCellObservation)
  90. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  91. if run_as_openhands:
  92. assert 'openhands' in obs.content
  93. else:
  94. assert 'root' in obs.content
  95. # print pwd
  96. test_code = 'import os; print(os.getcwd())'
  97. action_ipython = IPythonRunCellAction(code=test_code)
  98. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  99. obs = await runtime.run_action(action_ipython)
  100. assert isinstance(obs, IPythonRunCellObservation)
  101. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  102. assert (
  103. obs.content.strip()
  104. == (
  105. '/workspace\n'
  106. '[Jupyter current working directory: /workspace]\n'
  107. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  108. ).strip()
  109. )
  110. # write a file
  111. test_code = "with open('test.txt', 'w') as f: f.write('Hello, world!')"
  112. action_ipython = IPythonRunCellAction(code=test_code)
  113. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  114. obs = await runtime.run_action(action_ipython)
  115. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  116. assert isinstance(obs, IPythonRunCellObservation)
  117. assert (
  118. obs.content.strip()
  119. == (
  120. '[Code executed successfully with no output]\n'
  121. '[Jupyter current working directory: /workspace]\n'
  122. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  123. ).strip()
  124. )
  125. # check file owner via bash
  126. action = CmdRunAction(command='ls -alh test.txt')
  127. logger.info(action, extra={'msg_type': 'ACTION'})
  128. obs = await runtime.run_action(action)
  129. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  130. assert obs.exit_code == 0
  131. if run_as_openhands:
  132. # -rw-r--r-- 1 openhands root 13 Jul 28 03:53 test.txt
  133. assert 'openhands' in obs.content.split('\r\n')[0]
  134. assert 'root' in obs.content.split('\r\n')[0]
  135. else:
  136. # -rw-r--r-- 1 root root 13 Jul 28 03:53 test.txt
  137. assert 'root' in obs.content.split('\r\n')[0]
  138. # clean up
  139. action = CmdRunAction(command='rm -rf test')
  140. logger.info(action, extra={'msg_type': 'ACTION'})
  141. obs = await runtime.run_action(action)
  142. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  143. assert obs.exit_code == 0
  144. await runtime.close()
  145. await asyncio.sleep(1)
  146. @pytest.mark.asyncio
  147. async def test_ipython_simple(temp_dir, box_class):
  148. runtime = await _load_runtime(temp_dir, box_class)
  149. # Test run ipython
  150. # get username
  151. test_code = 'print(1)'
  152. action_ipython = IPythonRunCellAction(code=test_code)
  153. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  154. obs = await runtime.run_action(action_ipython)
  155. assert isinstance(obs, IPythonRunCellObservation)
  156. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  157. assert (
  158. obs.content.strip()
  159. == (
  160. '1\n'
  161. '[Jupyter current working directory: /workspace]\n'
  162. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  163. ).strip()
  164. )
  165. await runtime.close()
  166. await asyncio.sleep(1)
  167. async def _test_ipython_agentskills_fileop_pwd_impl(
  168. runtime: EventStreamRuntime, enable_auto_lint: bool
  169. ):
  170. # remove everything in /workspace
  171. action = CmdRunAction(command='rm -rf /workspace/*')
  172. logger.info(action, extra={'msg_type': 'ACTION'})
  173. obs = await runtime.run_action(action)
  174. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  175. assert obs.exit_code == 0
  176. action = CmdRunAction(command='mkdir test')
  177. logger.info(action, extra={'msg_type': 'ACTION'})
  178. obs = await runtime.run_action(action)
  179. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  180. assert isinstance(obs, CmdOutputObservation)
  181. assert obs.exit_code == 0
  182. action = IPythonRunCellAction(code="create_file('hello.py')")
  183. logger.info(action, extra={'msg_type': 'ACTION'})
  184. obs = await runtime.run_action(action)
  185. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  186. assert isinstance(obs, IPythonRunCellObservation)
  187. assert obs.content.replace('\r\n', '\n').strip().split('\n') == (
  188. '[File: /workspace/hello.py (1 lines total)]\n'
  189. '(this is the beginning of the file)\n'
  190. '1|\n'
  191. '(this is the end of the file)\n'
  192. '[File hello.py created.]\n'
  193. '[Jupyter current working directory: /workspace]\n'
  194. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  195. ).strip().split('\n')
  196. action = CmdRunAction(command='cd test')
  197. logger.info(action, extra={'msg_type': 'ACTION'})
  198. obs = await runtime.run_action(action)
  199. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  200. assert isinstance(obs, CmdOutputObservation)
  201. assert obs.exit_code == 0
  202. # This should create a file in the current working directory
  203. # i.e., /workspace/test/hello.py instead of /workspace/hello.py
  204. action = IPythonRunCellAction(code="create_file('hello.py')")
  205. logger.info(action, extra={'msg_type': 'ACTION'})
  206. obs = await runtime.run_action(action)
  207. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  208. assert isinstance(obs, IPythonRunCellObservation)
  209. assert obs.content.replace('\r\n', '\n').strip().split('\n') == (
  210. '[File: /workspace/test/hello.py (1 lines total)]\n'
  211. '(this is the beginning of the file)\n'
  212. '1|\n'
  213. '(this is the end of the file)\n'
  214. '[File hello.py created.]\n'
  215. '[Jupyter current working directory: /workspace/test]\n'
  216. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  217. ).strip().split('\n')
  218. if enable_auto_lint:
  219. # edit file, but make a mistake in indentation
  220. action = IPythonRunCellAction(
  221. code="insert_content_at_line('hello.py', 1, ' print(\"hello world\")')"
  222. )
  223. logger.info(action, extra={'msg_type': 'ACTION'})
  224. obs = await runtime.run_action(action)
  225. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  226. assert isinstance(obs, IPythonRunCellObservation)
  227. assert obs.content.replace('\r\n', '\n').strip().split('\n') == (
  228. """
  229. [Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]
  230. ERRORS:
  231. /workspace/test/hello.py:1:3: E999 IndentationError: unexpected indent
  232. [This is how your edit would have looked if applied]
  233. -------------------------------------------------
  234. (this is the beginning of the file)
  235. 1| print("hello world")
  236. (this is the end of the file)
  237. -------------------------------------------------
  238. [This is the original code before your edit]
  239. -------------------------------------------------
  240. (this is the beginning of the file)
  241. 1|
  242. (this is the end of the file)
  243. -------------------------------------------------
  244. Your changes have NOT been applied. Please fix your edit command and try again.
  245. You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.
  246. DO NOT re-run the same failed edit command. Running it again will lead to the same error.
  247. [Jupyter current working directory: /workspace/test]
  248. [Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
  249. """
  250. ).strip().split('\n')
  251. # edit file with correct indentation
  252. action = IPythonRunCellAction(
  253. code="insert_content_at_line('hello.py', 1, 'print(\"hello world\")')"
  254. )
  255. logger.info(action, extra={'msg_type': 'ACTION'})
  256. obs = await runtime.run_action(action)
  257. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  258. assert isinstance(obs, IPythonRunCellObservation)
  259. assert obs.content.replace('\r\n', '\n').strip().split('\n') == (
  260. """
  261. [File: /workspace/test/hello.py (1 lines total after edit)]
  262. (this is the beginning of the file)
  263. 1|print("hello world")
  264. (this is the end of the file)
  265. [File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
  266. [Jupyter current working directory: /workspace/test]
  267. [Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
  268. """
  269. ).strip().split('\n')
  270. action = CmdRunAction(command='rm -rf /workspace/*')
  271. logger.info(action, extra={'msg_type': 'ACTION'})
  272. obs = await runtime.run_action(action)
  273. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  274. assert obs.exit_code == 0
  275. await runtime.close()
  276. await asyncio.sleep(1)
  277. @pytest.mark.asyncio
  278. async def test_ipython_agentskills_fileop_pwd(
  279. temp_dir, box_class, run_as_openhands, enable_auto_lint
  280. ):
  281. """Make sure that cd in bash also update the current working directory in ipython."""
  282. runtime = await _load_runtime(
  283. temp_dir, box_class, run_as_openhands, enable_auto_lint=enable_auto_lint
  284. )
  285. await _test_ipython_agentskills_fileop_pwd_impl(runtime, enable_auto_lint)
  286. await runtime.close()
  287. await asyncio.sleep(1)
  288. @pytest.mark.asyncio
  289. async def test_ipython_agentskills_fileop_pwd_with_userdir(temp_dir, box_class):
  290. """Make sure that cd in bash also update the current working directory in ipython.
  291. Handle special case where the pwd is provided as "~", which should be expanded using os.path.expanduser
  292. on the client side.
  293. """
  294. runtime = await _load_runtime(
  295. temp_dir,
  296. box_class,
  297. run_as_openhands=False,
  298. )
  299. action = CmdRunAction(command='cd ~')
  300. logger.info(action, extra={'msg_type': 'ACTION'})
  301. obs = await runtime.run_action(action)
  302. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  303. assert obs.exit_code == 0
  304. action = CmdRunAction(command='mkdir test && ls -la')
  305. logger.info(action, extra={'msg_type': 'ACTION'})
  306. obs = await runtime.run_action(action)
  307. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  308. assert isinstance(obs, CmdOutputObservation)
  309. assert obs.exit_code == 0
  310. action = IPythonRunCellAction(code="create_file('hello.py')")
  311. logger.info(action, extra={'msg_type': 'ACTION'})
  312. obs = await runtime.run_action(action)
  313. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  314. assert isinstance(obs, IPythonRunCellObservation)
  315. assert obs.content.replace('\r\n', '\n').strip().split('\n') == (
  316. '[File: /root/hello.py (1 lines total)]\n'
  317. '(this is the beginning of the file)\n'
  318. '1|\n'
  319. '(this is the end of the file)\n'
  320. '[File hello.py created.]\n'
  321. '[Jupyter current working directory: /root]\n'
  322. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  323. ).strip().split('\n')
  324. action = CmdRunAction(command='cd test')
  325. logger.info(action, extra={'msg_type': 'ACTION'})
  326. obs = await runtime.run_action(action)
  327. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  328. assert isinstance(obs, CmdOutputObservation)
  329. assert obs.exit_code == 0
  330. # This should create a file in the current working directory
  331. # i.e., /workspace/test/hello.py instead of /workspace/hello.py
  332. action = IPythonRunCellAction(code="create_file('hello.py')")
  333. logger.info(action, extra={'msg_type': 'ACTION'})
  334. obs = await runtime.run_action(action)
  335. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  336. assert isinstance(obs, IPythonRunCellObservation)
  337. assert obs.content.replace('\r\n', '\n').strip().split('\n') == (
  338. '[File: /root/test/hello.py (1 lines total)]\n'
  339. '(this is the beginning of the file)\n'
  340. '1|\n'
  341. '(this is the end of the file)\n'
  342. '[File hello.py created.]\n'
  343. '[Jupyter current working directory: /root/test]\n'
  344. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  345. ).strip().split('\n')
  346. await runtime.close()
  347. await asyncio.sleep(1)
  348. @pytest.mark.asyncio
  349. async def test_ipython_package_install(temp_dir, box_class, run_as_openhands):
  350. """Make sure that cd in bash also update the current working directory in ipython."""
  351. runtime = await _load_runtime(temp_dir, box_class, run_as_openhands)
  352. # It should error out since pymsgbox is not installed
  353. action = IPythonRunCellAction(code='import pymsgbox')
  354. logger.info(action, extra={'msg_type': 'ACTION'})
  355. obs = await runtime.run_action(action)
  356. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  357. assert "ModuleNotFoundError: No module named 'pymsgbox'" in obs.content
  358. # Install pymsgbox in Jupyter
  359. action = IPythonRunCellAction(code='%pip install pymsgbox==1.0.9')
  360. logger.info(action, extra={'msg_type': 'ACTION'})
  361. obs = await runtime.run_action(action)
  362. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  363. assert (
  364. 'Successfully installed pymsgbox-1.0.9' in obs.content
  365. or '[Package installed successfully]' in obs.content
  366. )
  367. action = IPythonRunCellAction(code='import pymsgbox')
  368. logger.info(action, extra={'msg_type': 'ACTION'})
  369. obs = await runtime.run_action(action)
  370. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  371. # import should not error out
  372. assert obs.content.strip() == (
  373. '[Code executed successfully with no output]\n'
  374. '[Jupyter current working directory: /workspace]\n'
  375. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]'
  376. )
  377. await runtime.close()
  378. await asyncio.sleep(1)