run_infer.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import asyncio
  2. import json
  3. import logging
  4. import multiprocessing as mp
  5. import os
  6. import pathlib
  7. import subprocess
  8. import time
  9. from concurrent.futures import ProcessPoolExecutor
  10. import pandas as pd
  11. from datasets import load_dataset
  12. from tqdm import tqdm
  13. import agenthub
  14. from evaluation.biocoder.biocoder_env_box import BiocoderData, BiocoderSSHBox
  15. from opendevin.controller.state.state import State
  16. from opendevin.core.config import args, config, get_llm_config_arg
  17. from opendevin.core.logger import get_console_handler
  18. from opendevin.core.logger import opendevin_logger as logger
  19. from opendevin.core.main import main
  20. from opendevin.events.action import MessageAction
  21. from opendevin.events.serialization.event import event_to_dict
  22. def cleanup():
  23. print('Cleaning up child processes...')
  24. for process in mp.active_children():
  25. print(f'Terminating child process: {process.name}')
  26. process.terminate()
  27. process.join()
  28. def codeact_user_response(state: State) -> str:
  29. msg = (
  30. 'Please continue working on the task on whatever approach you think is suitable.\n'
  31. 'If you think you have modified the code in a way that fixes the issue, please run the following command: <execute_bash> exit </execute_bash>.\n'
  32. 'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP OR USE THE INTERNET TO SOLVE THIS TASK.\n'
  33. )
  34. if state.history:
  35. user_msgs = [
  36. action
  37. for action, _ in state.history
  38. if isinstance(action, MessageAction) and action.source == 'user'
  39. ]
  40. if len(user_msgs) >= 2:
  41. # let the agent know that it can give up when it has tried 3 times
  42. return (
  43. msg
  44. + 'If you want to give up, run: <execute_bash> exit </execute_bash>.\n'
  45. )
  46. return msg
  47. def monologue_user_response(state: State) -> str:
  48. raise NotImplementedError('MonologueAgent should never ask for user responses.')
  49. AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
  50. 'CodeActAgent': codeact_user_response,
  51. 'MonologueAgent': monologue_user_response,
  52. }
  53. AGENT_CLS_TO_INST_SUFFIX = {
  54. 'CodeActAgent': 'When you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n'
  55. }
  56. def get_test_result(instance, sandbox, workspace_dir_name):
  57. test_result = {'result': {}, 'metadata': {}}
  58. try:
  59. code = sandbox.get_changed_code(include_signature=True)
  60. sandbox.copy_changed_code()
  61. test_result['metadata']['1_copy_change_success'] = True
  62. test_result['metadata']['1_copy_change_code'] = code
  63. except Exception:
  64. logger.error('Error fetching changed code for this instance')
  65. test_result['metadata']['1_copy_change_success'] = False
  66. test_result['metadata']['1_copy_change_code'] = None
  67. exit_code, output = sandbox.execute_and_check(
  68. 'cd /testing',
  69. 'Failed to cd /testing',
  70. )
  71. logger.info(f'cd $REPO_PATH: {output}')
  72. exit_code, output = sandbox.execute_and_check(
  73. 'whoami',
  74. 'Failed to run whoami',
  75. )
  76. logger.info(f'whoami: {output}')
  77. exit_code, output = sandbox.execute(
  78. '/home/devin/mambaforge/bin/mamba run -n test python3 /testing/start_test_opendevin.py'
  79. )
  80. logger.info(f'$TEST_CMD:\n{output}')
  81. exit_code, output = sandbox.execute_and_check(
  82. 'cat /testing_files/results_biocoder.json', 'Failed to read the result file'
  83. )
  84. if exit_code == 0:
  85. test_result['metadata']['2_run_test_success'] = True
  86. test_result['metadata']['2_run_test_result'] = str(output)
  87. else:
  88. test_result['metadata']['2_run_test_success'] = False
  89. test_result['metadata']['2_run_test_result'] = str(output)
  90. json_obj = json.loads(output)
  91. test_result['result'] = json_obj['result']
  92. return test_result
  93. def process_instance(
  94. instance,
  95. agent_class,
  96. metadata,
  97. skip_workspace_mount,
  98. eval_output_dir,
  99. reset_logger: bool = True,
  100. ):
  101. instance = BiocoderData(**instance)
  102. print(instance)
  103. workspace_dir_name = (
  104. f'{instance.repository}__{instance.test_case_id[:10]}__{os.getpid()}'.replace(
  105. '/', '__'
  106. )
  107. )
  108. workspace_mount_path = os.path.join(config.workspace_base, workspace_dir_name)
  109. # create process-specific workspace dir
  110. # if `not skip_workspace_mount` - we will create a workspace directory for EACH process
  111. # so that different agent don't interfere with each other.
  112. if not skip_workspace_mount:
  113. workspace_mount_path = os.path.join(workspace_mount_path, str(os.getpid()))
  114. pathlib.Path(workspace_mount_path).mkdir(parents=True, exist_ok=True)
  115. # Setup the logger properly, so you can run multi-processing to parallize the evaluation
  116. if reset_logger:
  117. # Set up logger
  118. log_file = os.path.join(
  119. eval_output_dir, 'logs', f'instance_{instance.test_case_id}.log'
  120. )
  121. # Remove all existing handlers from logger
  122. for handler in logger.handlers[:]:
  123. logger.removeHandler(handler)
  124. # add back the console handler to print ONE line
  125. logger.addHandler(get_console_handler())
  126. logger.info(
  127. f'Starting evaluation for instance {instance.test_case_id}.\nHint: run "tail -f {log_file}" to see live logs in a seperate shell'
  128. )
  129. # Remove all existing handlers from logger
  130. for handler in logger.handlers[:]:
  131. logger.removeHandler(handler)
  132. file_handler = logging.FileHandler(log_file)
  133. file_handler.setFormatter(
  134. logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
  135. )
  136. logger.addHandler(file_handler)
  137. if not skip_workspace_mount:
  138. logger.info(f'Process-specific workspace mounted at {workspace_mount_path}')
  139. # NOTE: this is something special we do for SWE-Bench due to the reason described in the previous section
  140. # You can omit this if you don't need to setup specialized sandbox
  141. workspace_dir_name = f'{instance.repository}__{instance.test_case_id[:10]}'.replace(
  142. '/', '__'
  143. )
  144. sandbox = BiocoderSSHBox.get_box_for_instance(
  145. instance,
  146. workspace_dir_name,
  147. skip_workspace_mount=False,
  148. workspace_mount_path=workspace_mount_path,
  149. sandbox_plugins=agenthub.Agent.get_cls(agent_class).sandbox_plugins,
  150. )
  151. sandbox.remove_code()
  152. # Prepare instruction
  153. instruction = (
  154. f'Please complete the function "{instance.signature}" in the file /workspace/{instance.repository.split("/")[1]}/{instance.filePath}.\n'
  155. f'The environment has been set up for you to start working. You may assume all necessary tools are installed.\n'
  156. f'To complete the task, you must directly modify the file and fill in the function, keeping in mind that the function signature is on line {instance.lineStart-1}\n\n'
  157. f'The function should do the following:\n'
  158. f'{instance.promptSummaryOnly}\n\n'
  159. )
  160. instruction += (
  161. 'IMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\n'
  162. 'You should NOT modify any other files other than the file intended. This means that you should NOT write any test cases.\n'
  163. 'You may need context from other files in the repository to complete this task.'
  164. 'Do NOT add any import statements or change anything else other than the writing the function body.\n'
  165. 'You do not need to run the code to check if it works. \n'
  166. 'Make sure to include proper formatting in Java and Python, including correct braces and/or indentation.\n'
  167. )
  168. # instruction = (
  169. # f'In the file {instance.filePath}, there is a function with a signature and without a body. Your job is to complete the function, according to the given instructions. When you complete the function, respond with the function body, and nothing else.'
  170. # 'The repository has cloned for you to start working. You are not allowed to run any bash commands, just modify the files. \n\n'
  171. # '# Problem Statement\n'
  172. # 'Complete the following function signature:\n\n'
  173. # f'{instance.signature}'
  174. # 'The function should do the following:\n\n'
  175. # f'{instance.promptSummaryOnly}\n\n'
  176. # )
  177. #
  178. # instruction += (
  179. # 'IMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\n'
  180. # 'You should NOT modify any other files other than the file intended. This means that you should NOT write any test cases.\n'
  181. # 'Do NOT add any import statements or change anything else other than the writing the function body.\n'
  182. # 'You do not need to run the code to check if it works. The system will automatically check the correctness of your code.\n'
  183. # 'Make sure to include proper formatting in Java and Python, including correct braces and/or indentation.\n'
  184. # )
  185. # NOTE: You can actually set slightly different instruction for different agents
  186. instruction += AGENT_CLS_TO_INST_SUFFIX.get(agent_class, '')
  187. # Here's how you can run the agent (similar to the `main` function) and get the final task state
  188. state: State = asyncio.run(
  189. main(
  190. instruction,
  191. fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(agent_class),
  192. sandbox=sandbox,
  193. )
  194. )
  195. test_result = get_test_result(instance, sandbox, workspace_dir_name)
  196. if state is None:
  197. raise ValueError('State should not be None.')
  198. metrics = state.metrics.get() if state.metrics else None
  199. # Save the output
  200. output = {
  201. 'test_case_id': instance.test_case_id,
  202. 'biocoder_instance': instance.to_dict(),
  203. 'instruction': instruction,
  204. 'generated': test_result['metadata']['1_copy_change_code'],
  205. 'metadata': metadata,
  206. 'history': [
  207. (event_to_dict(action), event_to_dict(obs)) for action, obs in state.history
  208. ],
  209. 'metrics': metrics,
  210. 'error': state.error if state and state.error else None,
  211. 'test_result': test_result,
  212. }
  213. # Close the sandbox
  214. sandbox.close()
  215. return output
  216. if __name__ == '__main__':
  217. # NOTE: It is preferable to load datasets from huggingface datasets and perform post-processing
  218. # so we don't need to manage file uploading to OpenDevin's repo
  219. dataset = load_dataset('lilbillbiscuit/biocoder_public')
  220. biocoder_tests = dataset['test'].to_pandas()
  221. # Check https://github.com/OpenDevin/OpenDevin/blob/main/evaluation/swe_bench/README.md#configure-opendevin-and-your-llm
  222. # for details of how to set `llm_config`
  223. if args.llm_config:
  224. specified_llm_config = get_llm_config_arg(args.llm_config)
  225. if specified_llm_config:
  226. config.llm = specified_llm_config
  227. logger.info(f'Config for evaluation: {config}')
  228. # TEST METADATA
  229. agent_class = args.agent_cls
  230. assert (
  231. agent_class in AGENT_CLS_TO_FAKE_USER_RESPONSE_FN
  232. ), f'Unsupported agent class: {agent_class}'
  233. model_name = config.llm.model.split('/')[-1]
  234. max_iterations = args.max_iterations
  235. eval_note = ''
  236. if args.eval_note is not None:
  237. eval_note += '_N_' + args.eval_note
  238. eval_output_dir = os.path.join(
  239. args.eval_output_dir,
  240. 'biocoder',
  241. agent_class,
  242. model_name + '_maxiter_' + str(max_iterations) + eval_note,
  243. )
  244. eval_output_dir = str(eval_output_dir)
  245. pathlib.Path(eval_output_dir).mkdir(parents=True, exist_ok=True)
  246. pathlib.Path(os.path.join(eval_output_dir, 'logs')).mkdir(
  247. parents=True, exist_ok=True
  248. )
  249. logger.info(f'Using evaluation output directory: {eval_output_dir}')
  250. metadata = {
  251. 'agent_class': agent_class,
  252. 'model_name': model_name,
  253. 'max_iterations': max_iterations,
  254. 'eval_output_dir': eval_output_dir,
  255. 'start_time': time.strftime('%Y-%m-%d %H:%M:%S'),
  256. # get the commit id of current repo for reproduciblity
  257. 'git_commit': subprocess.check_output(['git', 'rev-parse', 'HEAD'])
  258. .decode('utf-8')
  259. .strip(),
  260. }
  261. logger.info(f'Metadata: {metadata}')
  262. with open(os.path.join(eval_output_dir, 'metadata.json'), 'w') as f:
  263. json.dump(metadata, f)
  264. # LIMIT EVALUATION
  265. eval_n_limit = args.eval_n_limit
  266. if eval_n_limit:
  267. biocoder_tests = biocoder_tests.head(eval_n_limit)
  268. logger.info(f'Limiting evaluation to first {eval_n_limit} instances.')
  269. # OUTPUT FILE
  270. output_file = os.path.join(eval_output_dir, 'output.jsonl')
  271. logger.info(f'Writing evaluation output to {output_file}')
  272. finished_test_case_ids = set()
  273. if os.path.exists(output_file):
  274. with open(output_file, 'r') as f:
  275. for line in f:
  276. data = json.loads(line)
  277. finished_test_case_ids.add(data['test_case_id'])
  278. logger.warning(
  279. f'Output file {output_file} already exists. Loaded {len(finished_test_case_ids)} finished instances.'
  280. )
  281. output_fp = open(output_file, 'a')
  282. logger.info(
  283. f'Evaluation started with Agent {agent_class}, model {model_name}, max iterations {max_iterations}.'
  284. )
  285. # =============================================
  286. # filter out finished instances
  287. new_biocoder_tests = []
  288. for idx, instance in biocoder_tests.iterrows():
  289. if instance.test_case_id in finished_test_case_ids:
  290. logger.info(
  291. f'Skipping instance {instance.test_case_id} as it is already finished.'
  292. )
  293. continue
  294. new_biocoder_tests.append(instance)
  295. biocoder_tests = pd.DataFrame(new_biocoder_tests)
  296. logger.info(
  297. f'Finished instances: {len(finished_test_case_ids)}, Remaining instances: {len(biocoder_tests)}'
  298. )
  299. # =============================================
  300. pbar = tqdm(total=len(biocoder_tests))
  301. # This function tracks the progress AND write the output to a JSONL file
  302. def update_progress(future):
  303. pbar.update(1)
  304. output = future.result()
  305. pbar.set_description(f'Instance {output["test_case_id"]}')
  306. pbar.set_postfix_str(f'Test Result: {output["test_result"]}')
  307. logger.info(
  308. f'Finished evaluation for instance {output["test_case_id"]}: {output["test_result"]}'
  309. )
  310. output_fp.write(json.dumps(output) + '\n')
  311. output_fp.flush()
  312. # This sets the multi-processing
  313. num_workers = args.eval_num_workers
  314. logger.info(f'Using {num_workers} workers for evaluation.')
  315. # This is SWE-Bench specific - CodeActAgent doesn't require mounted workspace to work
  316. skip_workspace_mount = agent_class == 'CodeActAgent'
  317. logger.info(f'Skipping workspace mount: {skip_workspace_mount}')
  318. try:
  319. with ProcessPoolExecutor(num_workers) as executor:
  320. futures = []
  321. # This is how we perform multi-processing
  322. for row_idx, instance in biocoder_tests.iterrows():
  323. future = executor.submit(
  324. process_instance,
  325. instance,
  326. agent_class,
  327. metadata,
  328. skip_workspace_mount,
  329. eval_output_dir,
  330. reset_logger=bool(num_workers > 1),
  331. )
  332. future.add_done_callback(update_progress)
  333. futures.append(future)
  334. # Wait for all futures to complete
  335. for future in futures:
  336. future.result()
  337. except KeyboardInterrupt:
  338. print('KeyboardInterrupt received. Cleaning up...')
  339. cleanup()
  340. output_fp.close()
  341. logger.info('Evaluation finished.')