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.
Files changed (62) hide show
  1. h_adminsim/__init__.py +5 -0
  2. h_adminsim/admin_staff.py +280 -0
  3. h_adminsim/assets/configs/data4primary.yaml +47 -0
  4. h_adminsim/assets/configs/data4secondary.yaml +47 -0
  5. h_adminsim/assets/configs/data4tertiary.yaml +47 -0
  6. h_adminsim/assets/country/address.json +141859 -0
  7. h_adminsim/assets/country/country_code.json +244 -0
  8. h_adminsim/assets/departments/department.json +85 -0
  9. h_adminsim/assets/departments/symptom.json +4530 -0
  10. h_adminsim/assets/fhir.schema.json +75253 -0
  11. h_adminsim/assets/names/firstname.txt +1219 -0
  12. h_adminsim/assets/names/lastname.txt +88799 -0
  13. h_adminsim/assets/prompts/cancel_patient_system.txt +38 -0
  14. h_adminsim/assets/prompts/intake_staff_task_user.txt +16 -0
  15. h_adminsim/assets/prompts/intake_supervisor_system.txt +8 -0
  16. h_adminsim/assets/prompts/intake_supervisor_user.txt +31 -0
  17. h_adminsim/assets/prompts/reschedule_patient_system.txt +38 -0
  18. h_adminsim/assets/prompts/schedule_patient_rejected_system.txt +42 -0
  19. h_adminsim/assets/prompts/schedule_patient_system.txt +36 -0
  20. h_adminsim/assets/prompts/schedule_staff_reasoning.txt +57 -0
  21. h_adminsim/assets/prompts/schedule_staff_sc_tool_calling.txt +13 -0
  22. h_adminsim/assets/prompts/schedule_staff_system.txt +10 -0
  23. h_adminsim/assets/prompts/schedule_staff_tool_calling.txt +41 -0
  24. h_adminsim/client/__init__.py +3 -0
  25. h_adminsim/client/google_client.py +209 -0
  26. h_adminsim/client/openai_client.py +199 -0
  27. h_adminsim/client/vllm_client.py +160 -0
  28. h_adminsim/environment/__init__.py +1 -0
  29. h_adminsim/environment/hospital.py +462 -0
  30. h_adminsim/environment/op_scheduling_simulation.py +1126 -0
  31. h_adminsim/pipeline/__init__.py +3 -0
  32. h_adminsim/pipeline/data_generator.py +192 -0
  33. h_adminsim/pipeline/evaluator.py +33 -0
  34. h_adminsim/pipeline/simulation.py +231 -0
  35. h_adminsim/registry/__init__.py +5 -0
  36. h_adminsim/registry/errors.py +89 -0
  37. h_adminsim/registry/models.py +126 -0
  38. h_adminsim/registry/phrases.py +10 -0
  39. h_adminsim/registry/pydantic_models.py +21 -0
  40. h_adminsim/registry/variables.py +9 -0
  41. h_adminsim/supervisor.py +182 -0
  42. h_adminsim/task/agent_task.py +900 -0
  43. h_adminsim/task/fhir_manager.py +222 -0
  44. h_adminsim/task/schedule_assign.py +151 -0
  45. h_adminsim/tools/__init__.py +5 -0
  46. h_adminsim/tools/agent_data_builder.py +124 -0
  47. h_adminsim/tools/data_converter.py +536 -0
  48. h_adminsim/tools/data_synthesizer.py +365 -0
  49. h_adminsim/tools/evaluator.py +258 -0
  50. h_adminsim/tools/sanity_checker.py +216 -0
  51. h_adminsim/tools/scheduling_rule.py +420 -0
  52. h_adminsim/utils/__init__.py +136 -0
  53. h_adminsim/utils/common_utils.py +698 -0
  54. h_adminsim/utils/fhir_utils.py +190 -0
  55. h_adminsim/utils/filesys_utils.py +135 -0
  56. h_adminsim/utils/image_preprocess_utils.py +188 -0
  57. h_adminsim/utils/random_utils.py +358 -0
  58. h_adminsim/version.txt +1 -0
  59. h_adminsim-1.0.0.dist-info/LICENSE +30 -0
  60. h_adminsim-1.0.0.dist-info/METADATA +494 -0
  61. h_adminsim-1.0.0.dist-info/RECORD +62 -0
  62. 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
+