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,222 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from urllib.parse import urlencode
|
|
4
|
+
|
|
5
|
+
from h_adminsim.utils import log
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FHIRManager:
|
|
10
|
+
def __init__(self, fhir_url):
|
|
11
|
+
self.fhir_url = fhir_url
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def __logging(self, response: requests.Response, verbose=True) -> Optional[requests.Response]:
|
|
15
|
+
"""
|
|
16
|
+
Log the response status code and content.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
response (requests.Response): The HTTP response object.
|
|
20
|
+
verbose (bool, optional): If True, log details. Defaults to True.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Optional[requests.Response]: The response object if JSON parsing is successful, else None.
|
|
24
|
+
"""
|
|
25
|
+
if 200 <= response.status_code < 300:
|
|
26
|
+
if verbose:
|
|
27
|
+
log(f'Status code: {response.status_code}', color=True)
|
|
28
|
+
else:
|
|
29
|
+
if verbose:
|
|
30
|
+
log(f'Status code: {response.status_code}', level='error')
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
response_json = response.json()
|
|
34
|
+
if verbose:
|
|
35
|
+
log(f'Response JSON: {response_json}')
|
|
36
|
+
except ValueError:
|
|
37
|
+
if verbose:
|
|
38
|
+
log(f'Response Text: {response.text}', level='error')
|
|
39
|
+
response = None
|
|
40
|
+
return response
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def create(self,
|
|
44
|
+
resource_type: str,
|
|
45
|
+
resource_data: dict,
|
|
46
|
+
headers: Optional[dict] = None,
|
|
47
|
+
verbose: bool = True) -> Optional[requests.Response]:
|
|
48
|
+
"""
|
|
49
|
+
Create a FHIR resource of the specified type.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
resource_type (str): FHIR resource type (e.g., "Patient", "PractitionerRole").
|
|
53
|
+
resource_data (dict): FHIR resource data as a dictionary.
|
|
54
|
+
headers (Optional[dict], optional): HTTP headers to use. Defaults to None.
|
|
55
|
+
verbose (bool, optional): If True, log details. Defaults to True.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Optional[requests.Response]: The HTTP response object if JSON parsing is successful, else None.
|
|
59
|
+
"""
|
|
60
|
+
_id = resource_data.get('id')
|
|
61
|
+
fhir_url = f'{self.fhir_url}/{resource_type}/{_id}'
|
|
62
|
+
response = requests.put(
|
|
63
|
+
fhir_url,
|
|
64
|
+
headers={'Content-Type': 'application/fhir+json'} if headers is None else headers,
|
|
65
|
+
json=resource_data,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Log and return the response
|
|
69
|
+
return self.__logging(response, verbose)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def read(self,
|
|
73
|
+
resource_type: str,
|
|
74
|
+
id: str,
|
|
75
|
+
headers: Optional[dict] = None,
|
|
76
|
+
verbose: bool = True) -> Optional[requests.Response]:
|
|
77
|
+
"""
|
|
78
|
+
Read a FHIR resource of the specified type and ID.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
resource_type (str): FHIR resource type (e.g., "Patient", "PractitionerRole").
|
|
82
|
+
id (str): The ID of the FHIR resource to read.
|
|
83
|
+
headers (Optional[dict], optional): HTTP headers to use. Defaults to None.
|
|
84
|
+
verbose (bool, optional): If True, log details. Defaults to True.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Optional[requests.Response]: The HTTP response object if JSON parsing is successful, else None.
|
|
88
|
+
"""
|
|
89
|
+
fhir_url = f'{self.fhir_url}/{resource_type}/{id}'
|
|
90
|
+
response = requests.get(
|
|
91
|
+
fhir_url,
|
|
92
|
+
headers={'Accept': 'application/fhir+json'} if headers is None else headers,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Log and return the response
|
|
96
|
+
return self.__logging(response, verbose)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def update(self,
|
|
100
|
+
resource_type: str,
|
|
101
|
+
id: str,
|
|
102
|
+
resource_data: dict,
|
|
103
|
+
headers: Optional[dict] = None,
|
|
104
|
+
verbose: bool = True) -> Optional[requests.Response]:
|
|
105
|
+
"""
|
|
106
|
+
Update a FHIR resource of the specified type and ID.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
resource_type (str): FHIR resource type (e.g., "Patient", "PractitionerRole").
|
|
110
|
+
id (str): The ID of the FHIR resource to update.
|
|
111
|
+
resource_data (dict): FHIR resource data as a dictionary.
|
|
112
|
+
headers (Optional[dict], optional): HTTP headers to use. Defaults to None.
|
|
113
|
+
verbose (bool, optional): If True, log details. Defaults to True.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Optional[requests.Response]: _description_
|
|
117
|
+
"""
|
|
118
|
+
fhir_url = f'{self.fhir_url}/{resource_type}/{id}'
|
|
119
|
+
response = requests.put(
|
|
120
|
+
fhir_url,
|
|
121
|
+
headers={'Content-Type': 'application/fhir+json'} if headers is None else headers,
|
|
122
|
+
json=resource_data,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Log and return the response
|
|
126
|
+
return self.__logging(response, verbose)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def delete(self, resource_type: str, id: str, verbose=True):
|
|
130
|
+
fhir_url = f'{self.fhir_url}/{resource_type}/{id}'
|
|
131
|
+
response = requests.delete(
|
|
132
|
+
fhir_url
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Log and return the response
|
|
136
|
+
return self.__logging(response, verbose)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def read_all(self,
|
|
140
|
+
resource_type: str,
|
|
141
|
+
headers: Optional[dict] = None,
|
|
142
|
+
count: int = 100,
|
|
143
|
+
verbose: bool = True,
|
|
144
|
+
params: Optional[dict] = None) -> list[dict]:
|
|
145
|
+
"""
|
|
146
|
+
Read all resources of a given resource type using FHIR search with optional filtering.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
resource_type (str): FHIR resource type (e.g., "PractitionerRole").
|
|
150
|
+
headers (dict, optional): HTTP headers to use.
|
|
151
|
+
count (int): Number of resources to fetch per page (default: 100).
|
|
152
|
+
verbose (bool): If True, log each response. Defaults to True.
|
|
153
|
+
params (dict, optional): FHIR search parameters (e.g., {"specialty": "IMALL-2"}).
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
list[dict]: List of bundle entry dicts.
|
|
157
|
+
"""
|
|
158
|
+
all_entries = []
|
|
159
|
+
headers = {'Accept': 'application/fhir+json'} if headers is None else headers
|
|
160
|
+
|
|
161
|
+
# Build first page URL with params
|
|
162
|
+
q = {'_count': count}
|
|
163
|
+
if params:
|
|
164
|
+
q.update({k: v for k, v in params.items() if v is not None})
|
|
165
|
+
url = f"{self.fhir_url}/{resource_type}?{urlencode(q, doseq=True)}"
|
|
166
|
+
|
|
167
|
+
while url:
|
|
168
|
+
response = requests.get(url, headers=headers)
|
|
169
|
+
self.__logging(response, verbose)
|
|
170
|
+
try:
|
|
171
|
+
bundle = response.json()
|
|
172
|
+
except Exception:
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
if bundle.get('resourceType') != 'Bundle' or 'entry' not in bundle:
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
all_entries.extend(bundle['entry'])
|
|
179
|
+
|
|
180
|
+
# Check for next link (pagination)
|
|
181
|
+
next_link = next(
|
|
182
|
+
(link.get('url') for link in bundle.get('link', []) if link.get('relation') == 'next'),
|
|
183
|
+
None
|
|
184
|
+
)
|
|
185
|
+
url = next_link # Continue if next page exists, else break
|
|
186
|
+
|
|
187
|
+
return all_entries
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def delete_all(self, entry: list[dict], verbose: bool = True):
|
|
191
|
+
"""
|
|
192
|
+
Delete all FHIR resources from a given list of resource entries.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
entry (list[dict]): List of FHIR Bundle entries, typically from the `read_all()` method.
|
|
196
|
+
Each entry should contain a 'resource' dict with 'resourceType' and 'id'.
|
|
197
|
+
verbose (bool): If True, log each deletion response. Defaults to True.
|
|
198
|
+
"""
|
|
199
|
+
error_ids = list()
|
|
200
|
+
|
|
201
|
+
for resource in entry:
|
|
202
|
+
resource_type = resource.get('resource').get('resourceType')
|
|
203
|
+
id = resource.get('resource').get('id')
|
|
204
|
+
response = self.delete(resource_type, id, verbose)
|
|
205
|
+
|
|
206
|
+
if not 200 <= response.status_code < 300:
|
|
207
|
+
error_ids.append(id)
|
|
208
|
+
|
|
209
|
+
if error_ids:
|
|
210
|
+
log(f'Error(s) occurs during delete resources: {error_ids}', 'warning')
|
|
211
|
+
else:
|
|
212
|
+
log('Deletion successfully completed', color=True)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# PostgreSQL
|
|
218
|
+
# docker exec -it jmlee_fhir_db psql -U admin -d hapi
|
|
219
|
+
# SELECT * FROM hfj_resource WHERE res_type = 'Patient' LIMIT 1;
|
|
220
|
+
# SELECT * FROM HFJ_RES_VER WHERE RES_ID = 925754;
|
|
221
|
+
|
|
222
|
+
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from typing import Tuple, Optional
|
|
3
|
+
|
|
4
|
+
from h_adminsim.utils.common_utils import (
|
|
5
|
+
convert_segment_to_time,
|
|
6
|
+
convert_time_to_segment,
|
|
7
|
+
group_consecutive_segments,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ScheduleAssigner:
|
|
13
|
+
def __init__(self, start: float, end: float, interval: float):
|
|
14
|
+
"""
|
|
15
|
+
Initialize a ScheduleAssigner for generating random schedules or appointments.
|
|
16
|
+
|
|
17
|
+
This class divides a given time range into fixed-size segments and provides methods
|
|
18
|
+
to assign schedules or appointments by selecting and grouping these segments.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
start (float): Start time in hours (e.g., 9.0 for 09:00).
|
|
22
|
+
end (float): End time in hours (e.g., 18.0 for 18:00).
|
|
23
|
+
interval (float): Time interval in hours for each segment (e.g., 0.5 for 30 minutes).
|
|
24
|
+
"""
|
|
25
|
+
self.start = start
|
|
26
|
+
self.end = end
|
|
27
|
+
self.interval = interval
|
|
28
|
+
self.segments = convert_time_to_segment(self.start, self.end, self.interval)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def schedule_segment_assign(self,
|
|
32
|
+
p: float,
|
|
33
|
+
segments: Optional[list[int]] = None) -> list[list[int]]:
|
|
34
|
+
"""
|
|
35
|
+
Randomly assign a proportion of schedule time segments into grouped consecutive blocks.
|
|
36
|
+
|
|
37
|
+
This method selects a random subset of segments, where the number of segments is
|
|
38
|
+
determined by the proportion `p` (e.g., 0.5 means 50% of all segments).
|
|
39
|
+
The selected segments are then grouped into lists of consecutive segment indices.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
p (float): Proportion of total segments to assign, between 0 and 1.
|
|
43
|
+
segments (Optional[list[int]], optional): Specific segments. Defaults to None.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
list[list[int]]: A list of groups, where each group is a list of consecutive segment indices.
|
|
47
|
+
For example, [[0, 1], [3, 4, 5], [7]].
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
If segments = [0, 1, 2, ..., 11] and p = 0.5,
|
|
51
|
+
this function might return something like:
|
|
52
|
+
[[0, 1], [4], [6, 7, 8]]
|
|
53
|
+
|
|
54
|
+
Notes:
|
|
55
|
+
- The segment indices are selected randomly each time the function is called.
|
|
56
|
+
- Groups are always composed of consecutive indices from the selected subset.
|
|
57
|
+
"""
|
|
58
|
+
segments = self.segments if segments == None else segments
|
|
59
|
+
segment_n = round(len(segments) * p)
|
|
60
|
+
|
|
61
|
+
if segment_n > 0:
|
|
62
|
+
# Select random segments
|
|
63
|
+
chosen_segments = random.sample(segments, segment_n)
|
|
64
|
+
chosen_segments.sort()
|
|
65
|
+
|
|
66
|
+
# Grouping consecutive segments
|
|
67
|
+
grouped = group_consecutive_segments(chosen_segments)
|
|
68
|
+
return grouped
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def appointment_segment_assign(self,
|
|
73
|
+
p: float,
|
|
74
|
+
min_chunk_size: int,
|
|
75
|
+
max_chunk_size: int,
|
|
76
|
+
segments: Optional[list[int]] = None) -> list[list[int]]:
|
|
77
|
+
"""
|
|
78
|
+
Randomly assign appointment time segments from the remaining (unassigned) segments.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
p (float): Proportion of remaining segments to sample and assign.
|
|
82
|
+
min_chunk_size (int): The minimum time segment size for each appointment.
|
|
83
|
+
max_chunk_size (int): The maximum time segment size for each appointment.
|
|
84
|
+
segments (Optional[list[int]], optional): Specific segments. Defaults to None.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
list[list[int]]: Newly assigned segments from the remaining pool, grouped consecutively.
|
|
88
|
+
"""
|
|
89
|
+
segments = self.segments if segments == None else segments
|
|
90
|
+
avg_chunk_size = (min_chunk_size + max_chunk_size) / 2
|
|
91
|
+
segment_n = int(len(segments) * p // avg_chunk_size)
|
|
92
|
+
|
|
93
|
+
if segment_n > 0:
|
|
94
|
+
chosen = []
|
|
95
|
+
used = set()
|
|
96
|
+
max_index = max(segments)
|
|
97
|
+
random.shuffle(segments)
|
|
98
|
+
|
|
99
|
+
for s in segments:
|
|
100
|
+
max_possible_size = min(max_chunk_size, max_index - s + 1)
|
|
101
|
+
if max_possible_size < min_chunk_size:
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
chunk_size = random.randint(min_chunk_size, max_chunk_size)
|
|
105
|
+
trip = set(range(s, s + chunk_size))
|
|
106
|
+
|
|
107
|
+
if used.isdisjoint(trip):
|
|
108
|
+
chosen.append((s, chunk_size))
|
|
109
|
+
used |= trip
|
|
110
|
+
if len(chosen) == segment_n:
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
return [list(range(start, start + size)) for start, size in sorted(chosen)]
|
|
114
|
+
return []
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def __call__(self,
|
|
118
|
+
p: float,
|
|
119
|
+
is_appointment: bool = False,
|
|
120
|
+
segments: Optional[list[list[int]]] = None,
|
|
121
|
+
**kwargs) -> Tuple[list[list[int]], list[list[float]]]:
|
|
122
|
+
"""
|
|
123
|
+
Generate grouped time ranges by randomly selecting and grouping a proportion of time segments.
|
|
124
|
+
|
|
125
|
+
This method allows the ScheduleAssigner instance to be called directly with a proportion `p`.
|
|
126
|
+
It selects a subset of time segments based on `p`, groups them into consecutive segment blocks,
|
|
127
|
+
and converts each group of segment indices into their corresponding time values.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
p (float): Proportion of total segments to select, between 0 and 1.
|
|
131
|
+
is_appointment (bool, optional): Whether the generated schedules are for appointments. Defaults to False.
|
|
132
|
+
segments (Optional[list[list[int]]], optional): Specific segemnts. Defaults to None.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
list[list[float]]: A list of grouped time segments. Each group is a list of time segemnt values.
|
|
136
|
+
For example: [[2, 3], [5, 6, 7]].
|
|
137
|
+
list[list[float]]: A list of grouped time ranges. Each group is a list of time values (in hours),
|
|
138
|
+
corresponding to consecutive time segments.
|
|
139
|
+
For example: [[0.0, 0.5], [2.0, 3.0]].
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> assigner = ScheduleAssigner(0, 12, 0.5)
|
|
143
|
+
>>> assigner(0.25)
|
|
144
|
+
[[1.0, 1.5], [4.5], [6.0, 6.5]]
|
|
145
|
+
"""
|
|
146
|
+
time_segments = self.appointment_segment_assign(p, segments=segments, **kwargs) \
|
|
147
|
+
if is_appointment else self.schedule_segment_assign(p, segments)
|
|
148
|
+
|
|
149
|
+
if len(time_segments):
|
|
150
|
+
return time_segments, [list(convert_segment_to_time(self.start, self.end, self.interval, segments)) for segments in time_segments]
|
|
151
|
+
return [], []
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from tqdm import tqdm
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from decimal import getcontext
|
|
5
|
+
from importlib import resources
|
|
6
|
+
|
|
7
|
+
from h_adminsim.utils.fhir_utils import *
|
|
8
|
+
from h_adminsim.utils.random_utils import generate_random_symptom
|
|
9
|
+
from h_adminsim.utils.filesys_utils import json_load, json_save_fast, get_files
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentDataBuilder:
|
|
14
|
+
def __init__(self, config):
|
|
15
|
+
# Initialize configuration
|
|
16
|
+
data_dir = os.path.join(config.project, config.data_name, 'data')
|
|
17
|
+
self.data_files = get_files(data_dir, ext='json')
|
|
18
|
+
getcontext().prec = 10
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def build(data: dict,
|
|
23
|
+
save_path: Optional[str] = None,
|
|
24
|
+
symptom_file_path: Optional[str] = None) -> dict:
|
|
25
|
+
"""
|
|
26
|
+
Build agent test data from a single hospital data entry.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
data (dict): Dictionary containing metadata, departments, doctors, and patients.
|
|
30
|
+
save_path (Optional[str], optional): If provided, the generated agent data will be saved to this path.
|
|
31
|
+
symptom_file_path (str, optional): Path to the JSON file containing symptoms per department. Defaults to None.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
dict: A dictionary with the following keys:
|
|
35
|
+
- 'metadata': Original metadata from input.
|
|
36
|
+
- 'department': Department-level information.
|
|
37
|
+
- 'doctor': Doctor-level information.
|
|
38
|
+
- 'agent_data': List of tuples. Each tuple consists of:
|
|
39
|
+
- Ground-truth scheduling information.
|
|
40
|
+
- Agent input (symptom and constraints).
|
|
41
|
+
"""
|
|
42
|
+
if symptom_file_path == None:
|
|
43
|
+
symptom_file_path = str(resources.files("h_adminsim.assets.departments").joinpath("symptom.json"))
|
|
44
|
+
|
|
45
|
+
agent_data = {'metadata': data['metadata'], 'department': data['department'], 'doctor': data['doctor'], 'agent_data': []}
|
|
46
|
+
|
|
47
|
+
for patient, patient_values in data['patient'].items():
|
|
48
|
+
doctor, department, date = patient_values['attending_physician'], patient_values['department'], patient_values['date']
|
|
49
|
+
gender, telecom, birth_date, identifier, address = \
|
|
50
|
+
patient_values['gender'], patient_values['telecom'], patient_values['birthDate'], patient_values['identifier'], patient_values['address']
|
|
51
|
+
preference, symptom_level = patient_values['preference'], patient_values['symptom_level']
|
|
52
|
+
disease = generate_random_symptom(
|
|
53
|
+
department=department,
|
|
54
|
+
symptom_file_path=symptom_file_path,
|
|
55
|
+
ensure_unique_department='doctor' in patient_values['preference']
|
|
56
|
+
)
|
|
57
|
+
gt_department = disease['department'] if isinstance(disease, dict) else [department]
|
|
58
|
+
gt = {
|
|
59
|
+
'patient': patient,
|
|
60
|
+
'gender': gender,
|
|
61
|
+
'telecom': telecom,
|
|
62
|
+
'birthDate': birth_date,
|
|
63
|
+
'identifier': identifier,
|
|
64
|
+
'address': address,
|
|
65
|
+
'department': gt_department,
|
|
66
|
+
'attending_physician': doctor,
|
|
67
|
+
'valid_from': date if 'date' in preference else 'N/A',
|
|
68
|
+
'preference': preference,
|
|
69
|
+
'symptom_level': symptom_level,
|
|
70
|
+
}
|
|
71
|
+
agent = {
|
|
72
|
+
'patient': patient,
|
|
73
|
+
'gender': gender,
|
|
74
|
+
'telecom': telecom,
|
|
75
|
+
'birthDate': birth_date,
|
|
76
|
+
'identifier': identifier,
|
|
77
|
+
'address': address,
|
|
78
|
+
'constraint': {
|
|
79
|
+
'preference': preference,
|
|
80
|
+
'attending_physician': doctor,
|
|
81
|
+
'valid_from': date if 'date' in preference else 'N/A',
|
|
82
|
+
'symptom_level': symptom_level,
|
|
83
|
+
'symptom': disease,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
agent_data['agent_data'].append((gt, agent))
|
|
87
|
+
|
|
88
|
+
if save_path:
|
|
89
|
+
json_save_fast(
|
|
90
|
+
save_path,
|
|
91
|
+
agent_data
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return agent_data
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def __call__(self,
|
|
98
|
+
output_dir: Optional[str] = None,
|
|
99
|
+
symptom_file_path: Optional[str] = None) -> list[dict]:
|
|
100
|
+
"""
|
|
101
|
+
Generate agent test datasets for all input data files.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
output_dir (Optional[str], optional): Directory to save the generated agent data files.
|
|
105
|
+
If not provided, files are not saved.
|
|
106
|
+
symptom_file_path (Optional[str], optional): Path to the symptom file used during agent construction. Defaults to None.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
list[dict]: A list of agent test data dictionaries, one for each processed input file.
|
|
110
|
+
"""
|
|
111
|
+
if symptom_file_path == None:
|
|
112
|
+
symptom_file_path = str(resources.files("h_adminsim.assets.departments").joinpath("symptom.json"))
|
|
113
|
+
|
|
114
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
115
|
+
all_agent_data = list()
|
|
116
|
+
|
|
117
|
+
for data_file in tqdm(self.data_files, desc='Generating data for agent simulation..'):
|
|
118
|
+
data = json_load(data_file)
|
|
119
|
+
basename, ext = os.path.splitext(os.path.basename(data_file))
|
|
120
|
+
agent_data = AgentDataBuilder.build(data, os.path.join(output_dir, f"{basename}_agent{ext}"), symptom_file_path)
|
|
121
|
+
all_agent_data.append(agent_data)
|
|
122
|
+
|
|
123
|
+
return all_agent_data
|
|
124
|
+
|