h-adminsim 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- h_adminsim/__init__.py +5 -0
- h_adminsim/admin_staff.py +280 -0
- h_adminsim/assets/configs/data4primary.yaml +47 -0
- h_adminsim/assets/configs/data4secondary.yaml +47 -0
- h_adminsim/assets/configs/data4tertiary.yaml +47 -0
- h_adminsim/assets/country/address.json +141859 -0
- h_adminsim/assets/country/country_code.json +244 -0
- h_adminsim/assets/departments/department.json +85 -0
- h_adminsim/assets/departments/symptom.json +4530 -0
- h_adminsim/assets/fhir.schema.json +75253 -0
- h_adminsim/assets/names/firstname.txt +1219 -0
- h_adminsim/assets/names/lastname.txt +88799 -0
- h_adminsim/assets/prompts/cancel_patient_system.txt +38 -0
- h_adminsim/assets/prompts/intake_staff_task_user.txt +16 -0
- h_adminsim/assets/prompts/intake_supervisor_system.txt +8 -0
- h_adminsim/assets/prompts/intake_supervisor_user.txt +31 -0
- h_adminsim/assets/prompts/reschedule_patient_system.txt +38 -0
- h_adminsim/assets/prompts/schedule_patient_rejected_system.txt +42 -0
- h_adminsim/assets/prompts/schedule_patient_system.txt +36 -0
- h_adminsim/assets/prompts/schedule_staff_reasoning.txt +57 -0
- h_adminsim/assets/prompts/schedule_staff_sc_tool_calling.txt +13 -0
- h_adminsim/assets/prompts/schedule_staff_system.txt +10 -0
- h_adminsim/assets/prompts/schedule_staff_tool_calling.txt +41 -0
- h_adminsim/client/__init__.py +3 -0
- h_adminsim/client/google_client.py +209 -0
- h_adminsim/client/openai_client.py +199 -0
- h_adminsim/client/vllm_client.py +160 -0
- h_adminsim/environment/__init__.py +1 -0
- h_adminsim/environment/hospital.py +462 -0
- h_adminsim/environment/op_scheduling_simulation.py +1126 -0
- h_adminsim/pipeline/__init__.py +3 -0
- h_adminsim/pipeline/data_generator.py +192 -0
- h_adminsim/pipeline/evaluator.py +33 -0
- h_adminsim/pipeline/simulation.py +231 -0
- h_adminsim/registry/__init__.py +5 -0
- h_adminsim/registry/errors.py +89 -0
- h_adminsim/registry/models.py +126 -0
- h_adminsim/registry/phrases.py +10 -0
- h_adminsim/registry/pydantic_models.py +21 -0
- h_adminsim/registry/variables.py +9 -0
- h_adminsim/supervisor.py +182 -0
- h_adminsim/task/agent_task.py +900 -0
- h_adminsim/task/fhir_manager.py +222 -0
- h_adminsim/task/schedule_assign.py +151 -0
- h_adminsim/tools/__init__.py +5 -0
- h_adminsim/tools/agent_data_builder.py +124 -0
- h_adminsim/tools/data_converter.py +536 -0
- h_adminsim/tools/data_synthesizer.py +365 -0
- h_adminsim/tools/evaluator.py +258 -0
- h_adminsim/tools/sanity_checker.py +216 -0
- h_adminsim/tools/scheduling_rule.py +420 -0
- h_adminsim/utils/__init__.py +136 -0
- h_adminsim/utils/common_utils.py +698 -0
- h_adminsim/utils/fhir_utils.py +190 -0
- h_adminsim/utils/filesys_utils.py +135 -0
- h_adminsim/utils/image_preprocess_utils.py +188 -0
- h_adminsim/utils/random_utils.py +358 -0
- h_adminsim/version.txt +1 -0
- h_adminsim-1.0.0.dist-info/LICENSE +30 -0
- h_adminsim-1.0.0.dist-info/METADATA +494 -0
- h_adminsim-1.0.0.dist-info/RECORD +62 -0
- h_adminsim-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import random
|
|
3
|
+
import numpy as np
|
|
4
|
+
from sconf import Config
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from typing import Optional, Union
|
|
8
|
+
|
|
9
|
+
from h_adminsim.task.fhir_manager import FHIRManager
|
|
10
|
+
from h_adminsim.tools import DataSynthesizer, DataConverter, AgentDataBuilder
|
|
11
|
+
from h_adminsim.utils import Information, colorstr, log
|
|
12
|
+
from h_adminsim.utils.random_utils import random_uuid
|
|
13
|
+
from h_adminsim.utils.filesys_utils import get_files, json_load
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DataGenerator:
|
|
18
|
+
def __init__(self,
|
|
19
|
+
care_level: str = 'primary',
|
|
20
|
+
config: Optional[Union[str, Config]] = None):
|
|
21
|
+
|
|
22
|
+
# Initialize
|
|
23
|
+
self.config = self.load_config(care_level, config)
|
|
24
|
+
self.__env_setup(self.config)
|
|
25
|
+
self.fhir_url = self.config.get('fhir_url', None)
|
|
26
|
+
self.data_synthesizer = DataSynthesizer(self.config)
|
|
27
|
+
self.save_dir = self.data_synthesizer._save_dir
|
|
28
|
+
log(f'Data saving directory: {colorstr(self.save_dir)}')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_config(self, care_level: str, config: Optional[Union[str, Config]]) -> Config:
|
|
32
|
+
"""
|
|
33
|
+
Load a configuration object.
|
|
34
|
+
|
|
35
|
+
If `config` is None, a default configuration is loaded based on the given
|
|
36
|
+
`care_level`. If `config` is a string, it is treated as a file path and
|
|
37
|
+
loaded as a Config object. If a Config instance is provided, it is returned
|
|
38
|
+
as-is.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
care_level (str): Care level used to select the default config.
|
|
42
|
+
config (Optional[Union[str, Config]]): A file path or Config instance.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
TypeError: If `config` is not None, str, or Config.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Config: A fully initialized Config object.
|
|
49
|
+
"""
|
|
50
|
+
# Case 1: config is None -> load built-in config based on care_level
|
|
51
|
+
if config is None:
|
|
52
|
+
log(f"No config provided; using default {care_level} config.", "warning")
|
|
53
|
+
assert care_level in ['primary', 'secondary', 'tertiary'], \
|
|
54
|
+
log(f"Invalid care_level: '{care_level}'. Expected one of: primary, secondary, tertiary.", "error")
|
|
55
|
+
default_path = str(resources.files("h_adminsim.assets.configs").joinpath(f"data4{care_level}.yaml"))
|
|
56
|
+
return Config(default_path)
|
|
57
|
+
|
|
58
|
+
# Case 2: config is a string path
|
|
59
|
+
if isinstance(config, str):
|
|
60
|
+
config_inst = Config(config)
|
|
61
|
+
return config_inst
|
|
62
|
+
|
|
63
|
+
# Case 3: config is already a Config object
|
|
64
|
+
if isinstance(config, Config):
|
|
65
|
+
return config
|
|
66
|
+
|
|
67
|
+
# Otherwise error
|
|
68
|
+
raise TypeError(
|
|
69
|
+
log(f"Invalid config: expected None, str, or Config, got {type(config).__name__}", "error")
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def __env_setup(self, config: Config):
|
|
74
|
+
"""
|
|
75
|
+
Initialize environment-level random seeds using the given configuration.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
config (Config): Configuration containing the seed value.
|
|
79
|
+
"""
|
|
80
|
+
random.seed(config.seed)
|
|
81
|
+
np.random.seed(config.seed)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def build(self,
|
|
85
|
+
sanity_check: bool = True,
|
|
86
|
+
convert_to_fhir: bool = False,
|
|
87
|
+
build_agent_data: bool = True) -> Information:
|
|
88
|
+
"""
|
|
89
|
+
Build the complete information bundle for the administrative simulation pipeline.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
sanity_check (bool, optional): Whether to perform validation checks during synthetic data generation. Defaults to True.
|
|
93
|
+
convert_to_fhir (bool, optional): If True, converts synthesized data into FHIR-compliant resources and stores them
|
|
94
|
+
in the configured output directory. Defaults to False.
|
|
95
|
+
build_agent_data (bool, optional): If True, generates additional derived data required for agent-based
|
|
96
|
+
simulations (e.g., patient profiles, department assignments, task inputs). Defaults to True.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
Exception: Propagates any exception encountered during:
|
|
100
|
+
- synthetic data synthesis
|
|
101
|
+
- FHIR conversion
|
|
102
|
+
- agent data generation
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Information:
|
|
106
|
+
A structured container holding:
|
|
107
|
+
- `data`: the synthesized dataset
|
|
108
|
+
- `fhir_data`: list of FHIR resources (or None if disabled)
|
|
109
|
+
- `agent_data`: processed agent input data (or None if disabled)
|
|
110
|
+
"""
|
|
111
|
+
# Data generator
|
|
112
|
+
try:
|
|
113
|
+
data, hospital_obj = self.data_synthesizer.synthesize(sanity_check=sanity_check)
|
|
114
|
+
log(f"Data synthesis completed successfully", color=True)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
log("Data synthesis failed.", level="error")
|
|
117
|
+
raise e
|
|
118
|
+
|
|
119
|
+
# FHIR conversion
|
|
120
|
+
all_resource_list = None
|
|
121
|
+
if convert_to_fhir:
|
|
122
|
+
converter = DataConverter(self.config)
|
|
123
|
+
try:
|
|
124
|
+
all_resource_list = converter(self.save_dir / 'fhir_data', sanity_check)
|
|
125
|
+
log(f"Data FHIR conversion completed successfully", color=True)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
log("Data FHIR conversion failed.", level='error')
|
|
128
|
+
raise e
|
|
129
|
+
|
|
130
|
+
# Build data for agent simulation
|
|
131
|
+
agent_data_list = None
|
|
132
|
+
if build_agent_data:
|
|
133
|
+
builder = AgentDataBuilder(self.config)
|
|
134
|
+
try:
|
|
135
|
+
agent_data_list = builder(self.save_dir / 'agent_data')
|
|
136
|
+
log(f"Agent data generation completed successfully", color=True)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
log("Agent data generation failed.", level='error')
|
|
139
|
+
raise e
|
|
140
|
+
|
|
141
|
+
output = Information(
|
|
142
|
+
data=data,
|
|
143
|
+
fhir_data=all_resource_list,
|
|
144
|
+
agent_data=agent_data_list
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return output
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def upload_to_fhir(self,
|
|
151
|
+
fhir_data_dir: str,
|
|
152
|
+
fhir_url: Optional[str] = None):
|
|
153
|
+
"""
|
|
154
|
+
Upload synthesized FHIR resources to the specified FHIR server.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
fhir_data_dir (str): Directory containing FHIR resource JSON files (e.g., practitioner, practitionerrole, schedule, slot).
|
|
158
|
+
fhir_url (Optional[str], optional): Base URL of the FHIR server. If not provided, the instance's default FHIR URL is used.
|
|
159
|
+
"""
|
|
160
|
+
# Initialize FHIR URL and manager
|
|
161
|
+
if not fhir_url:
|
|
162
|
+
fhir_url = self.fhir_url
|
|
163
|
+
assert fhir_url != None, log('')
|
|
164
|
+
|
|
165
|
+
if not fhir_url.endswith('fhir'):
|
|
166
|
+
fhir_url = os.path.join(fhir_url, 'fhir')
|
|
167
|
+
|
|
168
|
+
fhir_manager = FHIRManager(fhir_url)
|
|
169
|
+
|
|
170
|
+
# FHIR resources
|
|
171
|
+
fhir_data_dir = Path(fhir_data_dir)
|
|
172
|
+
fhir_resources_dirs = [fhir_data_dir / resource for resource in ['practitioner', 'practitionerrole', 'schedule', 'slot']]
|
|
173
|
+
|
|
174
|
+
# Upload resources to FHIR
|
|
175
|
+
for path in fhir_resources_dirs:
|
|
176
|
+
files = get_files(path, ext='json')
|
|
177
|
+
error_files = list()
|
|
178
|
+
|
|
179
|
+
for file in files:
|
|
180
|
+
resource_data = json_load(file)
|
|
181
|
+
resource_type = resource_data.get('resourceType')
|
|
182
|
+
if 'id' not in resource_data:
|
|
183
|
+
resource_data['id'] = random_uuid(False)
|
|
184
|
+
|
|
185
|
+
response = fhir_manager.create(resource_type, resource_data)
|
|
186
|
+
if 200 <= response.status_code < 300:
|
|
187
|
+
log(f"Created {resource_type} with ID {response.json().get('id')}")
|
|
188
|
+
else:
|
|
189
|
+
error_files.append(file)
|
|
190
|
+
|
|
191
|
+
if len(error_files):
|
|
192
|
+
log(f'Error files during creating data: {error_files}', 'warning')
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from h_adminsim.tools import Evaluator as BaseEvaluator
|
|
2
|
+
from h_adminsim.utils import log
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Evaluator(BaseEvaluator):
|
|
7
|
+
def __init__(self, path: str):
|
|
8
|
+
super().__init__(path)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def evaluate(self, tasks: list[str]):
|
|
12
|
+
"""
|
|
13
|
+
Evaluate the performance of an agent.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
tasks (list[str]): A task list to evaluate.
|
|
17
|
+
"""
|
|
18
|
+
if 'task' in tasks:
|
|
19
|
+
self.task_evaluation()
|
|
20
|
+
log('')
|
|
21
|
+
|
|
22
|
+
# if 'feedback' in tasks:
|
|
23
|
+
# self.supervisor_evaluation()
|
|
24
|
+
# log('')
|
|
25
|
+
|
|
26
|
+
if 'rounds' in tasks:
|
|
27
|
+
self.calculate_avg_rounds()
|
|
28
|
+
log('')
|
|
29
|
+
|
|
30
|
+
if 'department' in tasks:
|
|
31
|
+
self.department_evaluation()
|
|
32
|
+
log('')
|
|
33
|
+
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import random
|
|
3
|
+
import numpy as np
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from h_adminsim.task.agent_task import *
|
|
7
|
+
from h_adminsim.task.fhir_manager import FHIRManager
|
|
8
|
+
from h_adminsim.environment.hospital import HospitalEnvironment
|
|
9
|
+
from h_adminsim.utils.filesys_utils import json_load, json_save_fast, get_files
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Simulator:
|
|
14
|
+
def __init__(self,
|
|
15
|
+
intake_task: Optional[OutpatientFirstIntake] = None,
|
|
16
|
+
scheduling_task: Optional[OutpatientFirstScheduling] = None,
|
|
17
|
+
simulation_start_day_before: float = 3,
|
|
18
|
+
fhir_integration: bool = False,
|
|
19
|
+
fhir_url: Optional[str] = None,
|
|
20
|
+
fhir_max_connection_retries: int = 5,
|
|
21
|
+
random_seed: int = 9999):
|
|
22
|
+
|
|
23
|
+
# Initialize
|
|
24
|
+
self.__env_setup(random_seed)
|
|
25
|
+
self.simulation_start_day_before = simulation_start_day_before
|
|
26
|
+
self.fhir_integration = fhir_integration
|
|
27
|
+
self.fhir_url = fhir_url if self.fhir_integration else None
|
|
28
|
+
self.fhir_max_connection_retries = fhir_max_connection_retries
|
|
29
|
+
self.task_queue, self.task_list = self._init_task(intake_task, scheduling_task)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def __env_setup(self, random_seed: int):
|
|
33
|
+
"""
|
|
34
|
+
Initialize environment-level random seeds.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
random_seed (int): Random seed.
|
|
38
|
+
"""
|
|
39
|
+
random.seed(random_seed)
|
|
40
|
+
np.random.seed(random_seed)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _init_task(self,
|
|
44
|
+
intake_task: Optional[OutpatientFirstIntake] = None,
|
|
45
|
+
scheduling_task: Optional[OutpatientFirstScheduling] = None) -> Tuple[list[FirstVisitOutpatientTask], list[str]]:
|
|
46
|
+
"""
|
|
47
|
+
Initialize the task queue for first-visit outpatient workflow.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
intake_task (Optional[OutpatientFirstIntake], optional): Intake task instance to include in the queue. Defaults to None.
|
|
51
|
+
scheduling_task (Optional[OutpatientFirstScheduling], optional): Scheduling task instance to include in the queue. Defaults to None.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Tuple[list[FirstVisitOutpatientTask], list[str]]:
|
|
55
|
+
A tuple containing:
|
|
56
|
+
- the ordered list of task objects
|
|
57
|
+
- the list of task names in execution order
|
|
58
|
+
"""
|
|
59
|
+
task_queue, task_list = list(), list()
|
|
60
|
+
assert intake_task != None or scheduling_task != None, \
|
|
61
|
+
log("At least one of 'intake_task' or 'scheduling_task' must be provided (both cannot be None).", level='error')
|
|
62
|
+
|
|
63
|
+
if intake_task != None:
|
|
64
|
+
task_queue.append(intake_task)
|
|
65
|
+
task_list.append(intake_task.name)
|
|
66
|
+
if scheduling_task != None:
|
|
67
|
+
task_queue.append(scheduling_task)
|
|
68
|
+
task_list.append(scheduling_task.name)
|
|
69
|
+
|
|
70
|
+
return task_queue, task_list
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def clean_fhir(self):
|
|
74
|
+
"""
|
|
75
|
+
Clear the FHIR data.
|
|
76
|
+
"""
|
|
77
|
+
if self.fhir_integration:
|
|
78
|
+
fhir_manager = FHIRManager(self.fhir_url)
|
|
79
|
+
appointment_entries = fhir_manager.read_all('Appointment')
|
|
80
|
+
patient_entries = fhir_manager.read_all('Patient')
|
|
81
|
+
fhir_manager.delete_all(appointment_entries, verbose=False)
|
|
82
|
+
fhir_manager.delete_all(patient_entries, verbose=False)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def shuffle_data(data: dict):
|
|
87
|
+
"""
|
|
88
|
+
Shuffle the agent test data by the schedule start time.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
data (dict): An agent test data to simulate a hospital environmnet.
|
|
92
|
+
"""
|
|
93
|
+
random.shuffle(data['agent_data']) # In-place logic
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def resume_results(agent_simulation_data: dict, results_path: str, d_results_path: str) -> Tuple[dict, dict, dict, set]:
|
|
98
|
+
"""
|
|
99
|
+
Resume a previously saved simulation by aligning agent results.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
agent_simulation_data (dict): Static agent test data for a simulation.
|
|
103
|
+
results_path (str): Path to the JSON file containing the saved simulation results.
|
|
104
|
+
d_results_path (str): Path to the JSON file containing the saved dialog results.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Tuple[dict, int]:
|
|
108
|
+
- dict: Schedule updated static agent test data.
|
|
109
|
+
- dict: Previously saved agent results.
|
|
110
|
+
- dict: Previously saved dialog results.
|
|
111
|
+
- set: A dictionary containing patients that have already been processed for each task.
|
|
112
|
+
"""
|
|
113
|
+
# Load previous results
|
|
114
|
+
agent_results = json_load(results_path)
|
|
115
|
+
dialog_results = json_load(d_results_path) if os.path.exists(d_results_path) else dict()
|
|
116
|
+
|
|
117
|
+
# Get patients that have already been processed for each task
|
|
118
|
+
done_patients = dict()
|
|
119
|
+
for task_name, result in agent_results.items():
|
|
120
|
+
if task_name == 'intake':
|
|
121
|
+
done_patients[task_name] = {done['patient']['name'] for done in result['gt']}
|
|
122
|
+
elif task_name == 'schedule':
|
|
123
|
+
done_patients[task_name] = set()
|
|
124
|
+
for done in result['gt']:
|
|
125
|
+
try:
|
|
126
|
+
done_patients[task_name].add(done['patient'])
|
|
127
|
+
except (KeyError, TypeError):
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
# Updated doctor schedules based on the resumed results
|
|
131
|
+
if 'schedule' in agent_results:
|
|
132
|
+
fixed_schedule = agent_simulation_data['doctor']
|
|
133
|
+
statuses = [x for y in agent_results['schedule']['status'] for x in (y if isinstance(y, list) or isinstance(y, tuple) else [y])]
|
|
134
|
+
preds = [x for y in agent_results['schedule']['pred'] for x in (y if isinstance(y, list) or isinstance(y, tuple) else [y])]
|
|
135
|
+
for status, pred in zip(statuses, preds):
|
|
136
|
+
if status and 'status' in pred and pred['status'] != 'cancelled':
|
|
137
|
+
fixed_schedule[pred['attending_physician']]['schedule'][pred['date']].append(pred['schedule'])
|
|
138
|
+
fixed_schedule[pred['attending_physician']]['schedule'][pred['date']].sort()
|
|
139
|
+
|
|
140
|
+
return agent_simulation_data, agent_results, dialog_results, done_patients
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def run(self,
|
|
144
|
+
simulation_data_path: str,
|
|
145
|
+
output_dir: str,
|
|
146
|
+
resume: bool = False,
|
|
147
|
+
verbose: bool = False):
|
|
148
|
+
"""
|
|
149
|
+
Run the agent-based hospital administrative simulation.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
simulation_data_path (str): Path to a JSON file or directory containing agent simulation input data.
|
|
153
|
+
output_dir (str): Directory to store simulation results.
|
|
154
|
+
resume (bool, optional): Whether to resume a previous simulation if result files exist. Defaults to False.
|
|
155
|
+
verbose (bool, optional): Whether to print detailed logs during task execution. Defaults to False.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
Exception: Propagates any errors encountered during simulation or result saving.
|
|
159
|
+
"""
|
|
160
|
+
# Clear FHIR data
|
|
161
|
+
if not resume:
|
|
162
|
+
self.clean_fhir()
|
|
163
|
+
|
|
164
|
+
# Load agent simulation data
|
|
165
|
+
is_file = os.path.isfile(simulation_data_path)
|
|
166
|
+
agent_simulation_data_files = [simulation_data_path] if is_file else get_files(simulation_data_path, ext='json')
|
|
167
|
+
all_agent_simulation_data = [json_load(path) for path in agent_simulation_data_files] # one agent simulation data per hospital
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
171
|
+
|
|
172
|
+
# Data per hospital
|
|
173
|
+
for i, agent_simulation_data in enumerate(all_agent_simulation_data):
|
|
174
|
+
agent_results, done_patients, dialog_results = dict(), dict(), dict()
|
|
175
|
+
Simulator.shuffle_data(agent_simulation_data)
|
|
176
|
+
environment = HospitalEnvironment(
|
|
177
|
+
agent_simulation_data,
|
|
178
|
+
self.fhir_url,
|
|
179
|
+
self.fhir_max_connection_retries,
|
|
180
|
+
self.simulation_start_day_before
|
|
181
|
+
)
|
|
182
|
+
basename = os.path.splitext(os.path.basename(agent_simulation_data_files[i]))[0]
|
|
183
|
+
save_path = os.path.join(output_dir, f'{basename}_result.json')
|
|
184
|
+
d_save_path = os.path.join(output_dir, f'{basename}_dialog.json')
|
|
185
|
+
log(f'{basename} simulation started..', color=True)
|
|
186
|
+
|
|
187
|
+
# Resume the results and the virtual hospital environment
|
|
188
|
+
if resume and os.path.exists(save_path):
|
|
189
|
+
agent_simulation_data, agent_results, dialog_results, done_patients = Simulator.resume_results(agent_simulation_data, save_path, d_save_path)
|
|
190
|
+
environment.resume(agent_results)
|
|
191
|
+
|
|
192
|
+
# Data per patient
|
|
193
|
+
for j, (gt, test_data) in enumerate(agent_simulation_data['agent_data']):
|
|
194
|
+
for task in self.task_queue:
|
|
195
|
+
if task.name in done_patients and gt['patient'] in done_patients[task.name]:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
result = task((gt, test_data), agent_simulation_data, agent_results, environment, verbose)
|
|
199
|
+
dialogs = result.pop('dialog')
|
|
200
|
+
|
|
201
|
+
# Append a single result
|
|
202
|
+
agent_results.setdefault(task.name, {'gt': [], 'pred': [], 'status': [], 'status_code': [], 'trial': [], 'dialog': []})
|
|
203
|
+
for k in result:
|
|
204
|
+
agent_results[task.name][k] += result[k]
|
|
205
|
+
|
|
206
|
+
if task.name == 'intake':
|
|
207
|
+
dialog_results[gt['patient']] = dialogs[0]
|
|
208
|
+
else:
|
|
209
|
+
agent_results[task.name]['dialog'] += dialogs
|
|
210
|
+
|
|
211
|
+
# Logging the results
|
|
212
|
+
for task_name, result in agent_results.items():
|
|
213
|
+
correctness = [x for y in result['status'] for x in (y if isinstance(y, list) or isinstance(y, tuple) else [y])]
|
|
214
|
+
status_code = [x for y in result['status_code'] for x in (y if isinstance(y, list) or isinstance(y, tuple) else [y])]
|
|
215
|
+
accuracy = sum(correctness) / len(correctness)
|
|
216
|
+
log(f'{basename} - {task_name} task results..', color=True)
|
|
217
|
+
log(f' - accuracy: {accuracy:.3f}, length: {len(correctness)}, status_code: {status_code}')
|
|
218
|
+
|
|
219
|
+
json_save_fast(save_path, agent_results)
|
|
220
|
+
if 'intake' in self.task_list:
|
|
221
|
+
json_save_fast(d_save_path, dialog_results)
|
|
222
|
+
|
|
223
|
+
log(f"Agent completed the tasks successfully", color=True)
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
if len(agent_results):
|
|
227
|
+
json_save_fast(save_path, agent_results)
|
|
228
|
+
if 'intake' in self.task_list:
|
|
229
|
+
json_save_fast(d_save_path, dialog_results)
|
|
230
|
+
log("Error occured while execute the tasks.", level='error')
|
|
231
|
+
raise e
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
STATUS_CODES = {
|
|
2
|
+
'format': 'incorrect format',
|
|
3
|
+
'department': 'incorrect department',
|
|
4
|
+
'patient': 'incorrect patient information',
|
|
5
|
+
'department & patient': 'incorrect department and patient information',
|
|
6
|
+
'simulation': 'incomplete simulation',
|
|
7
|
+
'schedule': 'invalid schedule',
|
|
8
|
+
'duration': 'wrong duration',
|
|
9
|
+
'conflict': {
|
|
10
|
+
'physician': 'physician conflict',
|
|
11
|
+
'time': 'time conflict'
|
|
12
|
+
},
|
|
13
|
+
'preference': {
|
|
14
|
+
'physician': 'mismatched physician',
|
|
15
|
+
'asap': 'not earliest schedule',
|
|
16
|
+
'date': 'not valid date',
|
|
17
|
+
},
|
|
18
|
+
'cancel': {
|
|
19
|
+
'identify': 'cancel: fail to identify requested schedule',
|
|
20
|
+
'type': 'cancel: unexpected tool calling result'
|
|
21
|
+
},
|
|
22
|
+
'reschedule': {
|
|
23
|
+
'identify': 'reschedule: fail to identify requested schedule',
|
|
24
|
+
'schedule': 'reschedule: {status_code}',
|
|
25
|
+
'type': 'reschedule: unexpected tool calling result'
|
|
26
|
+
},
|
|
27
|
+
'preceding': 'preceding task failed',
|
|
28
|
+
'unexpected': "unexpected error: {e}",
|
|
29
|
+
'correct': 'pass',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# SCHEDULING_ERROR_CAUSE = {
|
|
33
|
+
# 'incorrect format': [
|
|
34
|
+
# '* There is an issue with the output format. Please perform scheduling in the correct format.',
|
|
35
|
+
# ],
|
|
36
|
+
# 'physician conflict': [
|
|
37
|
+
# '* More than one doctor has been assigned. A schedule must be made with exactly one doctor.',
|
|
38
|
+
# ],
|
|
39
|
+
# 'time conflict': [
|
|
40
|
+
# "* The scheduling result overlaps with the doctor's existing schedule.",
|
|
41
|
+
# ],
|
|
42
|
+
# 'mismatched physician': [
|
|
43
|
+
# '* A different doctor was assigned even though the patient requested a specific doctor.',
|
|
44
|
+
# ],
|
|
45
|
+
# 'not earliest schedule': [
|
|
46
|
+
# '* The patient wants the earliest possible appointment in the department, but the assigned time is not the earliest available based on the current time.',
|
|
47
|
+
# '* When scheduling, it is possible to assign an earlier date or time.',
|
|
48
|
+
# "* The previous patient's schedule may have been cancelled. Therefore, it is necessary to carefully compare the hospital's start time with the doctor's schedule to identify available time slots.",
|
|
49
|
+
# ],
|
|
50
|
+
# 'not valid date': [
|
|
51
|
+
# '* The patient is available after a specific date and would like to make an appointment. Please choose the earliest possible time after that date.',
|
|
52
|
+
# ],
|
|
53
|
+
# 'invalid schedule': [
|
|
54
|
+
# "* The scheduling result may fall outside the hospital's operating hours.",
|
|
55
|
+
# "* The scheduling result may be in the past relative to the current time.",
|
|
56
|
+
# "* The scheduling result may not be a valid date.",
|
|
57
|
+
# "* The assigned doctor may not belong to the department the patient should visit.",
|
|
58
|
+
# ],
|
|
59
|
+
# 'wrong duration': [
|
|
60
|
+
# "* The patient's schedule does not match the consultation duration required by the doctor.",
|
|
61
|
+
# ],
|
|
62
|
+
# # 'workload balancing': [
|
|
63
|
+
# # "* You must schedule the appointment with a doctor who has a lower workload than the current doctor.",
|
|
64
|
+
# # ]
|
|
65
|
+
# }
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ToolCallingError(Exception):
|
|
69
|
+
error_code = "TOOL_CALLING_ERROR"
|
|
70
|
+
|
|
71
|
+
def __init__(self, message: str):
|
|
72
|
+
super().__init__(message)
|
|
73
|
+
self.message = message
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ScheduleNotFoundError(Exception):
|
|
77
|
+
error_code = "SCHEDULE_NOT_FOUND_ERROR"
|
|
78
|
+
|
|
79
|
+
def __init__(self, message: str):
|
|
80
|
+
super().__init__(message)
|
|
81
|
+
self.message = message
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SchedulingError(Exception):
|
|
85
|
+
error_code = "SCHEDULING_ERROR"
|
|
86
|
+
|
|
87
|
+
def __init__(self, message: str):
|
|
88
|
+
super().__init__(message)
|
|
89
|
+
self.message = message
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
class Hospital:
|
|
2
|
+
def __init__(self,
|
|
3
|
+
hospital_name: str,
|
|
4
|
+
department_num: int,
|
|
5
|
+
doctor_num: int,
|
|
6
|
+
time: dict,
|
|
7
|
+
**kwargs):
|
|
8
|
+
self.hospital_name = hospital_name
|
|
9
|
+
self.department_num = department_num
|
|
10
|
+
self.doctor_num = doctor_num
|
|
11
|
+
self.time = time
|
|
12
|
+
self.department: list[Department] = []
|
|
13
|
+
for key, value in kwargs.items():
|
|
14
|
+
setattr(self, key, value)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def add_department(self, department_name: str, **kwargs):
|
|
18
|
+
"""
|
|
19
|
+
Add a department to the hospital.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
department_name (str): Name of the department to add.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Department: The newly created Department object.
|
|
26
|
+
"""
|
|
27
|
+
dept = Department(department_name, **kwargs)
|
|
28
|
+
self.department.append(dept)
|
|
29
|
+
return dept
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def reset_departments(self):
|
|
33
|
+
"""
|
|
34
|
+
Reset the list of departments in the hospital.
|
|
35
|
+
"""
|
|
36
|
+
self.department = []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def __repr__(self):
|
|
40
|
+
return f"Hospital(name={self.hospital_name}, departments={[d.name for d in self.department]}, time={self.time})"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Department:
|
|
45
|
+
def __init__(self, name: str, **kwargs):
|
|
46
|
+
self.name = name
|
|
47
|
+
self.doctor: list[Doctor] = []
|
|
48
|
+
for key, value in kwargs.items():
|
|
49
|
+
setattr(self, key, value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def add_doctor(self, doctor_name: str, **kwargs):
|
|
53
|
+
"""
|
|
54
|
+
Add a doctor to the department.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
doctor_name (str): Name of the doctor to add.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Doctor: The newly created Doctor object.
|
|
61
|
+
"""
|
|
62
|
+
doctor = Doctor(doctor_name, self, **kwargs)
|
|
63
|
+
self.doctor.append(doctor)
|
|
64
|
+
return doctor
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def reset_doctors(self):
|
|
68
|
+
"""
|
|
69
|
+
Reset the list of doctors in the department.
|
|
70
|
+
"""
|
|
71
|
+
self.doctor = []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def __repr__(self):
|
|
75
|
+
return f"Department(name={self.name}, doctors={[d.name for d in self.doctor]})"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Doctor:
|
|
80
|
+
def __init__(self, name: str, department: Department, **kwargs):
|
|
81
|
+
self.name = name
|
|
82
|
+
self.department = department
|
|
83
|
+
self.schedule = []
|
|
84
|
+
self.patient: list[Patient] = []
|
|
85
|
+
for key, value in kwargs.items():
|
|
86
|
+
setattr(self, key, value)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def add_patient(self, patient_name: str, **kwargs):
|
|
90
|
+
"""
|
|
91
|
+
Add a patient to the doctor.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
patient_name (str): Name of the patient to add.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Paitnet: The newly created Paitent object.
|
|
98
|
+
"""
|
|
99
|
+
patient = Patient(patient_name, self, **kwargs)
|
|
100
|
+
self.patient.append(patient)
|
|
101
|
+
return patient
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def reset_patients(self):
|
|
105
|
+
"""
|
|
106
|
+
Reset the list of patients in the doctor.
|
|
107
|
+
"""
|
|
108
|
+
self.patient = []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def __repr__(self):
|
|
112
|
+
return f"Doctor(name={self.name}, department={self.department.name}), schedule={self.schedule})"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Patient:
|
|
117
|
+
def __init__(self, name: str, attending_physician: Doctor, **kwargs):
|
|
118
|
+
self.name = name
|
|
119
|
+
self.attending_physician = attending_physician
|
|
120
|
+
self.schedule = []
|
|
121
|
+
for key, value in kwargs.items():
|
|
122
|
+
setattr(self, key, value)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def __repr__(self):
|
|
126
|
+
return f"Patient(name={self.name}, department={self.attending_physician.department.name}), attending_physician={self.attending_physician.name}), schedule={self.schedule})"
|