test_ipython.py 17 KB

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