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,1126 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import random
|
|
5
|
+
from importlib import resources
|
|
6
|
+
from patientsim import PatientAgent
|
|
7
|
+
from decimal import Decimal, getcontext
|
|
8
|
+
from typing import Tuple, Union, Optional
|
|
9
|
+
from langchain.agents import AgentExecutor
|
|
10
|
+
from langchain_core.messages import HumanMessage, AIMessage
|
|
11
|
+
|
|
12
|
+
from h_adminsim import AdminStaffAgent
|
|
13
|
+
from h_adminsim.registry.errors import ToolCallingError, ScheduleNotFoundError, SchedulingError
|
|
14
|
+
from h_adminsim.registry import PREFERENCE_PHRASE_PATIENT, PREFERENCE_PHRASE_STAFF, STATUS_CODES
|
|
15
|
+
from h_adminsim.environment.hospital import HospitalEnvironment
|
|
16
|
+
from h_adminsim.utils import log, colorstr
|
|
17
|
+
from h_adminsim.tools.sanity_checker import SanityChecker
|
|
18
|
+
from h_adminsim.tools import SchedulingRule, scheduling_tool_calling
|
|
19
|
+
from h_adminsim.utils.common_utils import *
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OPScehdulingSimulation:
|
|
24
|
+
def __init__(self,
|
|
25
|
+
patient_agent: PatientAgent,
|
|
26
|
+
admin_staff_agent: AdminStaffAgent,
|
|
27
|
+
metadata: dict,
|
|
28
|
+
department_data: dict,
|
|
29
|
+
environment: HospitalEnvironment,
|
|
30
|
+
scheduling_strategy: str = 'tool_calling',
|
|
31
|
+
preference_rejection_prob: float = 0.3,
|
|
32
|
+
preference_rejection_prob_decay: float = 0.5,
|
|
33
|
+
fhir_integration: bool = False,
|
|
34
|
+
schedule_rejection_prompt_path: Optional[str] = None,
|
|
35
|
+
sanity_checker: Optional[SanityChecker] = None):
|
|
36
|
+
|
|
37
|
+
# Initialize simulation parameters
|
|
38
|
+
getcontext().prec = 10
|
|
39
|
+
self.patient_agent = patient_agent
|
|
40
|
+
self.admin_staff_agent = admin_staff_agent
|
|
41
|
+
self.environment = environment
|
|
42
|
+
self._START_HOUR = metadata['time']['start_hour']
|
|
43
|
+
self._END_HOUR = metadata['time']['end_hour']
|
|
44
|
+
self._TIME_UNIT = metadata['time']['interval_hour']
|
|
45
|
+
self._DAY = metadata['days']
|
|
46
|
+
self.scheduling_strategy = scheduling_strategy
|
|
47
|
+
self.preference_rejection_prob = preference_rejection_prob
|
|
48
|
+
self.preference_rejection_prob_decay = preference_rejection_prob_decay
|
|
49
|
+
self.fhir_integration = fhir_integration
|
|
50
|
+
self.rejection_system_prompt_template = self._init_prompt(schedule_rejection_prompt_path)
|
|
51
|
+
self.sanity_checker = sanity_checker
|
|
52
|
+
self.rules = SchedulingRule(metadata, department_data, self.environment, self.fhir_integration)
|
|
53
|
+
self.end_phrase = "Thank you."
|
|
54
|
+
self._init_history()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _init_prompt(self, schedule_rejection_prompt_path: Optional[str] = None) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Initialize the schedule rejection system prompt for the administration staff agent.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
schedule_rejection_prompt_path (Optional[str], optional): Path to a custom schedule rejection system prompt file.
|
|
63
|
+
If not provided, the default system prompt will be used. Defaults to None.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
FileNotFoundError: If the specified system prompt file does not exist.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
str: Schedule rejection prompt template.
|
|
70
|
+
"""
|
|
71
|
+
# Initialilze with the default system prompt
|
|
72
|
+
if not schedule_rejection_prompt_path:
|
|
73
|
+
prompt_file_name = "schedule_patient_rejected_system.txt"
|
|
74
|
+
file_path = resources.files("h_adminsim.assets.prompts").joinpath(prompt_file_name)
|
|
75
|
+
rejection_system_prompt_template = file_path.read_text()
|
|
76
|
+
|
|
77
|
+
# User can specify a custom system prompt
|
|
78
|
+
else:
|
|
79
|
+
if not os.path.exists(schedule_rejection_prompt_path):
|
|
80
|
+
raise FileNotFoundError(colorstr("red", f"System prompt file not found: {schedule_rejection_prompt_path}"))
|
|
81
|
+
with open(schedule_rejection_prompt_path, 'r') as f:
|
|
82
|
+
rejection_system_prompt_template = f.read()
|
|
83
|
+
return rejection_system_prompt_template
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _init_agents(self, verbose: bool = True):
|
|
87
|
+
"""
|
|
88
|
+
Reset the conversation histories and token usage records of both the Patient and Doctor agents.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
verbose (bool, optional): Whether to print verbose output. Defaults to True.
|
|
92
|
+
"""
|
|
93
|
+
self.patient_agent.reset_history(verbose=verbose)
|
|
94
|
+
self.admin_staff_agent.reset_history(verbose=verbose)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _init_history(self):
|
|
98
|
+
"""
|
|
99
|
+
Reset the dialogue histories.
|
|
100
|
+
"""
|
|
101
|
+
self.dialog_history = {
|
|
102
|
+
'scheduling': [],
|
|
103
|
+
'cancel': [],
|
|
104
|
+
'reschedule': [],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _to_lc_history(self, key: str) -> list:
|
|
109
|
+
"""
|
|
110
|
+
Convert the dialog history for the given key into LangChain message objects.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
key (str): Key identifying which dialog history to convert.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
list: A list of LangChain HumanMessage and AIMessage objects.
|
|
117
|
+
"""
|
|
118
|
+
msgs = []
|
|
119
|
+
for m in self.dialog_history[key]:
|
|
120
|
+
if m["role"] == "Patient":
|
|
121
|
+
msgs.append(HumanMessage(content=m["content"]))
|
|
122
|
+
elif m["role"] == "Staff":
|
|
123
|
+
msgs.append(AIMessage(content=m["content"]))
|
|
124
|
+
return msgs
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def postprocessing(self,
|
|
128
|
+
strategy: str,
|
|
129
|
+
data: Union[str, dict],
|
|
130
|
+
filtered_doctor_information: Optional[dict] = None) -> Union[str, dict]:
|
|
131
|
+
"""
|
|
132
|
+
Attempts to parse the given text as JSON. If parsing succeeds, returns a dictionary;
|
|
133
|
+
otherwise, returns the original string.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
strategy (str): Scheduling strategy. It must be either `reasoning` or `tool_calling`.
|
|
137
|
+
data (Union[str, dict]): The text output to post-process, potentially a JSON-formatted string.
|
|
138
|
+
filtered_doctor_information (Optional[dict], optional): Department-filtered doctor information
|
|
139
|
+
to postprocess the schedule by tool_calling strategy.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Union[str, dict]: A dictionary if the text is valid JSON, otherwise the original string.
|
|
143
|
+
"""
|
|
144
|
+
if strategy == 'reasoning':
|
|
145
|
+
try:
|
|
146
|
+
if isinstance(data, str):
|
|
147
|
+
match = re.search(r'```json\s*(\{.*?\})\s*```', data, re.DOTALL)
|
|
148
|
+
if match:
|
|
149
|
+
json_str = match.group(1)
|
|
150
|
+
text_dict = json.loads(json_str)
|
|
151
|
+
else:
|
|
152
|
+
try:
|
|
153
|
+
text_dict = json.loads(data)
|
|
154
|
+
except:
|
|
155
|
+
return data
|
|
156
|
+
else:
|
|
157
|
+
text_dict = data
|
|
158
|
+
|
|
159
|
+
assert len(text_dict) == 1 and all(k in text_dict for k in ['schedule']) # Basic sanity check
|
|
160
|
+
key = list(text_dict['schedule'].keys())[0]
|
|
161
|
+
text_dict['schedule'][key]['start'] = float(text_dict['schedule'][key]['start'])
|
|
162
|
+
text_dict['schedule'][key]['end'] = float(text_dict['schedule'][key]['end'])
|
|
163
|
+
text_dict['schedule'][key]['date'] = str(text_dict['schedule'][key]['date'])
|
|
164
|
+
return text_dict
|
|
165
|
+
|
|
166
|
+
except:
|
|
167
|
+
return str(data)
|
|
168
|
+
|
|
169
|
+
elif strategy == 'tool_calling':
|
|
170
|
+
doctor = data['doctor'][0]
|
|
171
|
+
duration = filtered_doctor_information['doctor'][doctor]['outpatient_duration']
|
|
172
|
+
date, st_hour = iso_to_date(data['schedule'][0]), iso_to_hour(data['schedule'][0])
|
|
173
|
+
tr_hour = float(Decimal(str(duration)) + Decimal(str(st_hour)))
|
|
174
|
+
return {'schedule': {doctor: {'date': date, 'start': st_hour, 'end': tr_hour}}}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def update_patient_preference_system_prompt(self,
|
|
178
|
+
patient_condition: dict,
|
|
179
|
+
rejected_preference: str):
|
|
180
|
+
"""
|
|
181
|
+
Update a system prompt of the patient agent for proposed schedule rejection scenario.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
patient_condition (dict): Patient ground-truth condition including current preference.
|
|
185
|
+
rejected_preference (str): The scheduling preference proposed by the staff agent in the previous turn
|
|
186
|
+
that the patient must explicitly reject.
|
|
187
|
+
"""
|
|
188
|
+
# Build new system prompts for rejection scenario
|
|
189
|
+
preference = patient_condition.get('preference')
|
|
190
|
+
preference_desc = PREFERENCE_PHRASE_PATIENT[preference] if preference != 'date' \
|
|
191
|
+
else PREFERENCE_PHRASE_PATIENT[preference].format(date=patient_condition.get('valid_from'))
|
|
192
|
+
rejected_preference_desc = PREFERENCE_PHRASE_STAFF[rejected_preference] if rejected_preference != 'date' \
|
|
193
|
+
else PREFERENCE_PHRASE_STAFF[rejected_preference].format(date='a specific date')
|
|
194
|
+
system_prompt = self.rejection_system_prompt_template.format(
|
|
195
|
+
preference=preference,
|
|
196
|
+
preference_desc=preference_desc,
|
|
197
|
+
preferred_doctor=patient_condition['preferred_doctor'],
|
|
198
|
+
rejected_preference=rejected_preference_desc,
|
|
199
|
+
personality=self.patient_agent.personality,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Update new system prompts for rejection scenario
|
|
203
|
+
self.patient_agent.system_prompt = system_prompt
|
|
204
|
+
if len(self.patient_agent.client.histories) and \
|
|
205
|
+
isinstance(self.patient_agent.client.histories[0], dict) and \
|
|
206
|
+
self.patient_agent.client.histories[0].get('role') == 'system':
|
|
207
|
+
self.patient_agent.client.histories[0]['content'][0]['text'] = system_prompt
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _get_rescheduled_result(self,
|
|
211
|
+
known_condition: dict,
|
|
212
|
+
doctor_information: Optional[dict] = None,
|
|
213
|
+
**kwargs) -> dict:
|
|
214
|
+
"""
|
|
215
|
+
Reschedule with the only scheduling tools.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
known_condition (dict): Patient conditions known to the staff.
|
|
219
|
+
doctor_information (Optional[dict], optional): A dictionary containing information about the doctor(s) involved,
|
|
220
|
+
including availability and other relevant details. Defaults to None.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
dict: Rescheduled schedule.
|
|
224
|
+
"""
|
|
225
|
+
# Sanity check
|
|
226
|
+
if not self.fhir_integration:
|
|
227
|
+
assert doctor_information is not None, log(f"Doctor information must be provided if you don't use FHIR.", level="error")
|
|
228
|
+
|
|
229
|
+
filtered_doctor_information = self.environment.get_doctor_schedule(
|
|
230
|
+
doctor_information=doctor_information if not self.fhir_integration else None,
|
|
231
|
+
department=known_condition['department'],
|
|
232
|
+
fhir_integration=self.fhir_integration,
|
|
233
|
+
)
|
|
234
|
+
_schedule_client = self.admin_staff_agent.build_agent(
|
|
235
|
+
rule=self.rules,
|
|
236
|
+
doctor_info=filtered_doctor_information,
|
|
237
|
+
only_schedule_tool=True
|
|
238
|
+
)
|
|
239
|
+
new_schedule = self.scheduling(
|
|
240
|
+
client=_schedule_client,
|
|
241
|
+
known_condition=known_condition,
|
|
242
|
+
doctor_information=doctor_information,
|
|
243
|
+
reschedule_flag=True,
|
|
244
|
+
**kwargs
|
|
245
|
+
)['result']
|
|
246
|
+
return new_schedule
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _check_reschedule_validity(self,
|
|
250
|
+
idx: int,
|
|
251
|
+
new_schedule: dict,
|
|
252
|
+
original_schedule: dict,
|
|
253
|
+
doctor_information: dict) -> Optional[dict]:
|
|
254
|
+
"""
|
|
255
|
+
Check the rescheduling availability.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
idx (int): Index of the requested schedule (original schedule index).
|
|
259
|
+
new_schedule (dict): New earliest schedule available.
|
|
260
|
+
original_schedule (dict): The original schedule.
|
|
261
|
+
doctor_information (Optional[dict], optional): A dictionary containing information about the doctor(s) involved,
|
|
262
|
+
including availability and other relevant details.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Optional[dict]: New schedule if the rescheduling available; otherwise None.
|
|
266
|
+
"""
|
|
267
|
+
pred_doctor_name = list(new_schedule['schedule'].keys())[0]
|
|
268
|
+
old_iso_time = get_iso_time(original_schedule['schedule'][0], original_schedule['date'])
|
|
269
|
+
new_iso_time = get_iso_time(new_schedule['schedule'][pred_doctor_name]['start'], new_schedule['schedule'][pred_doctor_name]['date'])
|
|
270
|
+
if compare_iso_time(old_iso_time, new_iso_time):
|
|
271
|
+
self.rules.cancel_schedule(idx, doctor_information, original_schedule)
|
|
272
|
+
final_schedule = {
|
|
273
|
+
'patient': original_schedule['patient'],
|
|
274
|
+
'attending_physician': pred_doctor_name,
|
|
275
|
+
'department': original_schedule['department'],
|
|
276
|
+
'date': new_schedule['schedule'][pred_doctor_name]['date'],
|
|
277
|
+
'schedule': [
|
|
278
|
+
new_schedule['schedule'][pred_doctor_name]['start'],
|
|
279
|
+
new_schedule['schedule'][pred_doctor_name]['end']
|
|
280
|
+
],
|
|
281
|
+
'patient_intention': original_schedule['patient_intention'],
|
|
282
|
+
'preference': original_schedule.get('preference'),
|
|
283
|
+
'preferred_doctor': original_schedule.get('preferred_doctor'),
|
|
284
|
+
'valid_from': original_schedule.get('valid_from'),
|
|
285
|
+
'last_updated_time': self.environment.current_time
|
|
286
|
+
}
|
|
287
|
+
return final_schedule
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def automatic_waiting_list_update(self,
|
|
292
|
+
doctor_information: dict,
|
|
293
|
+
**kwargs):
|
|
294
|
+
"""
|
|
295
|
+
Update waiting list availability automatically.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
doctor_information (Optional[dict], optional): A dictionary containing information about the doctor(s) involved,
|
|
299
|
+
including availability and other relevant details.
|
|
300
|
+
|
|
301
|
+
Yields:
|
|
302
|
+
dict: Updated (or not updated) doctor information and a result dictionary.
|
|
303
|
+
"""
|
|
304
|
+
for turn, (idx, original) in enumerate(self.environment.waiting_list):
|
|
305
|
+
if original['status'] == 'scheduled':
|
|
306
|
+
new_schedule = self._get_rescheduled_result(
|
|
307
|
+
known_condition=original,
|
|
308
|
+
doctor_information=doctor_information,
|
|
309
|
+
**kwargs
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Sanity check
|
|
313
|
+
## No GT case
|
|
314
|
+
if self.sanity_checker is None:
|
|
315
|
+
status, status_code = True, STATUS_CODES['correct']
|
|
316
|
+
else:
|
|
317
|
+
status, status_code = self.sanity_checker.schedule_check(
|
|
318
|
+
prediction=new_schedule,
|
|
319
|
+
gt_patient_condition=original,
|
|
320
|
+
doctor_information=doctor_information,
|
|
321
|
+
environment=self.environment
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
if status:
|
|
325
|
+
try:
|
|
326
|
+
final_schedule = self._check_reschedule_validity(
|
|
327
|
+
idx=idx,
|
|
328
|
+
new_schedule=new_schedule,
|
|
329
|
+
original_schedule=original,
|
|
330
|
+
doctor_information=doctor_information,
|
|
331
|
+
)
|
|
332
|
+
if final_schedule is not None:
|
|
333
|
+
result_dict = {
|
|
334
|
+
'gt': ['automatic rescheduling'],
|
|
335
|
+
'pred': [final_schedule],
|
|
336
|
+
'status': [True],
|
|
337
|
+
'status_code': [STATUS_CODES['correct']],
|
|
338
|
+
'dialog': ['automatic waiting list update from the system']
|
|
339
|
+
}
|
|
340
|
+
yield {'doctor_information': doctor_information, 'result_dict': result_dict, 'original': original}
|
|
341
|
+
|
|
342
|
+
except:
|
|
343
|
+
log('No sanity checker is available; an error occurred while parsing the prediction. Returning a failure result.', level='warning')
|
|
344
|
+
result_dict = {
|
|
345
|
+
'gt': ['automatic rescheduling'],
|
|
346
|
+
'pred': [new_schedule],
|
|
347
|
+
'status': [False],
|
|
348
|
+
'status_code': [STATUS_CODES['reschedule']['schedule'].format(status_code=STATUS_CODES['format'])],
|
|
349
|
+
'dialog': ['automatic waiting list update from the system']
|
|
350
|
+
}
|
|
351
|
+
yield {'doctor_information': doctor_information, 'result_dict': result_dict, 'original': original}
|
|
352
|
+
|
|
353
|
+
else:
|
|
354
|
+
result_dict = {
|
|
355
|
+
'gt': ['automatic rescheduling'],
|
|
356
|
+
'pred': [new_schedule],
|
|
357
|
+
'status': [status],
|
|
358
|
+
'status_code': [STATUS_CODES['reschedule']['schedule'].format(status_code=status_code)],
|
|
359
|
+
'dialog': ['automatic waiting list update from the system']
|
|
360
|
+
}
|
|
361
|
+
yield {'doctor_information': doctor_information, 'result_dict': result_dict, 'original': original}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def scheduling(self,
|
|
365
|
+
client: AgentExecutor,
|
|
366
|
+
known_condition: dict,
|
|
367
|
+
doctor_information: Optional[dict] = None,
|
|
368
|
+
reschedule_flag: bool = False,
|
|
369
|
+
chat_history: list = [],
|
|
370
|
+
**kwargs) -> dict:
|
|
371
|
+
"""
|
|
372
|
+
Make an appointment between the doctor and the patient.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
client (AgentExecutor): The agent executor to handle tool calls or conversation.
|
|
376
|
+
known_condition (dict): Patient conditions known to the staff.
|
|
377
|
+
doctor_information (Optional[dict], optional): A dictionary containing information about the doctor(s) involved,
|
|
378
|
+
including availability and other relevant details. Defaults to None.
|
|
379
|
+
reschedule_flag (bool, optional): Whether this process is rescheduling or not. Defaults to False.
|
|
380
|
+
chat_history (list, optional): Chat history. Defaults to [].
|
|
381
|
+
|
|
382
|
+
Raises:
|
|
383
|
+
ToolCallingError: If the agent fails to select or execute a valid scheduling tool.
|
|
384
|
+
TypeError: If the prediction or inputs are of an unsupported type.
|
|
385
|
+
|
|
386
|
+
Return
|
|
387
|
+
dict: Scheduling processed result.
|
|
388
|
+
"""
|
|
389
|
+
# Sanity Check
|
|
390
|
+
if not self.fhir_integration:
|
|
391
|
+
assert doctor_information is not None, log(f"Doctor information must be provided if you don't use FHIR.", level="error")
|
|
392
|
+
|
|
393
|
+
# Initialization based on the known condition from the staff
|
|
394
|
+
department = known_condition['department']
|
|
395
|
+
filtered_doctor_information = self.environment.get_doctor_schedule(
|
|
396
|
+
doctor_information=doctor_information if not self.fhir_integration else None,
|
|
397
|
+
department=department,
|
|
398
|
+
fhir_integration=self.fhir_integration,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# First, try to use the tool calling
|
|
402
|
+
try:
|
|
403
|
+
assert self.scheduling_strategy == 'tool_calling', log('Scheduling strategy is set to `reasoning`, directly use the reasoning method.', level='warning')
|
|
404
|
+
|
|
405
|
+
# Invoke
|
|
406
|
+
prediction = scheduling_tool_calling(
|
|
407
|
+
client=client,
|
|
408
|
+
user_prompt=known_condition['patient_intention'],
|
|
409
|
+
history=chat_history
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Post-processing
|
|
413
|
+
## Scheduling result
|
|
414
|
+
if prediction['type'] == 'tool':
|
|
415
|
+
schedule = self.postprocessing(
|
|
416
|
+
strategy='tool_calling',
|
|
417
|
+
data=prediction['result'],
|
|
418
|
+
filtered_doctor_information=filtered_doctor_information,
|
|
419
|
+
)
|
|
420
|
+
prediction['result'] = schedule
|
|
421
|
+
|
|
422
|
+
## Dialogue
|
|
423
|
+
elif prediction['type'] == 'text':
|
|
424
|
+
if 'no tool' in prediction['result'].lower():
|
|
425
|
+
raise ToolCallingError(colorstr('red', 'Failed to choose an appropriate scheduling tool.'))
|
|
426
|
+
|
|
427
|
+
## Error
|
|
428
|
+
else:
|
|
429
|
+
raise TypeError(colorstr("red", "Error: Unexpected return type from scheduling method."))
|
|
430
|
+
|
|
431
|
+
# If tool calling fails, fallback to LLM-based scheduling
|
|
432
|
+
except:
|
|
433
|
+
if self.scheduling_strategy == 'tool_calling':
|
|
434
|
+
log('Failed to select an appropriate tool. Falling back to reasoning-based scheduling.', level='warning')
|
|
435
|
+
|
|
436
|
+
reschedule_desc = "Rescheduling requested. This is the rescheduling of a patient who wishes to move their appointment earlier due to a previous patient's cancelled reservation" \
|
|
437
|
+
if reschedule_flag else 'Not requested.'
|
|
438
|
+
filtered_doctor_information = self.environment.get_doctor_schedule(
|
|
439
|
+
doctor_information=doctor_information if not self.fhir_integration else None,
|
|
440
|
+
department=department,
|
|
441
|
+
fhir_integration=self.fhir_integration,
|
|
442
|
+
express_detail=True
|
|
443
|
+
)
|
|
444
|
+
user_prompt = self.admin_staff_agent.scheduling_user_prompt_template.format(
|
|
445
|
+
START_HOUR=self._START_HOUR,
|
|
446
|
+
END_HOUR=self._END_HOUR,
|
|
447
|
+
TIME_UNIT=self._TIME_UNIT,
|
|
448
|
+
CURRENT_TIME=self.environment.current_time,
|
|
449
|
+
DEPARTMENT=department,
|
|
450
|
+
PREFERENCE=known_condition['patient_intention'], #if reschedule_flag else preprocess_dialog(self.dialog_history['scheduling']),
|
|
451
|
+
RESCHEDULING_FLAG=reschedule_desc,
|
|
452
|
+
DAY=self._DAY,
|
|
453
|
+
DOCTOR=json.dumps(filtered_doctor_information, indent=2),
|
|
454
|
+
)
|
|
455
|
+
schedule = self.admin_staff_agent(
|
|
456
|
+
user_prompt,
|
|
457
|
+
using_multi_turn=False,
|
|
458
|
+
verbose=False,
|
|
459
|
+
**kwargs,
|
|
460
|
+
)
|
|
461
|
+
schedule = self.postprocessing(
|
|
462
|
+
strategy='reasoning',
|
|
463
|
+
data=schedule,
|
|
464
|
+
)
|
|
465
|
+
prediction = {
|
|
466
|
+
'type': 'tool',
|
|
467
|
+
'result': schedule,
|
|
468
|
+
'raw': None
|
|
469
|
+
}
|
|
470
|
+
self.admin_staff_agent.reset_history(verbose=False)
|
|
471
|
+
|
|
472
|
+
return prediction
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def canceling(self,
|
|
476
|
+
client: AgentExecutor,
|
|
477
|
+
patient_intention: str,
|
|
478
|
+
chat_history: list = []) -> dict:
|
|
479
|
+
"""
|
|
480
|
+
Handle a multi-turn appointment cancellation request using a tool-calling agent.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
client (AgentExecutor): The agent executor to handle tool calls or conversation.
|
|
484
|
+
patient_intention (str): The patient's utterance expressing a rescheduling request.
|
|
485
|
+
chat_history (list, optional): Chat history. Defaults to [].
|
|
486
|
+
|
|
487
|
+
Raises:
|
|
488
|
+
TypeError: If the prediction or inputs are of an unsupported type.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
dict: Cancelling processed result.
|
|
492
|
+
"""
|
|
493
|
+
# Invoke
|
|
494
|
+
prediction = scheduling_tool_calling(
|
|
495
|
+
client=client,
|
|
496
|
+
user_prompt=patient_intention,
|
|
497
|
+
history=chat_history,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# Canceling result
|
|
501
|
+
if prediction['type'] == 'tool':
|
|
502
|
+
# Schedule not found case: -> return: str
|
|
503
|
+
if prediction['result']['result_dict']['pred'][0]['cancel'] == -1:
|
|
504
|
+
prediction['type'] = 'text'
|
|
505
|
+
prediction['result'] = "Sorry, we couldn't find a matching appointment. Could you please check your appointment details again?"
|
|
506
|
+
return prediction
|
|
507
|
+
|
|
508
|
+
# Successful cancellation case -> return: dict
|
|
509
|
+
else:
|
|
510
|
+
return prediction
|
|
511
|
+
|
|
512
|
+
# Clarification message case -> return: str
|
|
513
|
+
elif prediction['type'] == 'text':
|
|
514
|
+
return prediction
|
|
515
|
+
|
|
516
|
+
# Error
|
|
517
|
+
else:
|
|
518
|
+
raise TypeError(colorstr("red", "Error: Unexpected return type from canceling method."))
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def rescheduling(self,
|
|
522
|
+
client: AgentExecutor,
|
|
523
|
+
patient_intention: str,
|
|
524
|
+
doctor_information: Optional[dict] = None,
|
|
525
|
+
chat_history: list = [],
|
|
526
|
+
**kwargs) -> dict:
|
|
527
|
+
"""
|
|
528
|
+
Handle a multi-turn appointment rescheduling request using a tool-calling agent.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
client (AgentExecutor): The agent executor to handle tool calls or conversation.
|
|
532
|
+
patient_intention (str): The patient's utterance expressing a rescheduling request.
|
|
533
|
+
doctor_information (Optional[dict], optional): A dictionary containing information about the doctor(s) involved,
|
|
534
|
+
including availability and other relevant details. Defaults to None.
|
|
535
|
+
chat_history (list, optional): Chat history. Defaults to [].
|
|
536
|
+
|
|
537
|
+
Raises:
|
|
538
|
+
TypeError: If the returned type is not supported.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
dict: Rescheduling processed result.
|
|
542
|
+
"""
|
|
543
|
+
# Sanity check
|
|
544
|
+
if not self.fhir_integration:
|
|
545
|
+
assert doctor_information is not None, log(f"Doctor information must be provided if you don't use FHIR.", level="error")
|
|
546
|
+
|
|
547
|
+
# Invoke
|
|
548
|
+
prediction = scheduling_tool_calling(
|
|
549
|
+
client=client,
|
|
550
|
+
user_prompt=patient_intention,
|
|
551
|
+
history=chat_history,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Rescheduling result
|
|
555
|
+
if prediction['type'] == 'tool':
|
|
556
|
+
# Schedule not found case: -> return: str
|
|
557
|
+
if prediction['result']['result_dict']['pred'][0]['reschedule'] == -1:
|
|
558
|
+
prediction['type'] = 'text'
|
|
559
|
+
prediction['result'] = "Sorry, we couldn't find a matching appointment. Could you please check your appointment details again?"
|
|
560
|
+
return prediction
|
|
561
|
+
|
|
562
|
+
# Successfully retrieve the original schedule -> return: dict
|
|
563
|
+
else:
|
|
564
|
+
# Step 1: Retrieved original schedule
|
|
565
|
+
original_schedule = prediction['result']['original_schedule']
|
|
566
|
+
if prediction['result']['result_dict']['status'][0] == False: # Case of failure to identify the schedule
|
|
567
|
+
prediction['tmp_flag'] = 'retrieve'
|
|
568
|
+
return prediction
|
|
569
|
+
|
|
570
|
+
# Step 2: Try to reschedule based on the patient record (or the memo)
|
|
571
|
+
new_schedule = self._get_rescheduled_result(
|
|
572
|
+
known_condition=original_schedule,
|
|
573
|
+
doctor_information=doctor_information,
|
|
574
|
+
**kwargs
|
|
575
|
+
)
|
|
576
|
+
prediction['result']['new_schedule'] = new_schedule
|
|
577
|
+
|
|
578
|
+
# Sanity check
|
|
579
|
+
## No GT case
|
|
580
|
+
if self.sanity_checker is not None:
|
|
581
|
+
status, status_code = self.sanity_checker.schedule_check(
|
|
582
|
+
prediction=new_schedule,
|
|
583
|
+
gt_patient_condition=original_schedule,
|
|
584
|
+
doctor_information=doctor_information,
|
|
585
|
+
environment=self.environment
|
|
586
|
+
)
|
|
587
|
+
if not status:
|
|
588
|
+
prediction['result']['result_dict']['pred'] = [new_schedule]
|
|
589
|
+
prediction['result']['result_dict']['status'] = [False]
|
|
590
|
+
prediction['result']['result_dict']['status_code'] = [STATUS_CODES['reschedule']['schedule'].format(status_code=status_code)]
|
|
591
|
+
prediction['tmp_flag'] = 'schedule'
|
|
592
|
+
return prediction
|
|
593
|
+
|
|
594
|
+
# Step 3: Check the validity of the rescheduled result
|
|
595
|
+
try:
|
|
596
|
+
# Successful case
|
|
597
|
+
pred_idx = prediction['result']['result_dict']['pred'][0]['reschedule']
|
|
598
|
+
final_schedule = self._check_reschedule_validity(
|
|
599
|
+
idx=pred_idx,
|
|
600
|
+
new_schedule=new_schedule,
|
|
601
|
+
original_schedule=original_schedule,
|
|
602
|
+
doctor_information=doctor_information,
|
|
603
|
+
)
|
|
604
|
+
if final_schedule is not None:
|
|
605
|
+
prediction['result']['new_schedule'] = final_schedule
|
|
606
|
+
prediction['result']['result_dict']['pred'] = [final_schedule]
|
|
607
|
+
prediction['tmp_flag'] = 'reschedule'
|
|
608
|
+
else:
|
|
609
|
+
self.environment.add_waiting_list(pred_idx, True)
|
|
610
|
+
prediction['tmp_flag'] = 'waiting_list'
|
|
611
|
+
|
|
612
|
+
except:
|
|
613
|
+
log('No sanity checker is available; an error occurred while parsing the prediction. Returning a failure result.', level='warning')
|
|
614
|
+
prediction['result']['result_dict']['pred'] = [new_schedule]
|
|
615
|
+
prediction['result']['result_dict']['status'] = [False]
|
|
616
|
+
prediction['result']['result_dict']['status_code'] = [STATUS_CODES['reschedule']['schedule'].format(status_code=STATUS_CODES['format'])]
|
|
617
|
+
prediction['tmp_flag'] = 'schedule'
|
|
618
|
+
return prediction
|
|
619
|
+
|
|
620
|
+
return prediction
|
|
621
|
+
|
|
622
|
+
# Clarification message case -> return: str
|
|
623
|
+
elif prediction['type'] == 'text':
|
|
624
|
+
return prediction
|
|
625
|
+
|
|
626
|
+
# Error
|
|
627
|
+
else:
|
|
628
|
+
raise TypeError(colorstr("red", "Error: Unexpected return type from rescheduling method."))
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def scheduling_simulate(self,
|
|
632
|
+
gt_data: dict,
|
|
633
|
+
staff_known_data: dict,
|
|
634
|
+
doctor_information: Optional[dict] = None,
|
|
635
|
+
verbose: bool = False,
|
|
636
|
+
max_inferences: int = 5,
|
|
637
|
+
patient_kwargs: dict = {},
|
|
638
|
+
staff_kwargs: dict = {},
|
|
639
|
+
**kwargs) -> Tuple[dict, dict]:
|
|
640
|
+
"""
|
|
641
|
+
Simulate a multi-turn outpatient scheduling dialogue between a patient agent and an administrative staff agent.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
gt_data (dict): Ground-truth patient condition(s) for each dialogue turn.
|
|
645
|
+
staff_known_data (dict): Patient information known to the staff agent at each turn.
|
|
646
|
+
doctor_information (Optional[dict], optional): A dictionary containing information about the doctor(s) involved,
|
|
647
|
+
including availability and other relevant details. Defaults to None.
|
|
648
|
+
verbose (bool, optional): Whether to log detailed simulation outputs. Defaults to False.
|
|
649
|
+
max_inferences (int, optional): Maximum number of dialogue turns.
|
|
650
|
+
patient_kwargs (dict, optional): Additional keyword arguments passed to the patient agent.
|
|
651
|
+
staff_kwargs (dict, optional): Additional keyword arguments passed to the staff scheduling function.
|
|
652
|
+
**kwargs: Shared keyword arguments passed to both agents.
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
Tuple[dict, dict]: Doctor information and a result dictionary after scheduling a new appointment.
|
|
656
|
+
"""
|
|
657
|
+
# Sanity Check
|
|
658
|
+
if not self.fhir_integration:
|
|
659
|
+
assert doctor_information is not None, log(f"Doctor information must be provided if you don't use FHIR.", level="error")
|
|
660
|
+
|
|
661
|
+
# Initialize agents and result dictionary
|
|
662
|
+
self._init_agents(verbose=verbose)
|
|
663
|
+
filtered_doctor_information = self.environment.get_doctor_schedule(
|
|
664
|
+
doctor_information=doctor_information if not self.fhir_integration else None,
|
|
665
|
+
department=staff_known_data[0]['department'],
|
|
666
|
+
fhir_integration=self.fhir_integration,
|
|
667
|
+
)
|
|
668
|
+
client = self.admin_staff_agent.build_agent(
|
|
669
|
+
rule=self.rules,
|
|
670
|
+
doctor_info=filtered_doctor_information
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
# Sanity check for the simulation
|
|
674
|
+
assert len(gt_data) == len(staff_known_data), \
|
|
675
|
+
log(f"The lengths of gt_data and staff_known_data must be the same, but got gt_data length: {len(gt_data)} and staff_known_data length: {len(staff_known_data)}", level="error")
|
|
676
|
+
|
|
677
|
+
# Start conversation
|
|
678
|
+
staff_greet = self.admin_staff_agent.staff_greet
|
|
679
|
+
self.dialog_history['scheduling'].append({"role": "Staff", "content": staff_greet})
|
|
680
|
+
role = f"{colorstr('blue', 'Staff')}"
|
|
681
|
+
log(f"{role:<25}: {staff_greet}")
|
|
682
|
+
|
|
683
|
+
# Iterate over multiple preferences if exists
|
|
684
|
+
preference_reject_prob = 0.0 if len(gt_data) <= 1 else self.preference_rejection_prob
|
|
685
|
+
for i, (gt_patient_condition, staff_known_condition) in enumerate(zip(gt_data, staff_known_data)):
|
|
686
|
+
# For the rejection scenario
|
|
687
|
+
if i != 0:
|
|
688
|
+
self.update_patient_preference_system_prompt(
|
|
689
|
+
patient_condition=gt_patient_condition,
|
|
690
|
+
rejected_preference=gt_data[i-1]['preference']
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
tries = 0
|
|
694
|
+
while 1:
|
|
695
|
+
# Obtain response from patient
|
|
696
|
+
patient_kwargs.update(kwargs)
|
|
697
|
+
patient_response = self.patient_agent(
|
|
698
|
+
self.dialog_history['scheduling'][-1]["content"],
|
|
699
|
+
using_multi_turn=True,
|
|
700
|
+
verbose=False,
|
|
701
|
+
**patient_kwargs,
|
|
702
|
+
)
|
|
703
|
+
self.dialog_history['scheduling'].append({"role": "Patient", "content": patient_response})
|
|
704
|
+
role = f"{colorstr('green', 'Patient')} ({gt_patient_condition['preference']})"
|
|
705
|
+
log(f"{role:<25}: {patient_response}")
|
|
706
|
+
|
|
707
|
+
# Scheduling from staff
|
|
708
|
+
staff_kwargs.update(kwargs)
|
|
709
|
+
staff_known_condition.update({'patient_intention': patient_response})
|
|
710
|
+
staff_response = self.scheduling(
|
|
711
|
+
client,
|
|
712
|
+
staff_known_condition,
|
|
713
|
+
doctor_information,
|
|
714
|
+
chat_history=self._to_lc_history('scheduling'),
|
|
715
|
+
**staff_kwargs
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
# Clarification message
|
|
719
|
+
if staff_response['type'] == 'text':
|
|
720
|
+
response = staff_response['result']
|
|
721
|
+
self.dialog_history['scheduling'].append({"role": "Staff", "content": response})
|
|
722
|
+
role = f"{colorstr('blue', 'Staff')}"
|
|
723
|
+
log(f"{role:<25}: {response}")
|
|
724
|
+
|
|
725
|
+
# Tool calling result
|
|
726
|
+
elif staff_response['type'] == 'tool':
|
|
727
|
+
pred_schedule = staff_response['result']
|
|
728
|
+
response = self.admin_staff_agent.staff_suggestion.format(schedule=pred_schedule)
|
|
729
|
+
self.dialog_history['scheduling'].append({"role": "Staff", "content": response})
|
|
730
|
+
role = f"{colorstr('blue', 'Staff')}"
|
|
731
|
+
log(f"{role:<25}: {response}")
|
|
732
|
+
break
|
|
733
|
+
|
|
734
|
+
tries += 1
|
|
735
|
+
if tries > max_inferences:
|
|
736
|
+
result_dict = {
|
|
737
|
+
'gt': [gt_patient_condition],
|
|
738
|
+
'pred': [None],
|
|
739
|
+
'status': [False],
|
|
740
|
+
'status_code': [STATUS_CODES['simulation']],
|
|
741
|
+
'dialog': [preprocess_dialog(self.dialog_history['scheduling'])]
|
|
742
|
+
}
|
|
743
|
+
return doctor_information, result_dict
|
|
744
|
+
|
|
745
|
+
# Sanity check
|
|
746
|
+
## No GT case
|
|
747
|
+
if self.sanity_checker is None:
|
|
748
|
+
status, status_code = True, STATUS_CODES['correct']
|
|
749
|
+
## GT existing case
|
|
750
|
+
else:
|
|
751
|
+
status, status_code = self.sanity_checker.schedule_check(
|
|
752
|
+
prediction=pred_schedule,
|
|
753
|
+
gt_patient_condition=gt_patient_condition,
|
|
754
|
+
doctor_information=doctor_information,
|
|
755
|
+
environment=self.environment
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
if not status:
|
|
759
|
+
break
|
|
760
|
+
|
|
761
|
+
# Preference rejection logic
|
|
762
|
+
## Rejection case
|
|
763
|
+
if random.random() < preference_reject_prob and i != len(gt_data) - 1:
|
|
764
|
+
preference_reject_prob *= self.preference_rejection_prob_decay
|
|
765
|
+
## Non-rejection case
|
|
766
|
+
else:
|
|
767
|
+
self.dialog_history['scheduling'].append({"role": "Patient", "content": self.end_phrase})
|
|
768
|
+
role = f"{colorstr('green', 'Patient')} ({gt_data[i]['preference']})"
|
|
769
|
+
log(f"{role:<25}: {self.end_phrase}")
|
|
770
|
+
break
|
|
771
|
+
|
|
772
|
+
# Oranize the result
|
|
773
|
+
## Defaults to failure case dictionary
|
|
774
|
+
result_dict = {
|
|
775
|
+
'gt': [gt_patient_condition],
|
|
776
|
+
'pred': [pred_schedule],
|
|
777
|
+
'status': [False],
|
|
778
|
+
'status_code': [status_code],
|
|
779
|
+
'dialog': [preprocess_dialog(self.dialog_history['scheduling'])]
|
|
780
|
+
}
|
|
781
|
+
## Success case
|
|
782
|
+
if status:
|
|
783
|
+
try:
|
|
784
|
+
pred_doctor_name = list(pred_schedule['schedule'].keys())[0]
|
|
785
|
+
schedule = pred_schedule['schedule'][pred_doctor_name]
|
|
786
|
+
prediction = {
|
|
787
|
+
'patient': staff_known_data[i]['patient'],
|
|
788
|
+
'attending_physician': pred_doctor_name,
|
|
789
|
+
'department': staff_known_data[i]['department'],
|
|
790
|
+
'date': schedule['date'],
|
|
791
|
+
'schedule': [schedule['start'], schedule['end']],
|
|
792
|
+
'patient_intention': staff_known_data[i]['patient_intention'],
|
|
793
|
+
'preference': gt_data[i].get('preference'),
|
|
794
|
+
'preferred_doctor': gt_data[i].get('preferred_doctor'),
|
|
795
|
+
'valid_from': gt_data[i].get('valid_from'),
|
|
796
|
+
'last_updated_time': self.environment.current_time
|
|
797
|
+
}
|
|
798
|
+
result_dict['pred'] = [prediction]
|
|
799
|
+
result_dict['status'] = [True]
|
|
800
|
+
except:
|
|
801
|
+
result_dict['status_code'] = [STATUS_CODES['format']]
|
|
802
|
+
log('No sanity checker is available; an error occurred while parsing the prediction. Returning a failure result.', level='warning')
|
|
803
|
+
|
|
804
|
+
log("Simulation completed.", color=True)
|
|
805
|
+
return doctor_information, result_dict
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def canceling_simulate(self,
|
|
809
|
+
gt_idx: Optional[int] = None,
|
|
810
|
+
doctor_information: Optional[dict] = None,
|
|
811
|
+
patient_schedules: Optional[list[dict]] = None,
|
|
812
|
+
verbose: bool = True,
|
|
813
|
+
max_inferences: int = 5,
|
|
814
|
+
patient_kwargs: dict = {},
|
|
815
|
+
staff_kwargs: dict = {},
|
|
816
|
+
**kwargs) -> Tuple[dict, dict]:
|
|
817
|
+
"""
|
|
818
|
+
Simulate a multi-turn conversation for appointment cancellation.
|
|
819
|
+
|
|
820
|
+
Args:
|
|
821
|
+
gt_idx (Optional[int], optional): Ground-truth index of the appointment to be canceled. Defaults to None.
|
|
822
|
+
doctor_information (Optional[dict], optional): A dictionary containing information about the doctor(s).
|
|
823
|
+
patient_schedules (Optional[list[dict]], optional): List of patient appointment schedules. Defaults to None.
|
|
824
|
+
verbose (bool, optional): Whether to print conversation logs. Defaults to True.
|
|
825
|
+
max_inferences (int, optional): Maximum number of dialogue turns.
|
|
826
|
+
patient_kwargs (dict, optional): Additional keyword arguments passed to the patient agent.
|
|
827
|
+
staff_kwargs (dict, optional): Additional keyword arguments passed to the staff agent.
|
|
828
|
+
**kwargs: Additional keyword arguments passed to the patient and staff agent.
|
|
829
|
+
|
|
830
|
+
Raises:
|
|
831
|
+
ScheduleNotFoundError: Schedule not found error.
|
|
832
|
+
|
|
833
|
+
Returns:
|
|
834
|
+
Tuple[dict, dict]: Updated doctor information and a result dictionary after cancellation.
|
|
835
|
+
"""
|
|
836
|
+
# Sanity Check
|
|
837
|
+
if not self.fhir_integration:
|
|
838
|
+
assert doctor_information is not None, log(f"Doctor information must be provided if you don't use FHIR.", level="error")
|
|
839
|
+
|
|
840
|
+
# Initialize agents and result dictionary
|
|
841
|
+
result_dict = init_result_dict()
|
|
842
|
+
self._init_agents(verbose=verbose)
|
|
843
|
+
patient_schedules = self.environment.patient_schedules if patient_schedules is None else patient_schedules
|
|
844
|
+
doctor_information = self.environment.get_general_doctor_info_from_fhir() if self.fhir_integration else doctor_information
|
|
845
|
+
client = self.admin_staff_agent.build_agent(
|
|
846
|
+
rule=self.rules,
|
|
847
|
+
doctor_info=doctor_information,
|
|
848
|
+
patient_schedule_list=patient_schedules,
|
|
849
|
+
gt_idx=gt_idx,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
# Start conversation
|
|
853
|
+
staff_greet = self.admin_staff_agent.general_staff_greet
|
|
854
|
+
self.dialog_history['cancel'].append({"role": "Staff", "content": staff_greet})
|
|
855
|
+
role = f"{colorstr('blue', 'Staff')}"
|
|
856
|
+
log(f"{role:<25}: {staff_greet}")
|
|
857
|
+
|
|
858
|
+
try:
|
|
859
|
+
for _ in range(max_inferences):
|
|
860
|
+
# Obtain response from patient
|
|
861
|
+
patient_kwargs.update(kwargs)
|
|
862
|
+
patient_response = self.patient_agent(
|
|
863
|
+
self.dialog_history['cancel'][-1]["content"],
|
|
864
|
+
using_multi_turn=True,
|
|
865
|
+
verbose=False,
|
|
866
|
+
**patient_kwargs
|
|
867
|
+
)
|
|
868
|
+
self.dialog_history['cancel'].append({"role": "Patient", "content": patient_response})
|
|
869
|
+
role = f"{colorstr('green', 'Patient')} (cancel)"
|
|
870
|
+
log(f"{role:<25}: {patient_response}")
|
|
871
|
+
|
|
872
|
+
# Canceling from staff
|
|
873
|
+
staff_response = self.canceling(
|
|
874
|
+
client=client,
|
|
875
|
+
patient_intention=patient_response,
|
|
876
|
+
chat_history=self._to_lc_history('cancel'),
|
|
877
|
+
)
|
|
878
|
+
# Clarification message instead of tool calling
|
|
879
|
+
if staff_response['type'] == 'text':
|
|
880
|
+
response = staff_response['result']
|
|
881
|
+
self.dialog_history['cancel'].append({"role": "Staff", "content": response})
|
|
882
|
+
role = f"{colorstr('blue', 'Staff')}"
|
|
883
|
+
log(f"{role:<25}: {response}")
|
|
884
|
+
continue
|
|
885
|
+
|
|
886
|
+
# Tool calling result
|
|
887
|
+
elif staff_response['type'] == 'tool':
|
|
888
|
+
result = staff_response['result']
|
|
889
|
+
result_dict = result['result_dict']
|
|
890
|
+
|
|
891
|
+
# Succesful case
|
|
892
|
+
if result_dict['status'][0] is not False: # No GT and correct case
|
|
893
|
+
cancelled_schedule = {k: v for k, v in result['cancelled_schedule'].items() \
|
|
894
|
+
if k in ['patient', 'attending_physician', 'department', 'date', 'schedule']}
|
|
895
|
+
|
|
896
|
+
# Final response of staff
|
|
897
|
+
response = f"I've cancelled this schedule: {cancelled_schedule}"
|
|
898
|
+
self.dialog_history['cancel'].append({"role": "Staff", "content": response})
|
|
899
|
+
role = f"{colorstr('blue', 'Staff')}"
|
|
900
|
+
log(f"{role:<25}: {response}")
|
|
901
|
+
|
|
902
|
+
# Final response of patient
|
|
903
|
+
self.dialog_history['cancel'].append({"role": "Patient", "content": self.end_phrase})
|
|
904
|
+
role = f"{colorstr('green', 'Patient')} (cancel)"
|
|
905
|
+
log(f"{role:<25}: {self.end_phrase}")
|
|
906
|
+
|
|
907
|
+
result_dict['dialog'].append(preprocess_dialog(self.dialog_history['cancel']))
|
|
908
|
+
break
|
|
909
|
+
|
|
910
|
+
# Fail to identify the schedule
|
|
911
|
+
else:
|
|
912
|
+
raise ScheduleNotFoundError(colorstr("red", "Error: Schedule not found error."))
|
|
913
|
+
|
|
914
|
+
# The case without any determination during the simulation
|
|
915
|
+
if not len(result_dict['gt']):
|
|
916
|
+
result_dict = {
|
|
917
|
+
'gt': [{'cancel': gt_idx}],
|
|
918
|
+
'pred': [None],
|
|
919
|
+
'status': [False],
|
|
920
|
+
'status_code': [STATUS_CODES['cancel']['identify']],
|
|
921
|
+
'dialog': [preprocess_dialog(self.dialog_history['cancel'])]
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
# Requested schedule indentification error
|
|
925
|
+
except ScheduleNotFoundError:
|
|
926
|
+
result_dict['dialog'].append(preprocess_dialog(self.dialog_history['cancel']))
|
|
927
|
+
|
|
928
|
+
# Tool calling error
|
|
929
|
+
except TypeError:
|
|
930
|
+
result_dict = {
|
|
931
|
+
'gt': [{'cancel': gt_idx}],
|
|
932
|
+
'pred': [None],
|
|
933
|
+
'status': [False],
|
|
934
|
+
'status_code': [STATUS_CODES['cancel']['type']],
|
|
935
|
+
'dialog': [preprocess_dialog(self.dialog_history['cancel'])]
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
# Otherwhise
|
|
939
|
+
except Exception as e:
|
|
940
|
+
status_code = STATUS_CODES['unexpected'].format(e=e)
|
|
941
|
+
log(status_code, level='warning')
|
|
942
|
+
result_dict = {
|
|
943
|
+
'gt': [{'cancel': gt_idx}],
|
|
944
|
+
'pred': [None],
|
|
945
|
+
'status': [False],
|
|
946
|
+
'status_code': [status_code],
|
|
947
|
+
'dialog': [preprocess_dialog(self.dialog_history['cancel'])]
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
log("Simulation completed.", color=True)
|
|
951
|
+
return doctor_information, result_dict
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def rescheduling_simulate(self,
|
|
955
|
+
gt_idx: Optional[int] = None,
|
|
956
|
+
doctor_information: Optional[dict] = None,
|
|
957
|
+
patient_schedules: Optional[list[dict]] = None,
|
|
958
|
+
verbose: bool = True,
|
|
959
|
+
max_inferences: int = 5,
|
|
960
|
+
patient_kwargs: dict = {},
|
|
961
|
+
staff_kwargs: dict = {},
|
|
962
|
+
**kwargs) -> Tuple[dict, dict]:
|
|
963
|
+
"""
|
|
964
|
+
Simulate a multi-turn conversation for appointment rescheduling.
|
|
965
|
+
|
|
966
|
+
Args:
|
|
967
|
+
gt_idx (Optional[int], optional): Ground-truth index of the appointment to be rescheduled. Defaults to None.
|
|
968
|
+
doctor_information (Optional[dict], optional): A dictionary containing information about the doctor(s).
|
|
969
|
+
patient_schedules (Optional[list[dict]], optional): List of patient appointment schedules. Defaults to None.
|
|
970
|
+
verbose (bool, optional): Whether to print conversation logs. Defaults to True.
|
|
971
|
+
max_inferences (int, optional): Maximum number of dialogue turns.
|
|
972
|
+
patient_kwargs (dict, optional): Additional keyword arguments passed to the patient agent.
|
|
973
|
+
staff_kwargs (dict, optional): Additional keyword arguments passed to the staff agent.
|
|
974
|
+
**kwargs: Additional keyword arguments passed to the patient and staff agents.
|
|
975
|
+
|
|
976
|
+
Raises:
|
|
977
|
+
TypeError: If the return type from the rescheduling method is unexpected.
|
|
978
|
+
ScheduleNotFoundError: Schedule not found error.
|
|
979
|
+
SchedulingError: Scheduling error.
|
|
980
|
+
|
|
981
|
+
Returns:
|
|
982
|
+
Tuple[dict, dict]: Updated doctor information and a result dictionary after cancellation.
|
|
983
|
+
"""
|
|
984
|
+
# Sanity Check
|
|
985
|
+
if not self.fhir_integration:
|
|
986
|
+
assert doctor_information is not None, log(f"Doctor information must be provided if you don't use FHIR.", level="error")
|
|
987
|
+
|
|
988
|
+
# Initialize agents and result dictionary
|
|
989
|
+
self._branch = False
|
|
990
|
+
result_dict = init_result_dict()
|
|
991
|
+
self._init_agents(verbose=verbose)
|
|
992
|
+
patient_schedules = self.environment.patient_schedules if patient_schedules is None else patient_schedules
|
|
993
|
+
doctor_information = self.environment.get_general_doctor_info_from_fhir() if self.fhir_integration else doctor_information
|
|
994
|
+
client = self.admin_staff_agent.build_agent(
|
|
995
|
+
rule=self.rules,
|
|
996
|
+
doctor_info=doctor_information,
|
|
997
|
+
patient_schedule_list=patient_schedules,
|
|
998
|
+
gt_idx=gt_idx,
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
# Start conversation
|
|
1002
|
+
staff_greet = self.admin_staff_agent.general_staff_greet
|
|
1003
|
+
self.dialog_history['reschedule'].append({"role": "Staff", "content": staff_greet})
|
|
1004
|
+
role = f"{colorstr('blue', 'Staff')}"
|
|
1005
|
+
log(f"{role:<25}: {staff_greet}")
|
|
1006
|
+
|
|
1007
|
+
try:
|
|
1008
|
+
for _ in range(max_inferences):
|
|
1009
|
+
# Obtain response from patient
|
|
1010
|
+
patient_kwargs.update(kwargs)
|
|
1011
|
+
patient_response = self.patient_agent(
|
|
1012
|
+
self.dialog_history['reschedule'][-1]["content"],
|
|
1013
|
+
using_multi_turn=True,
|
|
1014
|
+
verbose=False,
|
|
1015
|
+
**patient_kwargs
|
|
1016
|
+
)
|
|
1017
|
+
self.dialog_history['reschedule'].append({"role": "Patient", "content": patient_response})
|
|
1018
|
+
role = f"{colorstr('green', 'Patient')} (move)"
|
|
1019
|
+
log(f"{role:<25}: {patient_response}")
|
|
1020
|
+
|
|
1021
|
+
# Rescheduling from staff
|
|
1022
|
+
staff_kwargs.update(kwargs)
|
|
1023
|
+
staff_response = self.rescheduling(
|
|
1024
|
+
client=client,
|
|
1025
|
+
patient_intention=patient_response,
|
|
1026
|
+
doctor_information=doctor_information,
|
|
1027
|
+
chat_history=self._to_lc_history('reschedule'),
|
|
1028
|
+
**staff_kwargs
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
# Clarification message
|
|
1032
|
+
if staff_response['type'] == 'text':
|
|
1033
|
+
response = staff_response['result']
|
|
1034
|
+
self.dialog_history['reschedule'].append({"role": "Staff", "content": response})
|
|
1035
|
+
role = f"{colorstr('blue', 'Staff')}"
|
|
1036
|
+
log(f"{role:<25}: {response}")
|
|
1037
|
+
|
|
1038
|
+
# Tool calling result
|
|
1039
|
+
elif staff_response['type'] == 'tool':
|
|
1040
|
+
# Fail to identify the schedule
|
|
1041
|
+
if staff_response['tmp_flag'] == 'retrieve':
|
|
1042
|
+
result_dict = staff_response['result']['result_dict']
|
|
1043
|
+
raise ScheduleNotFoundError(colorstr("red", "Error: Schedule not found error."))
|
|
1044
|
+
|
|
1045
|
+
# Scheduling failure case
|
|
1046
|
+
elif staff_response['tmp_flag'] == 'schedule':
|
|
1047
|
+
result_dict = staff_response['result']['result_dict']
|
|
1048
|
+
raise SchedulingError(colorstr("red", "Error: Scheduling error."))
|
|
1049
|
+
|
|
1050
|
+
# Successful case
|
|
1051
|
+
else:
|
|
1052
|
+
result = staff_response['result']
|
|
1053
|
+
tmp_original_schedule = {k: v for k, v in result['original_schedule'].items() \
|
|
1054
|
+
if k in ['patient', 'attending_physician', 'department', 'date', 'schedule']}
|
|
1055
|
+
|
|
1056
|
+
# Successfully adding to waiting list
|
|
1057
|
+
if staff_response['tmp_flag'] == 'waiting_list':
|
|
1058
|
+
result_dict = result['result_dict']
|
|
1059
|
+
response = f"There are no available times. I've added this schedule to the waiting list: {tmp_original_schedule}"
|
|
1060
|
+
|
|
1061
|
+
# Successfully rescheduled case
|
|
1062
|
+
elif staff_response['tmp_flag'] == 'reschedule':
|
|
1063
|
+
result_dict = result['result_dict']
|
|
1064
|
+
tmp_prediction_schedule = {k: v for k, v in result['new_schedule'].items() \
|
|
1065
|
+
if k in ['patient', 'attending_physician', 'department', 'date', 'schedule']}
|
|
1066
|
+
response = f"I've moved your original schedule: {tmp_original_schedule} to the new one: {tmp_prediction_schedule}"
|
|
1067
|
+
|
|
1068
|
+
else:
|
|
1069
|
+
raise TypeError(colorstr("red", "Error: Unexpected return type from rescheduling method."))
|
|
1070
|
+
|
|
1071
|
+
# Final response of staff
|
|
1072
|
+
self.dialog_history['reschedule'].append({"role": "Staff", "content": response})
|
|
1073
|
+
role = f"{colorstr('blue', 'Staff')}"
|
|
1074
|
+
log(f"{role:<25}: {response}")
|
|
1075
|
+
|
|
1076
|
+
# Final response of patient
|
|
1077
|
+
self.dialog_history['reschedule'].append({"role": "Patient", "content": self.end_phrase})
|
|
1078
|
+
role = f"{colorstr('green', 'Patient')} (move)"
|
|
1079
|
+
log(f"{role:<25}: {self.end_phrase}")
|
|
1080
|
+
|
|
1081
|
+
result_dict['dialog'].append(preprocess_dialog(self.dialog_history['reschedule']))
|
|
1082
|
+
break
|
|
1083
|
+
|
|
1084
|
+
# The case without any determination during the simulation
|
|
1085
|
+
if not len(result_dict['gt']):
|
|
1086
|
+
result_dict = {
|
|
1087
|
+
'gt': [{'reschedule': gt_idx}],
|
|
1088
|
+
'pred': [None],
|
|
1089
|
+
'status': [False],
|
|
1090
|
+
'status_code': [STATUS_CODES['reschedule']['identify']],
|
|
1091
|
+
'dialog': [preprocess_dialog(self.dialog_history['reschedule'])]
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
# Requested schedule indentification error
|
|
1095
|
+
except ScheduleNotFoundError:
|
|
1096
|
+
result_dict['dialog'].append(preprocess_dialog(self.dialog_history['reschedule']))
|
|
1097
|
+
|
|
1098
|
+
# Scheduling Error:
|
|
1099
|
+
except SchedulingError:
|
|
1100
|
+
result_dict['dialog'].append(preprocess_dialog(self.dialog_history['reschedule']))
|
|
1101
|
+
|
|
1102
|
+
# Tool calling error
|
|
1103
|
+
except TypeError:
|
|
1104
|
+
result_dict = {
|
|
1105
|
+
'gt': [{'reschedule': gt_idx}],
|
|
1106
|
+
'pred': [None],
|
|
1107
|
+
'status': [False],
|
|
1108
|
+
'status_code': [STATUS_CODES['reschedule']['type']],
|
|
1109
|
+
'dialog': [preprocess_dialog(self.dialog_history['reschedule'])]
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
# Otherwise
|
|
1113
|
+
except Exception as e:
|
|
1114
|
+
status_code = STATUS_CODES['unexpected'].format(e=e)
|
|
1115
|
+
log(status_code, level='warning')
|
|
1116
|
+
result_dict = {
|
|
1117
|
+
'gt': [{'reschedule': gt_idx}],
|
|
1118
|
+
'pred': [None],
|
|
1119
|
+
'status': [False],
|
|
1120
|
+
'status_code': [status_code],
|
|
1121
|
+
'dialog': [preprocess_dialog(self.dialog_history['reschedule'])]
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
log("Simulation completed.", color=True)
|
|
1125
|
+
return doctor_information, result_dict
|
|
1126
|
+
|