ljm565 commited on
Commit
b2a5882
·
1 Parent(s): c256411
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __pycache__/
2
+ simulation_arena/
3
+ .env
README.md CHANGED
@@ -1,13 +1,14 @@
1
  ---
2
- title: Adminsim Arena
3
- emoji: 👀
4
- colorFrom: gray
5
- colorTo: gray
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
 
11
  ---
12
 
13
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Adminsim-arena
3
+ emoji: 🏟️
4
+ colorFrom: yellow
5
+ colorTo: red
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
+ short_description: ' Model simulation arena'
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import random
3
+ import datetime
4
+ import gradio as gr
5
+ from typing import Tuple, Optional
6
+
7
+ from utils import log
8
+ from utils.common import upload_to_github
9
+ from utils.postprocess import make_dialog_dict
10
+
11
+
12
+
13
+ def sample_pair(dialog_dict: dict) -> Tuple[str, str, str, str]:
14
+ """
15
+ Sample two different models and one dialog from each.
16
+
17
+ Args:
18
+ dialog_dict (dict): Dictionary of dialogs per model.
19
+
20
+ Returns:
21
+ Tuple[str, str, str, str]: (model1 name, dialog1, model2 name, dialog2)
22
+ """
23
+ model1, model2 = random.sample(list(dialog_dict.keys()), 2)
24
+ dialog1 = random.choice(dialog_dict[model1])
25
+ dialog2 = random.choice(dialog_dict[model2])
26
+ return model1, dialog1, model2, dialog2
27
+
28
+
29
+
30
+ def new_comparison(dialog_dict: dict) -> Tuple[str, str, str, str, gr.Row, gr.Row, gr.Row]:
31
+ """
32
+ Generate a new comparison pair and make the arena row visible.
33
+
34
+ Args:
35
+ dialog_dict (dict): Dictionary of dialogs per model.
36
+
37
+ Returns:
38
+ Tuple[str, str, str, str, gr.Row]:
39
+ (model1 name, dialog1, model2 name, dialog2,
40
+ arena row visibility update, submit button visibility update, new comparison button visibility update)
41
+ """
42
+ m1, d1, m2, d2 = sample_pair(dialog_dict)
43
+ return m1, d1, m2, d2, gr.update(visible=True), gr.update(visible=True), gr.update(visible=False)
44
+
45
+
46
+
47
+ def record_vote(choice: str,
48
+ m1: str,
49
+ m2: str,
50
+ d1: str,
51
+ d2: str,
52
+ dialog_dict: dict,
53
+ is_dev: bool,
54
+ result_file_path: Optional[str] = None) -> Tuple[str, str, str, str, gr.Row, str, gr.Button]:
55
+ """
56
+ Record the user's vote and generate a new comparison.
57
+
58
+ Args:
59
+ choice (str): User's choice ('A' or 'B').
60
+ m1 (str): Model 1 name.
61
+ m2 (str): Model 2 name.
62
+ d1 (str): Dialog 1.
63
+ d2 (str): Dialog 2.
64
+ dialog_dict (dict): Dictionary of dialogs per model.
65
+ is_dev (bool): Flag indicating if in development mode.
66
+ result_file_path (str, optional): Path to save the results.
67
+
68
+ Returns:
69
+ Tuple[str, str, str, str, gr.Row, str, gr.Button]:
70
+ (New model1 name, new dialog1, new model2 name, new dialog2, arena row visibility update, message)
71
+ """
72
+ log(f"{choice=} | A: {m1} vs B: {m2}")
73
+
74
+ # Vote data saving
75
+ if not is_dev:
76
+ with open(result_file_path, "a") as f:
77
+ if choice == "A":
78
+ f.write(f"1\t0\t{m1}\t{m2}\n")
79
+ else:
80
+ f.write(f"0\t1\t{m1}\t{m2}\n")
81
+
82
+ # New pair sampling
83
+ new_m1, new_d1, new_m2, new_d2 = sample_pair(dialog_dict)
84
+
85
+ return new_m1, new_d1, new_m2, new_d2, gr.update(visible=True), "✅ Vote recorded! Next comparison ready!"
86
+
87
+
88
+
89
+ def save_data(path: str) -> str:
90
+ """
91
+ Save the human evaluation data.
92
+
93
+ Args:
94
+ path (str): Path to save the results.
95
+
96
+ Returns:
97
+ str: Message of the result submission.
98
+ """
99
+ try:
100
+ upload_to_github(path, open(path).read())
101
+ return "✅ Results successfully uploaded to GitHub!"
102
+ except Exception as e:
103
+ return f"❌ Upload failed: {e}"
104
+
105
+
106
+
107
+ def set_result_path() -> str:
108
+ """
109
+ Set the result saving path.
110
+
111
+ Returns:
112
+ str: Path to save the results.
113
+ """
114
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
115
+ unique_id = os.urandom(4).hex()
116
+ result_file = os.path.join("simulation_arena", f"result_{timestamp}_{unique_id}.txt")
117
+ os.makedirs(os.path.dirname(result_file), exist_ok=True)
118
+ return result_file
119
+
120
+
121
+
122
+ css = """
123
+ .dialog-box {
124
+ height: 600px;
125
+ overflow-y: auto;
126
+ }
127
+ """
128
+
129
+
130
+
131
+ # Arena GUI
132
+ is_dev = False
133
+ result_save_path = set_result_path()
134
+ dialog_dict = make_dialog_dict()
135
+ with gr.Blocks(title="1:1 Outpatient Model Simulation Arena", css=css) as demo:
136
+ gr.Markdown("# 🤖 Model Arena Evaluation")
137
+ gr.Markdown("### Compare two model simulations and choose the better one!")
138
+ gr.Markdown("### This scenario assumes that the patient called the hospital's administrative office for an outpatient inquiry.")
139
+
140
+ # New comparison button
141
+ btn_new = gr.Button("🥊 Start Arena!! 🥊")
142
+ msg = gr.Markdown("")
143
+ state_dict = gr.State(dialog_dict)
144
+
145
+ # Showing two model simulations side by side
146
+ with gr.Row(visible=False) as arena_row:
147
+ with gr.Column():
148
+ model1_name = gr.Textbox(label="Model A", interactive=False, visible=is_dev)
149
+ dialog1_box = gr.Markdown(label="Simulation A", elem_classes="dialog-box") # Textbox → Markdown
150
+ vote1 = gr.Button("👍 Choose A")
151
+
152
+ with gr.Column():
153
+ model2_name = gr.Textbox(label="Model B", interactive=False, visible=is_dev)
154
+ dialog2_box = gr.Markdown(label="Simulation B", elem_classes="dialog-box") # Textbox → Markdown
155
+ vote2 = gr.Button("👍 Choose B")
156
+
157
+ # Submit button
158
+ with gr.Row(visible=False) as submit_row:
159
+ submit_btn = gr.Button("📤 Submit All Results")
160
+ submit_msg = gr.Markdown("")
161
+
162
+ # Button actions
163
+ btn_new.click(
164
+ fn=new_comparison,
165
+ inputs=[state_dict],
166
+ outputs=[model1_name, dialog1_box, model2_name, dialog2_box, arena_row, submit_row, btn_new],
167
+ )
168
+
169
+ vote1.click(
170
+ fn=lambda m1, m2, d1, d2, state, is_dev=is_dev, result_file_path=result_save_path: record_vote("A", m1, m2, d1, d2, state, is_dev, result_file_path),
171
+ inputs=[model1_name, model2_name, dialog1_box, dialog2_box, state_dict],
172
+ outputs=[model1_name, dialog1_box, model2_name, dialog2_box, arena_row, msg],
173
+ )
174
+
175
+ vote2.click(
176
+ fn=lambda m1, m2, d1, d2, state, is_dev=is_dev, result_file_path=result_save_path: record_vote("B", m1, m2, d1, d2, state, is_dev, result_file_path),
177
+ inputs=[model1_name, model2_name, dialog1_box, dialog2_box, state_dict],
178
+ outputs=[model1_name, dialog1_box, model2_name, dialog2_box, arena_row, msg],
179
+ )
180
+
181
+ submit_btn.click(
182
+ fn=save_data,
183
+ inputs=[gr.State(result_save_path)],
184
+ outputs=[submit_msg],
185
+ )
186
+
187
+ # Launch the app
188
+ if is_dev:
189
+ demo.launch(server_port=7860)
190
+ else:
191
+ demo.launch()
dialogs/gemini-2.5-flash/primary/hospital_00_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gemini-2.5-flash/primary/hospital_01_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gemini-2.5-flash/primary/hospital_02_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gemini-2.5-flash/secondary/hospital_0_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gemini-2.5-flash/secondary/hospital_1_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gemini-2.5-flash/secondary/hospital_2_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gpt-5-mini/primary/hospital_00_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gpt-5-mini/primary/hospital_01_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gpt-5-mini/primary/hospital_02_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gpt-5-mini/secondary/hospital_0_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gpt-5-mini/secondary/hospital_1_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gpt-5-mini/secondary/hospital_2_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gpt-5-nano/primary/hospital_00_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gpt-5-nano/primary/hospital_01_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gpt-5-nano/primary/hospital_02_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gpt-5-nano/sencondary/hospital_0_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gpt-5-nano/sencondary/hospital_1_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
dialogs/gpt-5-nano/sencondary/hospital_2_agent_dialog.json ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio==5.49.1
2
+ dotenv==0.9.9
3
+ PyGithub==2.8.1
utils/__init__.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging.config
3
+
4
+
5
+
6
+ base_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
7
+ try:
8
+ version_file_path = os.path.join(base_path, 'version.txt')
9
+ LOGGING_NAME = f"Simulation_Arena_{open(version_file_path).read().strip()}"
10
+ except:
11
+ version_file_path = os.path.join(base_path, 'app', 'version.txt')
12
+ LOGGING_NAME = f"Simulation_Arena_{open(version_file_path).read().strip()}"
13
+ VERBOSE = True
14
+
15
+
16
+ def set_logging(name=LOGGING_NAME, verbose=True):
17
+ """Sets up logging for the given name."""
18
+ rank = int(os.getenv('RANK', -1)) # rank in world for Multi-GPU trainings
19
+ level = logging.INFO if verbose and rank in {-1, 0} else logging.ERROR
20
+
21
+ class ColorFormatter(logging.Formatter):
22
+ """Custom formatter to add colors to log messages using colorstr."""
23
+ def format(self, record):
24
+ if record.levelname == "ERROR":
25
+ record.msg = colorstr("red", record.msg)
26
+ elif record.levelname == "WARNING":
27
+ record.msg = colorstr("yellow", record.msg)
28
+ # elif record.levelname == "INFO":
29
+ # record.msg = colorstr("green", record.msg)
30
+ elif record.levelname == "DEBUG":
31
+ record.msg = colorstr("blue", record.msg)
32
+ return super().format(record)
33
+
34
+ logging.config.dictConfig({
35
+ 'version': 1,
36
+ 'disable_existing_loggers': False,
37
+ 'formatters': {
38
+ name: {
39
+ '()': ColorFormatter, # Use the custom formatter
40
+ 'format': '[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s'
41
+ }
42
+ },
43
+ 'handlers': {
44
+ name: {
45
+ 'class': 'logging.StreamHandler',
46
+ 'formatter': name,
47
+ 'level': level
48
+ }
49
+ },
50
+ 'loggers': {
51
+ name: {
52
+ 'level': level,
53
+ 'handlers': [name],
54
+ 'propagate': False
55
+ }
56
+ }
57
+ })
58
+
59
+
60
+ set_logging(LOGGING_NAME, verbose=VERBOSE)
61
+ LOGGER = logging.getLogger(LOGGING_NAME)
62
+
63
+
64
+
65
+ def colorstr(*input):
66
+ """
67
+ Colors a string based on the provided color and style arguments. Utilizes ANSI escape codes.
68
+ See https://en.wikipedia.org/wiki/ANSI_escape_code for more details.
69
+
70
+ This function can be called in two ways:
71
+ - colorstr('color', 'style', 'your string')
72
+ - colorstr('your string')
73
+
74
+ In the second form, 'blue' and 'bold' will be applied by default.
75
+
76
+ Args:
77
+ *input (str): A sequence of strings where the first n-1 strings are color and style arguments,
78
+ and the last string is the one to be colored.
79
+
80
+ Supported Colors and Styles:
81
+ Basic Colors: 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
82
+ Bright Colors: 'bright_black', 'bright_red', 'bright_green', 'bright_yellow',
83
+ 'bright_blue', 'bright_magenta', 'bright_cyan', 'bright_white'
84
+ Misc: 'end', 'bold', 'underline'
85
+
86
+ Returns:
87
+ (str): The input string wrapped with ANSI escape codes for the specified color and style.
88
+
89
+ Examples:
90
+ >>> colorstr('blue', 'bold', 'hello world')
91
+ >>> '\033[34m\033[1mhello world\033[0m'
92
+ """
93
+ *args, string = input if len(input) > 1 else ('blue', 'bold', input[0]) # color arguments, string
94
+ colors = {
95
+ 'black': '\033[30m', # basic colors
96
+ 'red': '\033[31m',
97
+ 'green': '\033[32m',
98
+ 'yellow': '\033[33m',
99
+ 'blue': '\033[34m',
100
+ 'magenta': '\033[35m',
101
+ 'cyan': '\033[36m',
102
+ 'white': '\033[37m',
103
+ 'bright_black': '\033[90m', # bright colors
104
+ 'bright_red': '\033[91m',
105
+ 'bright_green': '\033[92m',
106
+ 'bright_yellow': '\033[93m',
107
+ 'bright_blue': '\033[94m',
108
+ 'bright_magenta': '\033[95m',
109
+ 'bright_cyan': '\033[96m',
110
+ 'bright_white': '\033[97m',
111
+ 'end': '\033[0m', # misc
112
+ 'bold': '\033[1m',
113
+ 'underline': '\033[4m'}
114
+ return ''.join(colors[x] for x in args) + f'{string}' + colors['end']
115
+
116
+
117
+
118
+ def log(message, level='info', color=False):
119
+ if level.lower() == 'warning':
120
+ LOGGER.warning(message)
121
+ elif level.lower() == 'error':
122
+ LOGGER.error(message)
123
+ else:
124
+ if color:
125
+ LOGGER.info(colorstr(message))
126
+ else:
127
+ LOGGER.info(message)
utils/common.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from github import Github
3
+ from dotenv import load_dotenv
4
+
5
+ from utils import log
6
+
7
+
8
+
9
+ def upload_to_github(file_path, content):
10
+ load_dotenv(override=True)
11
+ g = Github(os.getenv("GITHUB_TOKEN"))
12
+ repo = g.get_repo("ljm565/adminsim-human-eval")
13
+ try:
14
+ contents = None
15
+ try:
16
+ contents = repo.get_contents(file_path, ref="main")
17
+ except Exception:
18
+ if contents is None:
19
+ repo.create_file(file_path, "feat: Add new results", content, branch="main")
20
+ log(f"File created successfully at {file_path}!")
21
+ else:
22
+ repo.update_file(file_path, "feat: Update the results", content, contents.sha, branch="main")
23
+ log(f"File updated successfully at {file_path}!")
24
+ except Exception as e:
25
+ log(f"An error occurred: {e}", level="error")
utils/filesys_utils.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from typing import Any
4
+
5
+
6
+
7
+ def json_load(path: str) -> Any:
8
+ """
9
+ Load and parse a JSON file.
10
+
11
+ Args:
12
+ path (str): Path to the JSON file.
13
+
14
+ Returns:
15
+ Any: The parsed Python object (usually a dict or list) from the JSON file.
16
+ """
17
+ with open(path, 'r') as f:
18
+ return json.load(f)
19
+
20
+
21
+
22
+ def get_files(path: str, ext: str = None) -> list[str]:
23
+ """
24
+ Get all files in a directory with a specific extension.
25
+
26
+ Args:
27
+ path (str): Folder path to search for files.
28
+ ext (str, optional): Extension that you want to filter. Defaults to None.
29
+
30
+ Raises:
31
+ ValueError: If file does not exist.
32
+
33
+ Returns:
34
+ list[str]: List of file paths that match the given extension.
35
+ """
36
+ if not os.path.isdir(path):
37
+ raise ValueError(f"Path {path} is not a directory.")
38
+
39
+ files = []
40
+ for root, _, filenames in os.walk(path):
41
+ for filename in filenames:
42
+ if ext is None or filename.endswith(ext):
43
+ files.append(os.path.join(root, filename))
44
+
45
+ return files
utils/postprocess.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, re
2
+ from utils.filesys_utils import json_load, get_files
3
+
4
+
5
+
6
+ def make_dialog_dict(dir_path: str = './dialogs') -> dict:
7
+ """
8
+ Create a dictionary of dialogs from JSON files in the specified directory.
9
+
10
+ Args:
11
+ dir_path (str, optional): Path to the directory containing dialog JSON files. Defaults to './dialogs'.
12
+
13
+ Returns:
14
+ dict: A dictionary where keys are model name and values are the parsed JSON content.
15
+ """
16
+ dialog_dict = {}
17
+
18
+ for model in os.listdir(dir_path):
19
+ model_path = os.path.join(dir_path, model)
20
+ dialog_files = get_files(model_path, ext='.json')
21
+ if not dialog_files:
22
+ continue
23
+
24
+ dialogues = [content
25
+ for file in dialog_files
26
+ for content in map(dialog_postprocessing, json_load(file).values())
27
+ if content
28
+ ]
29
+
30
+ if dialogues:
31
+ dialog_dict[model] = dialogues
32
+
33
+ return dialog_dict
34
+
35
+
36
+
37
+ def dialog_postprocessing(dialog: str) -> str:
38
+ """
39
+ Postprocess a dialog string by removing unwanted tokens.
40
+
41
+ Args:
42
+ dialog (str): The dialog string to be processed.
43
+
44
+ Returns:
45
+ str: The cleaned dialog string.
46
+ """
47
+ department_candidates = ["gastroenterology", "cardiology", "pulmonary", "endocrinology/metabolism", "nephrology", "hematology/oncology", "allergy", "infectious diseases", "rheumatology"]
48
+ try:
49
+ answer_pattern = re.compile(r'Answer:\s*\d+\.\s*(.+)')
50
+ split_pattern = re.compile(r'\bAnswer:')
51
+
52
+ department = answer_pattern.search(dialog).group(1)
53
+ for candidate in department_candidates:
54
+ if department.lower().startswith(candidate):
55
+ department = candidate
56
+ break
57
+ assert department.lower() in department_candidates
58
+
59
+ before_answer = re.split(split_pattern, dialog)[0].strip()
60
+ before_answer += f' I will introduce you to a physician who work in the {department}.'
61
+
62
+ before_answer = before_answer.replace("Staff:", "<span style='color:rgb(0,102,204); font-weight:bold'>Staff</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:")
63
+ before_answer = before_answer.replace("Patient:", "<span style='color:rgb(204,0,102); font-weight:bold'>Patient</span>:")
64
+ before_answer = before_answer.replace("\n", "<br>")
65
+
66
+ except:
67
+ before_answer = ''
68
+
69
+ return before_answer
70
+
71
+
version.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ 1.0.0