shared.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import json
  2. import multiprocessing as mp
  3. import os
  4. import pathlib
  5. import subprocess
  6. import time
  7. from asyncio.log import logger
  8. from concurrent.futures import ProcessPoolExecutor
  9. from typing import Any, Callable
  10. import pandas as pd
  11. from pydantic import BaseModel
  12. from tqdm import tqdm
  13. from opendevin.controller.state.state import State
  14. from opendevin.core.config import LLMConfig
  15. from opendevin.events.action import Action
  16. from opendevin.events.action.message import MessageAction
  17. class EvalMetadata(BaseModel):
  18. agent_class: str
  19. llm_config: LLMConfig
  20. max_iterations: int
  21. eval_output_dir: str
  22. start_time: str
  23. git_commit: str
  24. dataset: str | None = None
  25. data_split: str | None = None
  26. details: dict[str, Any] | None = None
  27. def codeact_user_response(
  28. state: State,
  29. encapsulate_solution: bool = False,
  30. try_parse: Callable[[Action], str] | None = None,
  31. ) -> str:
  32. encaps_str = (
  33. (
  34. 'Please encapsulate your final answer (answer ONLY) within <solution> and </solution>.\n'
  35. 'For example: The answer to the question is <solution> 42 </solution>.\n'
  36. )
  37. if encapsulate_solution
  38. else ''
  39. )
  40. msg = (
  41. 'Please continue working on the task on whatever approach you think is suitable.\n'
  42. 'If you think you have solved the task, please first send your answer to user through message and then <execute_bash> exit </execute_bash>.\n'
  43. f'{encaps_str}'
  44. 'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP.\n'
  45. )
  46. if state.history:
  47. if try_parse is not None:
  48. last_action, _ = state.history[-1]
  49. ans = try_parse(last_action)
  50. if ans is not None:
  51. return '/exit'
  52. user_msgs = [
  53. action
  54. for action, _ in state.history
  55. if isinstance(action, MessageAction) and action.source == 'user'
  56. ]
  57. if len(user_msgs) >= 2:
  58. # let the agent know that it can give up when it has tried 3 times
  59. return (
  60. msg
  61. + 'If you want to give up, run: <execute_bash> exit </execute_bash>.\n'
  62. )
  63. return msg
  64. def monologue_user_response(state: State) -> str:
  65. raise NotImplementedError('MonologueAgent should never ask for user responses.')
  66. def cleanup():
  67. print('Cleaning up child processes...')
  68. for process in mp.active_children():
  69. print(f'Terminating child process: {process.name}')
  70. process.terminate()
  71. process.join()
  72. def make_metadata(
  73. llm_config: LLMConfig,
  74. dataset_name: str,
  75. agent_class: str,
  76. max_iterations: int,
  77. eval_note: str | None,
  78. eval_output_dir: str,
  79. data_split: str | None = None,
  80. details: dict[str, Any] | None = None,
  81. ) -> EvalMetadata:
  82. model_name = llm_config.model.split('/')[-1]
  83. eval_note = f'_N_{eval_note}' if eval_note else ''
  84. eval_output_path = os.path.join(
  85. eval_output_dir,
  86. dataset_name,
  87. agent_class,
  88. f'{model_name}_maxiter_{max_iterations}{eval_note}',
  89. )
  90. pathlib.Path(eval_output_path).mkdir(parents=True, exist_ok=True)
  91. pathlib.Path(os.path.join(eval_output_path, 'logs')).mkdir(
  92. parents=True, exist_ok=True
  93. )
  94. logger.info(f'Using evaluation output directory: {eval_output_path}')
  95. metadata = EvalMetadata(
  96. agent_class=agent_class,
  97. llm_config=llm_config,
  98. max_iterations=max_iterations,
  99. eval_output_dir=eval_output_path,
  100. start_time=time.strftime('%Y-%m-%d %H:%M:%S'),
  101. git_commit=subprocess.check_output(['git', 'rev-parse', 'HEAD'])
  102. .decode('utf-8')
  103. .strip(),
  104. dataset=dataset_name,
  105. data_split=data_split,
  106. details=details,
  107. )
  108. metadata_json = metadata.model_dump_json()
  109. logger.info(f'Metadata: {metadata_json}')
  110. with open(os.path.join(eval_output_path, 'metadata.json'), 'w') as f:
  111. f.write(metadata_json)
  112. return metadata
  113. def prepare_dataset(dataset: pd.DataFrame, output_file, eval_n_limit, id_column):
  114. logger.info(f'Writing evaluation output to {output_file}')
  115. finished_ids = set()
  116. if os.path.exists(output_file):
  117. with open(output_file, 'r') as f:
  118. for line in f:
  119. data = json.loads(line)
  120. finished_ids.add(data[id_column])
  121. logger.warning(
  122. f'Output file {output_file} already exists. Loaded {len(finished_ids)} finished instances.'
  123. )
  124. if eval_n_limit:
  125. dataset = dataset.head(eval_n_limit)
  126. logger.info(f'Limiting evaluation to first {eval_n_limit} instances.')
  127. new_dataset = [
  128. instance
  129. for _, instance in dataset.iterrows()
  130. if instance[id_column] not in finished_ids
  131. ]
  132. logger.info(
  133. f'Finished instances: {len(finished_ids)}, Remaining instances: {len(new_dataset)}'
  134. )
  135. return pd.DataFrame(new_dataset)
  136. def run_evaluation(
  137. dataset: pd.DataFrame,
  138. metadata: EvalMetadata,
  139. output_file: str,
  140. num_workers: int,
  141. process_instance_func: Callable[[pd.Series, EvalMetadata, bool], Any],
  142. id_column: str,
  143. ):
  144. logger.info(
  145. f'Evaluation started with Agent {metadata.agent_class}, '
  146. f'model {metadata.llm_config.model}, max iterations {metadata.max_iterations}.'
  147. )
  148. pbar = tqdm(total=len(dataset))
  149. output_fp = open(output_file, 'a')
  150. def update_progress(future):
  151. pbar.update(1)
  152. output = future.result()
  153. pbar.set_description(f'Instance {output[id_column]}')
  154. pbar.set_postfix_str(f'Test Result: {output["test_result"]["result"]}')
  155. logger.info(
  156. f'Finished evaluation for instance {output[id_column]}: {output["test_result"]["result"]}'
  157. )
  158. output_fp.write(json.dumps(output) + '\n')
  159. output_fp.flush()
  160. try:
  161. with ProcessPoolExecutor(num_workers) as executor:
  162. futures = []
  163. for _, instance in dataset.iterrows():
  164. future = executor.submit(
  165. process_instance_func,
  166. instance,
  167. metadata,
  168. bool(num_workers > 1),
  169. )
  170. future.add_done_callback(update_progress)
  171. futures.append(future)
  172. for future in futures:
  173. future.result()
  174. except KeyboardInterrupt:
  175. print('KeyboardInterrupt received. Cleaning up...')
  176. cleanup()
  177. output_fp.close()
  178. logger.info('Evaluation finished.')