rakib72642 commited on
Commit
91c3bff
·
1 Parent(s): d514191

enhance email normalization and add confirmation previews for appointment updates and deletions

Browse files
Files changed (2) hide show
  1. core/backend.py +88 -7
  2. services/tts.py +4 -4
core/backend.py CHANGED
@@ -10,6 +10,7 @@ import pytz
10
  from datetime import datetime, timedelta
11
  from dotenv import load_dotenv
12
  import re
 
13
 
14
  from langchain_core.messages import (
15
  AIMessage, AIMessageChunk, HumanMessage, RemoveMessage,
@@ -58,6 +59,23 @@ def _clean_text(text: str) -> str:
58
  return re.sub(r"\s+", " ", (text or "").strip())
59
 
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  _DIGIT_TRANSLATION = str.maketrans({
62
  "০": "0",
63
  "১": "1",
@@ -679,7 +697,7 @@ async def book_appointment(
679
  visiting_date = _clean_text(visiting_date)
680
  visiting_day = _clean_text(visiting_day)
681
  visiting_time = _clean_text(visiting_time)
682
- patient_mail = _clean_text(patient_mail)
683
 
684
  if visiting_date:
685
  parsed_date = _parse_visit_date(visiting_date)
@@ -790,6 +808,7 @@ async def book_appointment(
790
  )
791
  mail_status = "\n📧 Confirmation mail sent."
792
  except Exception as e:
 
793
  mail_status = f"\n⚠️ Mail failed: {str(e)}"
794
 
795
  return (
@@ -818,6 +837,7 @@ async def update_appointment(
818
  new_doctor_name: str = "",
819
  new_patient_num: str = "",
820
  new_patient_mail: str = "",
 
821
  ) -> str:
822
  """
823
  Update an existing appointment found by phone number.
@@ -834,13 +854,14 @@ async def update_appointment(
834
  (or doctor_id) to select which one to update.
835
  - A confirmation email is REQUIRED for updates: either the existing
836
  appointment has an email, or provide new_patient_mail.
 
837
  """
838
  db_path = get_db_path()
839
  patient_num_norm = format_bd_number(patient_num)
840
  selector_name = _clean_text(doctor_name)
841
  new_doctor_name = _clean_text(new_doctor_name)
842
  new_patient_num = format_bd_number(new_patient_num) if new_patient_num else ""
843
- new_patient_mail = _clean_text(new_patient_mail)
844
  new_visiting_date = _clean_text(new_visiting_date)
845
 
846
  parsed_date = _parse_visit_date(new_visiting_date) if new_visiting_date else None
@@ -890,7 +911,7 @@ async def update_appointment(
890
  else:
891
  return f"No doctor found with name '{new_doctor_name}'."
892
 
893
- # Build updated fields
894
  updated_doctor_name = appt.get("doctor_name", "")
895
  updated_doctor_category = appt.get("doctor_category", "")
896
  updated_visiting_time = appt.get("visiting_time", "")
@@ -923,6 +944,40 @@ async def update_appointment(
923
  if not updated_patient_mail:
924
  return "Email is required to update an appointment. Please provide an email address."
925
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
926
  await db.execute(
927
  """UPDATE patients
928
  SET doctor_name = ?,
@@ -966,6 +1021,7 @@ async def update_appointment(
966
  )
967
  mail_status = "📧 Confirmation mail sent."
968
  except Exception as e:
 
969
  mail_status = f"⚠️ Mail failed: {str(e)}"
970
 
971
  return (
@@ -989,15 +1045,17 @@ async def delete_appointment(
989
  doctor_name: str = "",
990
  doctor_id: int = 0,
991
  patient_mail: str = "",
 
992
  ) -> str:
993
  """
994
  Cancel (delete) an appointment.
995
  Sends a confirmation email to the patient (required).
 
996
  """
997
  db_path = get_db_path()
998
  patient_num = format_bd_number(patient_num)
999
  doctor_name = _clean_text(doctor_name)
1000
- patient_mail = _clean_text(patient_mail)
1001
 
1002
  async with aiosqlite.connect(db_path) as db:
1003
  db.row_factory = aiosqlite.Row
@@ -1048,7 +1106,29 @@ async def delete_appointment(
1048
  "message": "Email is required to cancel an appointment. Please provide the email address.",
1049
  }, ensure_ascii=False)
1050
 
1051
- # Delete after we have all details for mail
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1052
  await db.execute("DELETE FROM patients WHERE id = ?", (appt["id"],))
1053
  await db.commit()
1054
 
@@ -1071,6 +1151,7 @@ async def delete_appointment(
1071
  )
1072
  mail_status = "Confirmation mail sent."
1073
  except Exception as e:
 
1074
  mail_status = f"Mail failed: {str(e)}"
1075
 
1076
  return json.dumps({
@@ -1185,13 +1266,13 @@ Important update rules:
1185
  - Ask whether to keep the rest of the existing appointment unchanged.
1186
  - If multiple appointments exist for a phone number, ask for the doctor name to select the correct one.
1187
  - Email is REQUIRED to update. If the existing record has no email, ask for it.
1188
- - Only call `update_appointment` after the user confirms the change(s).
1189
 
1190
  Important cancellation rules:
1191
  - If the user gave only a phone number and there is exactly one matching appointment, cancel it directly.
1192
  - If multiple appointments match, ask only for the doctor name.
1193
  - Email is REQUIRED to cancel or update. If missing, ask for email.
1194
- - Only call `delete_appointment` after the user confirms cancellation.
1195
 
1196
  Do not give a normal conversational answer before the tool call.
1197
  """
 
10
  from datetime import datetime, timedelta
11
  from dotenv import load_dotenv
12
  import re
13
+ import traceback
14
 
15
  from langchain_core.messages import (
16
  AIMessage, AIMessageChunk, HumanMessage, RemoveMessage,
 
59
  return re.sub(r"\s+", " ", (text or "").strip())
60
 
61
 
62
+ def _normalize_email(text: str) -> str:
63
+ """
64
+ Best-effort normalizer for dictated emails like:
65
+ "rakib dot hedigital at gmail dot com"
66
+ Keeps it conservative: only applies if obvious patterns exist.
67
+ """
68
+ raw = _clean_text(text).lower()
69
+ if not raw:
70
+ return ""
71
+ # Common voice dictation patterns
72
+ raw = raw.replace(" at ", "@").replace(" dot ", ".")
73
+ raw = raw.replace(" underscore ", "_").replace(" dash ", "-")
74
+ raw = raw.replace(" minus ", "-").replace(" plus ", "+")
75
+ raw = raw.replace(" ", "")
76
+ return raw
77
+
78
+
79
  _DIGIT_TRANSLATION = str.maketrans({
80
  "০": "0",
81
  "১": "1",
 
697
  visiting_date = _clean_text(visiting_date)
698
  visiting_day = _clean_text(visiting_day)
699
  visiting_time = _clean_text(visiting_time)
700
+ patient_mail = _normalize_email(patient_mail)
701
 
702
  if visiting_date:
703
  parsed_date = _parse_visit_date(visiting_date)
 
808
  )
809
  mail_status = "\n📧 Confirmation mail sent."
810
  except Exception as e:
811
+ traceback.print_exc()
812
  mail_status = f"\n⚠️ Mail failed: {str(e)}"
813
 
814
  return (
 
837
  new_doctor_name: str = "",
838
  new_patient_num: str = "",
839
  new_patient_mail: str = "",
840
+ confirm: bool = False,
841
  ) -> str:
842
  """
843
  Update an existing appointment found by phone number.
 
854
  (or doctor_id) to select which one to update.
855
  - A confirmation email is REQUIRED for updates: either the existing
856
  appointment has an email, or provide new_patient_mail.
857
+ - IMPORTANT: This tool will NOT change the database unless confirm=True.
858
  """
859
  db_path = get_db_path()
860
  patient_num_norm = format_bd_number(patient_num)
861
  selector_name = _clean_text(doctor_name)
862
  new_doctor_name = _clean_text(new_doctor_name)
863
  new_patient_num = format_bd_number(new_patient_num) if new_patient_num else ""
864
+ new_patient_mail = _normalize_email(new_patient_mail)
865
  new_visiting_date = _clean_text(new_visiting_date)
866
 
867
  parsed_date = _parse_visit_date(new_visiting_date) if new_visiting_date else None
 
911
  else:
912
  return f"No doctor found with name '{new_doctor_name}'."
913
 
914
+ # Build updated fields (proposal)
915
  updated_doctor_name = appt.get("doctor_name", "")
916
  updated_doctor_category = appt.get("doctor_category", "")
917
  updated_visiting_time = appt.get("visiting_time", "")
 
944
  if not updated_patient_mail:
945
  return "Email is required to update an appointment. Please provide an email address."
946
 
947
+ # If not confirmed, return a preview only (no DB changes).
948
+ if not confirm:
949
+ preview = {
950
+ "success": False,
951
+ "needs_confirmation": True,
952
+ "message": (
953
+ "I found your appointment. Please confirm the changes. "
954
+ "If you want to keep the rest of the existing information, "
955
+ "say YES and confirm."
956
+ ),
957
+ "current": {
958
+ "doctor_name": appt.get("doctor_name", ""),
959
+ "doctor_category": appt.get("doctor_category", ""),
960
+ "patient_name": appt.get("patient_name", ""),
961
+ "patient_num": appt.get("patient_num", ""),
962
+ "patient_mail": appt.get("patient_mail", ""),
963
+ "visiting_date": appt.get("visiting_date", ""),
964
+ "visiting_day": appt.get("visiting_day", ""),
965
+ "visiting_time": appt.get("visiting_time", ""),
966
+ },
967
+ "proposed": {
968
+ "doctor_name": updated_doctor_name,
969
+ "doctor_category": updated_doctor_category,
970
+ "patient_name": appt.get("patient_name", ""),
971
+ "patient_num": updated_patient_num,
972
+ "patient_mail": updated_patient_mail,
973
+ "visiting_date": updated_visiting_date,
974
+ "visiting_day": updated_visiting_day,
975
+ "visiting_time": updated_visiting_time,
976
+ },
977
+ }
978
+ return json.dumps(preview, ensure_ascii=False)
979
+
980
+ # Confirmed: apply update
981
  await db.execute(
982
  """UPDATE patients
983
  SET doctor_name = ?,
 
1021
  )
1022
  mail_status = "📧 Confirmation mail sent."
1023
  except Exception as e:
1024
+ traceback.print_exc()
1025
  mail_status = f"⚠️ Mail failed: {str(e)}"
1026
 
1027
  return (
 
1045
  doctor_name: str = "",
1046
  doctor_id: int = 0,
1047
  patient_mail: str = "",
1048
+ confirm: bool = False,
1049
  ) -> str:
1050
  """
1051
  Cancel (delete) an appointment.
1052
  Sends a confirmation email to the patient (required).
1053
+ IMPORTANT: This tool will NOT delete anything unless confirm=True.
1054
  """
1055
  db_path = get_db_path()
1056
  patient_num = format_bd_number(patient_num)
1057
  doctor_name = _clean_text(doctor_name)
1058
+ patient_mail = _normalize_email(patient_mail)
1059
 
1060
  async with aiosqlite.connect(db_path) as db:
1061
  db.row_factory = aiosqlite.Row
 
1106
  "message": "Email is required to cancel an appointment. Please provide the email address.",
1107
  }, ensure_ascii=False)
1108
 
1109
+ if not confirm:
1110
+ preview = {
1111
+ "success": False,
1112
+ "needs_confirmation": True,
1113
+ "message": (
1114
+ "I found your appointment. Please confirm cancellation. "
1115
+ "If you want to keep the rest of the existing information, "
1116
+ "you don't need to provide anything else—just confirm."
1117
+ ),
1118
+ "current": {
1119
+ "doctor_name": appt.get("doctor_name", doctor_name),
1120
+ "doctor_category": appt.get("doctor_category", ""),
1121
+ "patient_name": appt.get("patient_name", ""),
1122
+ "patient_num": appt.get("patient_num", patient_num),
1123
+ "patient_mail": appt_email,
1124
+ "visiting_date": appt.get("visiting_date", ""),
1125
+ "visiting_day": appt.get("visiting_day", ""),
1126
+ "visiting_time": appt.get("visiting_time", ""),
1127
+ },
1128
+ }
1129
+ return json.dumps(preview, ensure_ascii=False)
1130
+
1131
+ # Confirmed: delete after we have all details for mail
1132
  await db.execute("DELETE FROM patients WHERE id = ?", (appt["id"],))
1133
  await db.commit()
1134
 
 
1151
  )
1152
  mail_status = "Confirmation mail sent."
1153
  except Exception as e:
1154
+ traceback.print_exc()
1155
  mail_status = f"Mail failed: {str(e)}"
1156
 
1157
  return json.dumps({
 
1266
  - Ask whether to keep the rest of the existing appointment unchanged.
1267
  - If multiple appointments exist for a phone number, ask for the doctor name to select the correct one.
1268
  - Email is REQUIRED to update. If the existing record has no email, ask for it.
1269
+ - To avoid accidental changes: call `update_appointment` first with confirm=false to get a preview, show it to the user, then call again with confirm=true only after final confirmation.
1270
 
1271
  Important cancellation rules:
1272
  - If the user gave only a phone number and there is exactly one matching appointment, cancel it directly.
1273
  - If multiple appointments match, ask only for the doctor name.
1274
  - Email is REQUIRED to cancel or update. If missing, ask for email.
1275
+ - To avoid accidental deletion: call `delete_appointment` first with confirm=false to get a preview, show it to the user, then call again with confirm=true only after final confirmation.
1276
 
1277
  Do not give a normal conversational answer before the tool call.
1278
  """
services/tts.py CHANGED
@@ -45,9 +45,9 @@ def _parse_pct(text: str) -> float:
45
  # Effective speed = base * (1 + pct).
46
  # - `ELEVENLABS_SPEED_MAX` sets the upper clamp (default 3.0). If your ElevenLabs
47
  # model/voice rejects high values, lower this (e.g. 2.5).
48
- _ELEVEN_BASE_SPEED = float(os.getenv("ELEVENLABS_SPEED", "2.5"))
49
  _ELEVEN_SPEED_PCT = _parse_pct(os.getenv("ELEVENLABS_SPEED_PCT", "0%"))
50
- _ELEVEN_SPEED_MAX = float(os.getenv("ELEVENLABS_SPEED_MAX", "3.0"))
51
  ELEVENLABS_SPEED = _clamp(_ELEVEN_BASE_SPEED * (1.0 + _ELEVEN_SPEED_PCT), 0.5, _ELEVEN_SPEED_MAX)
52
  ELEVENLABS_OUTPUT_FORMAT = "mp3_22050_32"
53
  ELEVENLABS_STABILITY = 0.45
@@ -93,7 +93,7 @@ def split_sentences(text: str) -> list[str]:
93
  return [p.strip() for p in parts if len(p.strip()) > 1]
94
 
95
 
96
- async def _edge_tts_stream(text: str, voice: str = EDGE_VOICE, rate: str = "+18%"):
97
  """
98
  Stream Edge-TTS audio for a single text chunk.
99
  Default rate is slightly faster than normal.
@@ -173,7 +173,7 @@ async def _elevenlabs_stream(
173
  async def text_to_speech_stream(
174
  text: str,
175
  voice: str | None = None,
176
- rate: str = "+18%",
177
  ):
178
  """
179
  Stream TTS audio for `text`.
 
45
  # Effective speed = base * (1 + pct).
46
  # - `ELEVENLABS_SPEED_MAX` sets the upper clamp (default 3.0). If your ElevenLabs
47
  # model/voice rejects high values, lower this (e.g. 2.5).
48
+ _ELEVEN_BASE_SPEED = float(os.getenv("ELEVENLABS_SPEED", "2.8"))
49
  _ELEVEN_SPEED_PCT = _parse_pct(os.getenv("ELEVENLABS_SPEED_PCT", "0%"))
50
+ _ELEVEN_SPEED_MAX = float(os.getenv("ELEVENLABS_SPEED_MAX", "3.5"))
51
  ELEVENLABS_SPEED = _clamp(_ELEVEN_BASE_SPEED * (1.0 + _ELEVEN_SPEED_PCT), 0.5, _ELEVEN_SPEED_MAX)
52
  ELEVENLABS_OUTPUT_FORMAT = "mp3_22050_32"
53
  ELEVENLABS_STABILITY = 0.45
 
93
  return [p.strip() for p in parts if len(p.strip()) > 1]
94
 
95
 
96
+ async def _edge_tts_stream(text: str, voice: str = EDGE_VOICE, rate: str = "+22%"):
97
  """
98
  Stream Edge-TTS audio for a single text chunk.
99
  Default rate is slightly faster than normal.
 
173
  async def text_to_speech_stream(
174
  text: str,
175
  voice: str | None = None,
176
+ rate: str = "+22%",
177
  ):
178
  """
179
  Stream TTS audio for `text`.