Commit ยท
a6fa796
1
Parent(s): 42c497a
updated mail
Browse files- core/backend.py +304 -19
- services/tts.py +1 -1
core/backend.py
CHANGED
|
@@ -305,8 +305,16 @@ async def send_mail(to_mail: str, subject: str, body: str):
|
|
| 305 |
email["From"] = smtp_user
|
| 306 |
email["To"] = to_mail
|
| 307 |
email["Subject"] = subject
|
|
|
|
| 308 |
email.set_content(body)
|
| 309 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
await aiosmtplib.send(
|
| 311 |
email,
|
| 312 |
hostname="smtp.gmail.com",
|
|
@@ -316,6 +324,73 @@ async def send_mail(to_mail: str, subject: str, body: str):
|
|
| 316 |
use_tls=True,
|
| 317 |
)
|
| 318 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 320 |
# TOOLS
|
| 321 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -735,14 +810,198 @@ async def book_appointment(
|
|
| 735 |
|
| 736 |
|
| 737 |
@tool
|
| 738 |
-
async def
|
| 739 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 740 |
db_path = get_db_path()
|
| 741 |
patient_num = format_bd_number(patient_num)
|
| 742 |
doctor_name = _clean_text(doctor_name)
|
|
|
|
| 743 |
|
| 744 |
async with aiosqlite.connect(db_path) as db:
|
| 745 |
db.row_factory = aiosqlite.Row
|
|
|
|
| 746 |
|
| 747 |
if not doctor_name and doctor_id:
|
| 748 |
cursor = await db.execute("SELECT doctor_name FROM doctors WHERE id = ?", (doctor_id,))
|
|
@@ -756,16 +1015,9 @@ async def delete_appointment(patient_num: str, doctor_name: str = "", doctor_id:
|
|
| 756 |
WHERE patient_num = ? AND LOWER(doctor_name) = LOWER(?)""",
|
| 757 |
(patient_num, doctor_name),
|
| 758 |
)
|
| 759 |
-
|
| 760 |
-
if not
|
| 761 |
return json.dumps({"success": False, "message": "No matching appointment found."})
|
| 762 |
-
|
| 763 |
-
await db.execute(
|
| 764 |
-
"""DELETE FROM patients
|
| 765 |
-
WHERE patient_num = ? AND LOWER(doctor_name) = LOWER(?)""",
|
| 766 |
-
(patient_num, doctor_name),
|
| 767 |
-
)
|
| 768 |
-
await db.commit()
|
| 769 |
else:
|
| 770 |
cursor = await db.execute(
|
| 771 |
"""SELECT * FROM patients
|
|
@@ -784,17 +1036,46 @@ async def delete_appointment(patient_num: str, doctor_name: str = "", doctor_id:
|
|
| 784 |
"data": [dict(row) for row in rows],
|
| 785 |
}, ensure_ascii=False)
|
| 786 |
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 794 |
|
| 795 |
return json.dumps({
|
| 796 |
"success": True,
|
| 797 |
-
"message": f"Appointment with Dr. {doctor_name}
|
| 798 |
}, ensure_ascii=False)
|
| 799 |
|
| 800 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -844,6 +1125,7 @@ TOOL RULES:
|
|
| 844 |
- Use `find_doctors` first for doctor search, specialty search, and availability search.
|
| 845 |
- Use `get_doctors_by_day` or `get_categories_by_day` when the user asks about a day directly.
|
| 846 |
- Use `book_appointment` only after identifying the doctor and required patient details.
|
|
|
|
| 847 |
- Never invent `doctor_id`. Get it from tool results or resolve by doctor_name/category.
|
| 848 |
- If the user gives a Bangla date like "เฆเฆเฆพเฆฎเงเฆเฆพเฆฒ" or "เฆชเฆฐเฆถเง", convert it to a real date before booking.
|
| 849 |
- Email is REQUIRED for booking and must be used to send a confirmation mail.
|
|
@@ -882,6 +1164,7 @@ You must now choose the correct tool instead of answering in prose:
|
|
| 882 |
- Use `find_doctors` or `search_doctor` for doctor/specialty/availability questions.
|
| 883 |
- Use `get_doctors_by_day` or `get_categories_by_day` for day-based availability.
|
| 884 |
- Use `book_appointment` when the user is confirming a booking.
|
|
|
|
| 885 |
- Use `delete_appointment` when the user is cancelling a booking.
|
| 886 |
|
| 887 |
Important booking rules:
|
|
@@ -893,6 +1176,7 @@ Important booking rules:
|
|
| 893 |
Important cancellation rules:
|
| 894 |
- If the user gave only a phone number and there is exactly one matching appointment, cancel it directly.
|
| 895 |
- If multiple appointments match, ask only for the doctor name.
|
|
|
|
| 896 |
|
| 897 |
Do not give a normal conversational answer before the tool call.
|
| 898 |
"""
|
|
@@ -927,6 +1211,7 @@ class AIBackend:
|
|
| 927 |
book_appointment,
|
| 928 |
get_bd_time,
|
| 929 |
search_appointment_by_phone,
|
|
|
|
| 930 |
delete_appointment,
|
| 931 |
get_categories_by_day,
|
| 932 |
get_doctors_by_day
|
|
|
|
| 305 |
email["From"] = smtp_user
|
| 306 |
email["To"] = to_mail
|
| 307 |
email["Subject"] = subject
|
| 308 |
+
# Plain-text fallback
|
| 309 |
email.set_content(body)
|
| 310 |
|
| 311 |
+
# Professional HTML version
|
| 312 |
+
try:
|
| 313 |
+
html = _format_email_html(subject=subject, body_text=body)
|
| 314 |
+
email.add_alternative(html, subtype="html")
|
| 315 |
+
except Exception:
|
| 316 |
+
pass
|
| 317 |
+
|
| 318 |
await aiosmtplib.send(
|
| 319 |
email,
|
| 320 |
hostname="smtp.gmail.com",
|
|
|
|
| 324 |
use_tls=True,
|
| 325 |
)
|
| 326 |
|
| 327 |
+
|
| 328 |
+
def _format_email_html(subject: str, body_text: str) -> str:
|
| 329 |
+
"""
|
| 330 |
+
Render a simple, professional HTML email.
|
| 331 |
+
Input body_text should be plain text with newlines.
|
| 332 |
+
"""
|
| 333 |
+
safe = (
|
| 334 |
+
(body_text or "")
|
| 335 |
+
.replace("&", "&")
|
| 336 |
+
.replace("<", "<")
|
| 337 |
+
.replace(">", ">")
|
| 338 |
+
)
|
| 339 |
+
safe = safe.replace("\n", "<br>")
|
| 340 |
+
return f"""\
|
| 341 |
+
<!doctype html>
|
| 342 |
+
<html>
|
| 343 |
+
<body style="margin:0;padding:0;background:#f6f7fb;font-family:Arial,Helvetica,sans-serif;">
|
| 344 |
+
<div style="max-width:640px;margin:0 auto;padding:24px;">
|
| 345 |
+
<div style="background:#ffffff;border-radius:14px;border:1px solid #e6e8f0;overflow:hidden;">
|
| 346 |
+
<div style="padding:18px 20px;background:linear-gradient(135deg,#0ea5e9,#8b5cf6);color:#fff;">
|
| 347 |
+
<div style="font-size:16px;font-weight:700;">{subject}</div>
|
| 348 |
+
<div style="font-size:12px;opacity:.9;margin-top:4px;">Aasha โข Hospital Assistant</div>
|
| 349 |
+
</div>
|
| 350 |
+
<div style="padding:18px 20px;color:#0f172a;font-size:14px;line-height:1.55;">
|
| 351 |
+
{safe}
|
| 352 |
+
<div style="margin-top:18px;color:#64748b;font-size:12px;">
|
| 353 |
+
This is an automated message. If you did not request this, please ignore it.
|
| 354 |
+
</div>
|
| 355 |
+
</div>
|
| 356 |
+
</div>
|
| 357 |
+
</div>
|
| 358 |
+
</body>
|
| 359 |
+
</html>
|
| 360 |
+
"""
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def _format_appt_email_text(
|
| 364 |
+
action: str,
|
| 365 |
+
doctor_name: str,
|
| 366 |
+
patient_name: str,
|
| 367 |
+
patient_num: str,
|
| 368 |
+
visiting_date: str,
|
| 369 |
+
visiting_day: str,
|
| 370 |
+
visiting_time: str,
|
| 371 |
+
extra: str = "",
|
| 372 |
+
) -> str:
|
| 373 |
+
action_line = {
|
| 374 |
+
"booked": "โ
Appointment Confirmed",
|
| 375 |
+
"updated": "โ
Appointment Updated",
|
| 376 |
+
"cancelled": "โ
Appointment Cancelled",
|
| 377 |
+
}.get(action, "โ
Appointment Update")
|
| 378 |
+
|
| 379 |
+
lines = [
|
| 380 |
+
action_line,
|
| 381 |
+
"",
|
| 382 |
+
f"Doctor : {doctor_name}",
|
| 383 |
+
f"Patient : {patient_name}",
|
| 384 |
+
f"Contact : {patient_num}",
|
| 385 |
+
f"Visit Date : {visiting_date}",
|
| 386 |
+
f"Visit Day : {visiting_day}",
|
| 387 |
+
f"Visit Time : {visiting_time}",
|
| 388 |
+
]
|
| 389 |
+
if extra:
|
| 390 |
+
lines.extend(["", extra.strip()])
|
| 391 |
+
lines.extend(["", "Thank you.", "Aasha โข Hospital Assistant"])
|
| 392 |
+
return "\n".join(lines)
|
| 393 |
+
|
| 394 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 395 |
# TOOLS
|
| 396 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 810 |
|
| 811 |
|
| 812 |
@tool
|
| 813 |
+
async def update_appointment(
|
| 814 |
+
patient_num: str,
|
| 815 |
+
doctor_name: str = "",
|
| 816 |
+
doctor_id: int = 0,
|
| 817 |
+
new_visiting_date: str = "",
|
| 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.
|
| 824 |
+
|
| 825 |
+
You can update:
|
| 826 |
+
- visit date/day
|
| 827 |
+
- doctor (by name or id)
|
| 828 |
+
- phone number
|
| 829 |
+
- email
|
| 830 |
+
|
| 831 |
+
Rules:
|
| 832 |
+
- patient_num is required for lookup.
|
| 833 |
+
- If multiple appointments exist for the phone number, provide doctor_name
|
| 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
|
| 847 |
+
if parsed_date:
|
| 848 |
+
new_visiting_date = parsed_date
|
| 849 |
+
|
| 850 |
+
if not patient_num_norm:
|
| 851 |
+
return "Missing details. Need patient phone number."
|
| 852 |
+
|
| 853 |
+
if not any([new_visiting_date, new_doctor_name, new_patient_num, new_patient_mail, doctor_id]):
|
| 854 |
+
return "Nothing to update. Provide new date, doctor, phone, or email."
|
| 855 |
+
|
| 856 |
+
async with aiosqlite.connect(db_path) as db:
|
| 857 |
+
db.row_factory = aiosqlite.Row
|
| 858 |
+
|
| 859 |
+
# Find matching appointments
|
| 860 |
+
params = [patient_num_norm]
|
| 861 |
+
q = "SELECT * FROM patients WHERE patient_num = ?"
|
| 862 |
+
if selector_name:
|
| 863 |
+
q += " AND LOWER(doctor_name) = LOWER(?)"
|
| 864 |
+
params.append(selector_name)
|
| 865 |
+
rows = await (await db.execute(q, params)).fetchall()
|
| 866 |
+
|
| 867 |
+
if not rows:
|
| 868 |
+
return "No appointment found for this phone number."
|
| 869 |
+
if len(rows) > 1 and not selector_name:
|
| 870 |
+
return (
|
| 871 |
+
"Multiple appointments found for this phone number. "
|
| 872 |
+
"Please specify the doctor name to update."
|
| 873 |
+
)
|
| 874 |
+
|
| 875 |
+
appt = dict(rows[0])
|
| 876 |
+
|
| 877 |
+
# Resolve new doctor if requested
|
| 878 |
+
resolved_doctor = None
|
| 879 |
+
if doctor_id:
|
| 880 |
+
d = await (await db.execute("SELECT * FROM doctors WHERE id = ?", (doctor_id,))).fetchone()
|
| 881 |
+
if d:
|
| 882 |
+
resolved_doctor = dict(d)
|
| 883 |
+
if resolved_doctor is None and new_doctor_name:
|
| 884 |
+
d = await (await db.execute(
|
| 885 |
+
"SELECT * FROM doctors WHERE LOWER(doctor_name) = LOWER(?)",
|
| 886 |
+
(new_doctor_name,),
|
| 887 |
+
)).fetchone()
|
| 888 |
+
if d:
|
| 889 |
+
resolved_doctor = dict(d)
|
| 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", "")
|
| 897 |
+
updated_visiting_day = appt.get("visiting_day", "")
|
| 898 |
+
updated_visiting_date = appt.get("visiting_date", "")
|
| 899 |
+
updated_patient_num = appt.get("patient_num", "")
|
| 900 |
+
updated_patient_mail = appt.get("patient_mail", "")
|
| 901 |
+
|
| 902 |
+
if resolved_doctor:
|
| 903 |
+
updated_doctor_name = resolved_doctor.get("doctor_name", updated_doctor_name)
|
| 904 |
+
updated_doctor_category = resolved_doctor.get("category", updated_doctor_category)
|
| 905 |
+
updated_visiting_time = (resolved_doctor.get("visiting_time") or updated_visiting_time).strip()
|
| 906 |
+
updated_visiting_day = (resolved_doctor.get("visiting_days") or updated_visiting_day).strip()
|
| 907 |
+
|
| 908 |
+
if new_visiting_date:
|
| 909 |
+
updated_visiting_date = new_visiting_date
|
| 910 |
+
# Derive English day name from date for consistency
|
| 911 |
+
try:
|
| 912 |
+
import datetime as _dt
|
| 913 |
+
y, m, d = [int(x) for x in updated_visiting_date.split("-")]
|
| 914 |
+
updated_visiting_day = _dt.date(y, m, d).strftime("%A")
|
| 915 |
+
except Exception:
|
| 916 |
+
pass
|
| 917 |
+
|
| 918 |
+
if new_patient_num:
|
| 919 |
+
updated_patient_num = new_patient_num
|
| 920 |
+
if new_patient_mail:
|
| 921 |
+
updated_patient_mail = new_patient_mail
|
| 922 |
+
|
| 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 = ?,
|
| 929 |
+
doctor_category = ?,
|
| 930 |
+
patient_num = ?,
|
| 931 |
+
visiting_date = ?,
|
| 932 |
+
visiting_day = ?,
|
| 933 |
+
visiting_time = ?,
|
| 934 |
+
patient_mail = ?
|
| 935 |
+
WHERE id = ?""",
|
| 936 |
+
(
|
| 937 |
+
updated_doctor_name,
|
| 938 |
+
updated_doctor_category,
|
| 939 |
+
updated_patient_num,
|
| 940 |
+
updated_visiting_date,
|
| 941 |
+
updated_visiting_day,
|
| 942 |
+
updated_visiting_time,
|
| 943 |
+
updated_patient_mail,
|
| 944 |
+
appt["id"],
|
| 945 |
+
),
|
| 946 |
+
)
|
| 947 |
+
await db.commit()
|
| 948 |
+
|
| 949 |
+
# Send confirmation email
|
| 950 |
+
patient_name = appt.get("patient_name", "Patient")
|
| 951 |
+
email_text = _format_appt_email_text(
|
| 952 |
+
action="updated",
|
| 953 |
+
doctor_name=updated_doctor_name,
|
| 954 |
+
patient_name=patient_name,
|
| 955 |
+
patient_num=updated_patient_num,
|
| 956 |
+
visiting_date=updated_visiting_date,
|
| 957 |
+
visiting_day=updated_visiting_day,
|
| 958 |
+
visiting_time=updated_visiting_time,
|
| 959 |
+
extra="Your appointment details have been updated successfully.",
|
| 960 |
+
)
|
| 961 |
+
try:
|
| 962 |
+
await send_mail(
|
| 963 |
+
to_mail=updated_patient_mail,
|
| 964 |
+
subject="Appointment Updated",
|
| 965 |
+
body=email_text,
|
| 966 |
+
)
|
| 967 |
+
mail_status = "๐ง Confirmation mail sent."
|
| 968 |
+
except Exception as e:
|
| 969 |
+
mail_status = f"โ ๏ธ Mail failed: {str(e)}"
|
| 970 |
+
|
| 971 |
+
return (
|
| 972 |
+
"โ
Appointment Updated!\n"
|
| 973 |
+
"โโโโโโโโโโโโโโโโโโโโโโ\n"
|
| 974 |
+
f"Doctor : {updated_doctor_name}\n"
|
| 975 |
+
f"Patient : {patient_name}\n"
|
| 976 |
+
f"Date : {updated_visiting_date}\n"
|
| 977 |
+
f"Day : {updated_visiting_day}\n"
|
| 978 |
+
f"Time : {updated_visiting_time}\n"
|
| 979 |
+
f"Contact : {updated_patient_num}\n"
|
| 980 |
+
f"Email : {updated_patient_mail}\n"
|
| 981 |
+
"โโโโโโโโโโโโโโโโโโโโโโ\n"
|
| 982 |
+
f"{mail_status}"
|
| 983 |
+
)
|
| 984 |
+
|
| 985 |
+
|
| 986 |
+
@tool
|
| 987 |
+
async def delete_appointment(
|
| 988 |
+
patient_num: str,
|
| 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
|
| 1004 |
+
appt_row = None
|
| 1005 |
|
| 1006 |
if not doctor_name and doctor_id:
|
| 1007 |
cursor = await db.execute("SELECT doctor_name FROM doctors WHERE id = ?", (doctor_id,))
|
|
|
|
| 1015 |
WHERE patient_num = ? AND LOWER(doctor_name) = LOWER(?)""",
|
| 1016 |
(patient_num, doctor_name),
|
| 1017 |
)
|
| 1018 |
+
appt_row = await cursor.fetchone()
|
| 1019 |
+
if not appt_row:
|
| 1020 |
return json.dumps({"success": False, "message": "No matching appointment found."})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1021 |
else:
|
| 1022 |
cursor = await db.execute(
|
| 1023 |
"""SELECT * FROM patients
|
|
|
|
| 1036 |
"data": [dict(row) for row in rows],
|
| 1037 |
}, ensure_ascii=False)
|
| 1038 |
|
| 1039 |
+
appt_row = rows[0]
|
| 1040 |
+
doctor_name = appt_row["doctor_name"] or doctor_name
|
| 1041 |
+
|
| 1042 |
+
# Resolve email (required)
|
| 1043 |
+
appt = dict(appt_row) if appt_row is not None else {}
|
| 1044 |
+
appt_email = _clean_text(appt.get("patient_mail", "")) or patient_mail
|
| 1045 |
+
if not appt_email:
|
| 1046 |
+
return json.dumps({
|
| 1047 |
+
"success": False,
|
| 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 |
+
|
| 1055 |
+
# Send cancellation email
|
| 1056 |
+
email_text = _format_appt_email_text(
|
| 1057 |
+
action="cancelled",
|
| 1058 |
+
doctor_name=appt.get("doctor_name", doctor_name),
|
| 1059 |
+
patient_name=appt.get("patient_name", "Patient"),
|
| 1060 |
+
patient_num=appt.get("patient_num", patient_num),
|
| 1061 |
+
visiting_date=appt.get("visiting_date", ""),
|
| 1062 |
+
visiting_day=appt.get("visiting_day", ""),
|
| 1063 |
+
visiting_time=appt.get("visiting_time", ""),
|
| 1064 |
+
extra="Your appointment has been cancelled successfully.",
|
| 1065 |
+
)
|
| 1066 |
+
try:
|
| 1067 |
+
await send_mail(
|
| 1068 |
+
to_mail=appt_email,
|
| 1069 |
+
subject="Appointment Cancelled",
|
| 1070 |
+
body=email_text,
|
| 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({
|
| 1077 |
"success": True,
|
| 1078 |
+
"message": f"Appointment with Dr. {doctor_name} cancelled successfully. {mail_status}",
|
| 1079 |
}, ensure_ascii=False)
|
| 1080 |
|
| 1081 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 1125 |
- Use `find_doctors` first for doctor search, specialty search, and availability search.
|
| 1126 |
- Use `get_doctors_by_day` or `get_categories_by_day` when the user asks about a day directly.
|
| 1127 |
- Use `book_appointment` only after identifying the doctor and required patient details.
|
| 1128 |
+
- Use `update_appointment` when the user wants to change an existing appointment.
|
| 1129 |
- Never invent `doctor_id`. Get it from tool results or resolve by doctor_name/category.
|
| 1130 |
- If the user gives a Bangla date like "เฆเฆเฆพเฆฎเงเฆเฆพเฆฒ" or "เฆชเฆฐเฆถเง", convert it to a real date before booking.
|
| 1131 |
- Email is REQUIRED for booking and must be used to send a confirmation mail.
|
|
|
|
| 1164 |
- Use `find_doctors` or `search_doctor` for doctor/specialty/availability questions.
|
| 1165 |
- Use `get_doctors_by_day` or `get_categories_by_day` for day-based availability.
|
| 1166 |
- Use `book_appointment` when the user is confirming a booking.
|
| 1167 |
+
- Use `update_appointment` when the user wants to update an appointment.
|
| 1168 |
- Use `delete_appointment` when the user is cancelling a booking.
|
| 1169 |
|
| 1170 |
Important booking rules:
|
|
|
|
| 1176 |
Important cancellation rules:
|
| 1177 |
- If the user gave only a phone number and there is exactly one matching appointment, cancel it directly.
|
| 1178 |
- If multiple appointments match, ask only for the doctor name.
|
| 1179 |
+
- Email is REQUIRED to cancel or update. If missing, ask for email.
|
| 1180 |
|
| 1181 |
Do not give a normal conversational answer before the tool call.
|
| 1182 |
"""
|
|
|
|
| 1211 |
book_appointment,
|
| 1212 |
get_bd_time,
|
| 1213 |
search_appointment_by_phone,
|
| 1214 |
+
update_appointment,
|
| 1215 |
delete_appointment,
|
| 1216 |
get_categories_by_day,
|
| 1217 |
get_doctors_by_day
|
services/tts.py
CHANGED
|
@@ -44,7 +44,7 @@ def _parse_pct(text: str) -> float:
|
|
| 44 |
# - `ELEVENLABS_SPEED_PCT` is an optional relative adjustment like "+10%" or "-5%".
|
| 45 |
# This is applied on top of the base: effective = base * (1 + pct).
|
| 46 |
# - The final value is clamped to a safe range to avoid invalid API values.
|
| 47 |
-
_ELEVEN_BASE_SPEED = float(os.getenv("ELEVENLABS_SPEED", "
|
| 48 |
_ELEVEN_SPEED_PCT = _parse_pct(os.getenv("ELEVENLABS_SPEED_PCT", "0%"))
|
| 49 |
ELEVENLABS_SPEED = _clamp(_ELEVEN_BASE_SPEED * (1.0 + _ELEVEN_SPEED_PCT), 0.5, 2.5)
|
| 50 |
ELEVENLABS_OUTPUT_FORMAT = "mp3_22050_32"
|
|
|
|
| 44 |
# - `ELEVENLABS_SPEED_PCT` is an optional relative adjustment like "+10%" or "-5%".
|
| 45 |
# This is applied on top of the base: effective = base * (1 + pct).
|
| 46 |
# - The final value is clamped to a safe range to avoid invalid API values.
|
| 47 |
+
_ELEVEN_BASE_SPEED = float(os.getenv("ELEVENLABS_SPEED", "5"))
|
| 48 |
_ELEVEN_SPEED_PCT = _parse_pct(os.getenv("ELEVENLABS_SPEED_PCT", "0%"))
|
| 49 |
ELEVENLABS_SPEED = _clamp(_ELEVEN_BASE_SPEED * (1.0 + _ELEVEN_SPEED_PCT), 0.5, 2.5)
|
| 50 |
ELEVENLABS_OUTPUT_FORMAT = "mp3_22050_32"
|