rakib72642 commited on
Commit
17cb949
·
1 Parent(s): 0102e34

many issue

Browse files
Files changed (7) hide show
  1. app.py +47 -2
  2. core/backend.py +212 -27
  3. db_view/db.html +1 -1
  4. db_view/dbapi.py +2 -1
  5. frontend/script.js +10 -6
  6. frontend/style.css +35 -24
  7. services/tts.py +1 -1
app.py CHANGED
@@ -38,6 +38,7 @@ from starlette.websockets import WebSocketState
38
  from core.backend import AIBackend
39
  from services.stt import STTProcessor
40
  from services.streaming import ParallelTTSStreamer
 
41
 
42
  # ── WebRTC (optional) ─────────────────────────────────────────────────────────
43
  try:
@@ -101,6 +102,11 @@ try:
101
  except Exception:
102
  pass
103
 
 
 
 
 
 
104
 
105
  @app.get("/")
106
  async def root():
@@ -110,6 +116,14 @@ async def root():
110
  return HTMLResponse("<h2>frontend/index.html not found</h2>", status_code=404)
111
 
112
 
 
 
 
 
 
 
 
 
113
  @app.get("/health")
114
  async def health():
115
  from services.stt import _model_ready, _model_error
@@ -311,6 +325,8 @@ async def ws_voice(ws: WebSocket):
311
  stt = STTProcessor()
312
  _active_streamer: ParallelTTSStreamer | None = None
313
  _active_task: asyncio.Task | None = None
 
 
314
 
315
  async def _cancel_active():
316
  nonlocal _active_streamer, _active_task
@@ -325,6 +341,13 @@ async def ws_voice(ws: WebSocket):
325
  pass
326
  _active_task = None
327
 
 
 
 
 
 
 
 
328
  async def _handle_utterance(audio_bytes: bytes):
329
  nonlocal _active_streamer
330
 
@@ -368,7 +391,22 @@ async def ws_voice(ws: WebSocket):
368
  _active_streamer = None
369
  await _safe_text(ws, {"type": "end"})
370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  try:
 
372
  while True:
373
  if not _ws_open(ws):
374
  break
@@ -386,8 +424,7 @@ async def ws_voice(ws: WebSocket):
386
  if "bytes" in data and data["bytes"]:
387
  audio_bytes = data["bytes"]
388
  print(f"[VOICE] [{user_id}] Utterance: {len(audio_bytes):,} bytes")
389
- await _cancel_active()
390
- _active_task = asyncio.create_task(_handle_utterance(audio_bytes))
391
 
392
  elif "text" in data and data["text"]:
393
  try:
@@ -404,6 +441,7 @@ async def ws_voice(ws: WebSocket):
404
  await _safe_text(ws, {"type": "pong"})
405
  elif t == "cancel":
406
  await _cancel_active()
 
407
  await _safe_text(ws, {"type": "end"})
408
  except json.JSONDecodeError:
409
  pass
@@ -414,5 +452,12 @@ async def ws_voice(ws: WebSocket):
414
  if "disconnect" not in str(exc).lower():
415
  print(f"[VOICE] Error: {exc}")
416
  finally:
 
 
 
 
 
 
 
417
  await _cancel_active()
418
  print(f"[VOICE] [{user_id}] Handler exiting cleanly.")
 
38
  from core.backend import AIBackend
39
  from services.stt import STTProcessor
40
  from services.streaming import ParallelTTSStreamer
41
+ from db_view.dbapi import app as db_api_app
42
 
43
  # ── WebRTC (optional) ─────────────────────────────────────────────────────────
44
  try:
 
102
  except Exception:
103
  pass
104
 
105
+ try:
106
+ app.mount("/dbapi", db_api_app)
107
+ except Exception:
108
+ pass
109
+
110
 
111
  @app.get("/")
112
  async def root():
 
116
  return HTMLResponse("<h2>frontend/index.html not found</h2>", status_code=404)
117
 
118
 
119
+ @app.get("/db-view")
120
+ async def db_view():
121
+ db_index_path = BASE_DIR / "db_view" / "db.html"
122
+ if db_index_path.exists():
123
+ return FileResponse(str(db_index_path))
124
+ return HTMLResponse("<h2>db_view/db.html not found</h2>", status_code=404)
125
+
126
+
127
  @app.get("/health")
128
  async def health():
129
  from services.stt import _model_ready, _model_error
 
325
  stt = STTProcessor()
326
  _active_streamer: ParallelTTSStreamer | None = None
327
  _active_task: asyncio.Task | None = None
328
+ _utterance_q: asyncio.Queue[bytes | None] = asyncio.Queue()
329
+ _worker_task: asyncio.Task | None = None
330
 
331
  async def _cancel_active():
332
  nonlocal _active_streamer, _active_task
 
341
  pass
342
  _active_task = None
343
 
344
+ async def _drain_utterance_queue():
345
+ while True:
346
+ try:
347
+ _utterance_q.get_nowait()
348
+ except asyncio.QueueEmpty:
349
+ break
350
+
351
  async def _handle_utterance(audio_bytes: bytes):
352
  nonlocal _active_streamer
353
 
 
391
  _active_streamer = None
392
  await _safe_text(ws, {"type": "end"})
393
 
394
+ async def _utterance_worker():
395
+ while True:
396
+ audio_bytes = await _utterance_q.get()
397
+ if audio_bytes is None:
398
+ break
399
+ try:
400
+ await _handle_utterance(audio_bytes)
401
+ except asyncio.CancelledError:
402
+ raise
403
+ except Exception as exc:
404
+ print(f"[VOICE] Utterance worker error: {exc}")
405
+ await _safe_text(ws, {"type": "error", "text": str(exc)})
406
+ await _safe_text(ws, {"type": "end"})
407
+
408
  try:
409
+ _worker_task = asyncio.create_task(_utterance_worker())
410
  while True:
411
  if not _ws_open(ws):
412
  break
 
424
  if "bytes" in data and data["bytes"]:
425
  audio_bytes = data["bytes"]
426
  print(f"[VOICE] [{user_id}] Utterance: {len(audio_bytes):,} bytes")
427
+ await _utterance_q.put(audio_bytes)
 
428
 
429
  elif "text" in data and data["text"]:
430
  try:
 
441
  await _safe_text(ws, {"type": "pong"})
442
  elif t == "cancel":
443
  await _cancel_active()
444
+ await _drain_utterance_queue()
445
  await _safe_text(ws, {"type": "end"})
446
  except json.JSONDecodeError:
447
  pass
 
452
  if "disconnect" not in str(exc).lower():
453
  print(f"[VOICE] Error: {exc}")
454
  finally:
455
+ await _utterance_q.put(None)
456
+ if _worker_task is not None and not _worker_task.done():
457
+ _worker_task.cancel()
458
+ try:
459
+ await _worker_task
460
+ except (asyncio.CancelledError, Exception):
461
+ pass
462
  await _cancel_active()
463
  print(f"[VOICE] [{user_id}] Handler exiting cleanly.")
core/backend.py CHANGED
@@ -47,7 +47,7 @@ def get_db_path() -> str:
47
 
48
 
49
  def format_bd_number(num: str) -> str:
50
- num = num.strip().replace(" ", "")
51
  if num.startswith("01") and len(num) == 11:
52
  return "+88" + num
53
  if num.startswith("8801"):
@@ -59,6 +59,34 @@ def _clean_text(text: str) -> str:
59
  return re.sub(r"\s+", " ", (text or "").strip())
60
 
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  DAY_ALIASES = {
63
  "sunday": "Sunday",
64
  "monday": "Monday",
@@ -76,6 +104,16 @@ DAY_ALIASES = {
76
  "শনিবার": "Saturday",
77
  }
78
 
 
 
 
 
 
 
 
 
 
 
79
  SPECIALTY_ALIASES = {
80
  "চক্ষু": ["ophthalmologist", "eye specialist", "eye doctor", "ophthalmology", "eye"],
81
  "আই": ["ophthalmologist", "eye specialist", "eye doctor", "ophthalmology", "eye"],
@@ -192,6 +230,58 @@ def _parse_visit_date(text: str) -> Optional[str]:
192
  return None
193
 
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  def send_sms(to_number: str, message: str) -> None:
196
  client = Client(os.getenv("TWILIO_ACCOUNT_SID"), os.getenv("TWILIO_AUTH_TOKEN"))
197
  client.messages.create(
@@ -490,7 +580,7 @@ async def book_appointment(
490
  patient_age: Age of the patient (e.g. "32").
491
  patient_num: Contact phone number of the patient.
492
  visiting_date: Date of visit in YYYY-MM-DD format or natural text.
493
- patient_mail: Mail address for confirmation mail.
494
  """
495
  db_path = get_db_path()
496
  patient_num = format_bd_number(patient_num)
@@ -499,14 +589,15 @@ async def book_appointment(
499
  doctor_name = _clean_text(doctor_name)
500
  category = _clean_text(category)
501
  visiting_date = _clean_text(visiting_date)
 
502
  parsed_date = _parse_visit_date(visiting_date)
503
  if parsed_date:
504
  visiting_date = parsed_date
505
 
506
- if not patient_name or not patient_age or not patient_num or not visiting_date or not patient_mail:
507
  return (
508
  "Missing booking details. Need patient name, age, phone number, "
509
- "visit date, and email."
510
  )
511
 
512
  async with aiosqlite.connect(db_path) as db:
@@ -561,7 +652,7 @@ async def book_appointment(
561
  )
562
  await db.commit()
563
 
564
- # Mail SMS confirmation
565
  mail_message = (
566
  f"Doctor : {doctor_name}\n"
567
  f"Patient : {patient_name}\n"
@@ -569,15 +660,19 @@ async def book_appointment(
569
  f"Visit Time : {visiting_time}\n"
570
  f"Please arrive on time."
571
  )
572
- try:
573
- await send_mail(
574
- to_mail=patient_mail,
575
- subject="✅ Appointment Confirmed!",
576
- body=mail_message,
577
- )
578
- mail_status = "\n📧 Mail confirmation sent."
579
- except Exception as e:
580
- mail_status = f"\n⚠️ Mail failed: {str(e)}"
 
 
 
 
581
 
582
  return (
583
  f"✅ Appointment Booked!\n"
@@ -609,20 +704,47 @@ async def delete_appointment(patient_num: str, doctor_name: str = "", doctor_id:
609
  if row:
610
  doctor_name = row["doctor_name"]
611
 
612
- cursor = await db.execute(
613
- """SELECT * FROM patients
614
- WHERE patient_num = ? AND LOWER(doctor_name) = LOWER(?)""",
615
- (patient_num, doctor_name),
616
- )
617
- if not await cursor.fetchone():
618
- return json.dumps({"success": False, "message": "No matching appointment found."})
 
 
619
 
620
- await db.execute(
621
- """DELETE FROM patients
622
- WHERE patient_num = ? AND LOWER(doctor_name) = LOWER(?)""",
623
- (patient_num, doctor_name),
624
- )
625
- await db.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
 
627
  return json.dumps({
628
  "success": True,
@@ -660,6 +782,9 @@ TOOL RULES:
660
  - Use `book_appointment` only after identifying the doctor and required patient details.
661
  - Never invent `doctor_id`. Get it from tool results or resolve by doctor_name/category.
662
  - If the user gives a Bangla date like "আগামীকাল" or "পরশু", convert it to a real date before booking.
 
 
 
663
 
664
  LANGUAGE RULE:
665
  - Respond in the user’s language.
@@ -684,6 +809,27 @@ SUMMARY_SYSTEM = (
684
  "Use this memory for continuity. Do not repeat it unless asked."
685
  )
686
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
 
688
  # ═══════════════════════════════════════════════════════════════════════════════
689
  # AGENT
@@ -788,6 +934,36 @@ class AIBackend:
788
  "messages": [RemoveMessage(id=m.id) for m in messages[:-2]],
789
  }
790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
791
  # ── Chat node ──────────────────────────────────────────────────────────────
792
  async def chat_node(self, state: ChatState):
793
  """
@@ -813,6 +989,15 @@ class AIBackend:
813
 
814
  response = await self.llm_with_tools.ainvoke(full_messages)
815
 
 
 
 
 
 
 
 
 
 
816
  print(f"[AI]: {str(response.content)[:200]}")
817
  print(">>>>>>>>>> CHAT NODE END <<<<<<<<<<")
818
  return {"messages": [response]}
 
47
 
48
 
49
  def format_bd_number(num: str) -> str:
50
+ num = _normalize_digits(num).replace(" ", "")
51
  if num.startswith("01") and len(num) == 11:
52
  return "+88" + num
53
  if num.startswith("8801"):
 
59
  return re.sub(r"\s+", " ", (text or "").strip())
60
 
61
 
62
+ _DIGIT_TRANSLATION = str.maketrans({
63
+ "০": "0",
64
+ "১": "1",
65
+ "২": "2",
66
+ "৩": "3",
67
+ "৪": "4",
68
+ "৫": "5",
69
+ "৬": "6",
70
+ "৭": "7",
71
+ "৮": "8",
72
+ "৯": "9",
73
+ "٠": "0",
74
+ "١": "1",
75
+ "٢": "2",
76
+ "٣": "3",
77
+ "٤": "4",
78
+ "٥": "5",
79
+ "٦": "6",
80
+ "٧": "7",
81
+ "٨": "8",
82
+ "٩": "9",
83
+ })
84
+
85
+
86
+ def _normalize_digits(text: str) -> str:
87
+ return _clean_text(text).translate(_DIGIT_TRANSLATION)
88
+
89
+
90
  DAY_ALIASES = {
91
  "sunday": "Sunday",
92
  "monday": "Monday",
 
104
  "শনিবার": "Saturday",
105
  }
106
 
107
+ BOOKING_CONFIRM_WORDS = (
108
+ "জি", "ঠিক আছে", "ঠিক", "হ্যাঁ", "yes", "okay", "ok", "তথ্য ঠিক", "সব ঠিক",
109
+ )
110
+
111
+ TOOL_INTENT_WORDS = (
112
+ "book", "booking", "appointment", "অ্যাপয়েন্ট", "অ্যাপয়েন্টমেন্ট", "বুক",
113
+ "slot", "স্লট", "available", "availability", "ডাক্তার", "doctor", "দেখাতে",
114
+ "কেনসেল", "cancel", "বাতিল", "delete", "খালি", "কোন ডাক্তার",
115
+ )
116
+
117
  SPECIALTY_ALIASES = {
118
  "চক্ষু": ["ophthalmologist", "eye specialist", "eye doctor", "ophthalmology", "eye"],
119
  "আই": ["ophthalmologist", "eye specialist", "eye doctor", "ophthalmology", "eye"],
 
230
  return None
231
 
232
 
233
+ def _message_text(content) -> str:
234
+ if isinstance(content, str):
235
+ return content
236
+ if isinstance(content, list):
237
+ parts: list[str] = []
238
+ for item in content:
239
+ if isinstance(item, dict):
240
+ if item.get("type") == "text":
241
+ parts.append(str(item.get("text", "")))
242
+ elif "text" in item:
243
+ parts.append(str(item.get("text", "")))
244
+ else:
245
+ parts.append(str(item))
246
+ return _clean_text(" ".join(parts))
247
+ return _clean_text(str(content))
248
+
249
+
250
+ def _last_human_text(messages) -> str:
251
+ for message in reversed(messages):
252
+ if isinstance(message, HumanMessage):
253
+ return _message_text(message.content)
254
+ return ""
255
+
256
+
257
+ def _previous_ai_text(messages) -> str:
258
+ seen_human = False
259
+ for message in reversed(messages):
260
+ if isinstance(message, HumanMessage) and not seen_human:
261
+ seen_human = True
262
+ continue
263
+ if seen_human and isinstance(message, AIMessage):
264
+ return _message_text(message.content)
265
+ return ""
266
+
267
+
268
+ def _has_tool_calls(message: AIMessage) -> bool:
269
+ tool_calls = getattr(message, "tool_calls", None)
270
+ if tool_calls:
271
+ return True
272
+ additional_kwargs = getattr(message, "additional_kwargs", {}) or {}
273
+ return bool(additional_kwargs.get("tool_calls"))
274
+
275
+
276
+ def _looks_like_tool_turn(text: str) -> bool:
277
+ lowered = _clean_text(text).lower()
278
+ if not lowered:
279
+ return False
280
+ return any(token.lower() in lowered for token in TOOL_INTENT_WORDS) or any(
281
+ token.lower() in lowered for token in BOOKING_CONFIRM_WORDS
282
+ )
283
+
284
+
285
  def send_sms(to_number: str, message: str) -> None:
286
  client = Client(os.getenv("TWILIO_ACCOUNT_SID"), os.getenv("TWILIO_AUTH_TOKEN"))
287
  client.messages.create(
 
580
  patient_age: Age of the patient (e.g. "32").
581
  patient_num: Contact phone number of the patient.
582
  visiting_date: Date of visit in YYYY-MM-DD format or natural text.
583
+ patient_mail: Optional mail address for confirmation mail.
584
  """
585
  db_path = get_db_path()
586
  patient_num = format_bd_number(patient_num)
 
589
  doctor_name = _clean_text(doctor_name)
590
  category = _clean_text(category)
591
  visiting_date = _clean_text(visiting_date)
592
+ patient_mail = _clean_text(patient_mail)
593
  parsed_date = _parse_visit_date(visiting_date)
594
  if parsed_date:
595
  visiting_date = parsed_date
596
 
597
+ if not patient_name or not patient_age or not patient_num or not visiting_date:
598
  return (
599
  "Missing booking details. Need patient name, age, phone number, "
600
+ "and visit date."
601
  )
602
 
603
  async with aiosqlite.connect(db_path) as db:
 
652
  )
653
  await db.commit()
654
 
655
+ # Mail confirmation is optional. Book first, then try to send email if available.
656
  mail_message = (
657
  f"Doctor : {doctor_name}\n"
658
  f"Patient : {patient_name}\n"
 
660
  f"Visit Time : {visiting_time}\n"
661
  f"Please arrive on time."
662
  )
663
+ mail_status = ""
664
+ if patient_mail:
665
+ try:
666
+ await send_mail(
667
+ to_mail=patient_mail,
668
+ subject="✅ Appointment Confirmed!",
669
+ body=mail_message,
670
+ )
671
+ mail_status = "\n📧 Mail confirmation sent."
672
+ except Exception as e:
673
+ mail_status = f"\n⚠️ Mail failed: {str(e)}"
674
+ else:
675
+ mail_status = "\n📧 No email provided, so email confirmation was skipped."
676
 
677
  return (
678
  f"✅ Appointment Booked!\n"
 
704
  if row:
705
  doctor_name = row["doctor_name"]
706
 
707
+ if doctor_name:
708
+ cursor = await db.execute(
709
+ """SELECT * FROM patients
710
+ WHERE patient_num = ? AND LOWER(doctor_name) = LOWER(?)""",
711
+ (patient_num, doctor_name),
712
+ )
713
+ row = await cursor.fetchone()
714
+ if not row:
715
+ return json.dumps({"success": False, "message": "No matching appointment found."})
716
 
717
+ await db.execute(
718
+ """DELETE FROM patients
719
+ WHERE patient_num = ? AND LOWER(doctor_name) = LOWER(?)""",
720
+ (patient_num, doctor_name),
721
+ )
722
+ await db.commit()
723
+ else:
724
+ cursor = await db.execute(
725
+ """SELECT * FROM patients
726
+ WHERE patient_num = ?
727
+ ORDER BY visiting_date ASC, id ASC""",
728
+ (patient_num,),
729
+ )
730
+ rows = await cursor.fetchall()
731
+ if not rows:
732
+ return json.dumps({"success": False, "message": "No matching appointment found."})
733
+ if len(rows) > 1:
734
+ return json.dumps({
735
+ "success": False,
736
+ "message": "Multiple appointments found. Please specify the doctor name to cancel.",
737
+ "count": len(rows),
738
+ "data": [dict(row) for row in rows],
739
+ }, ensure_ascii=False)
740
+
741
+ await db.execute(
742
+ """DELETE FROM patients
743
+ WHERE id = ?""",
744
+ (rows[0]["id"],),
745
+ )
746
+ await db.commit()
747
+ doctor_name = rows[0]["doctor_name"] or doctor_name
748
 
749
  return json.dumps({
750
  "success": True,
 
782
  - Use `book_appointment` only after identifying the doctor and required patient details.
783
  - Never invent `doctor_id`. Get it from tool results or resolve by doctor_name/category.
784
  - If the user gives a Bangla date like "আগামীকাল" or "পরশু", convert it to a real date before booking.
785
+ - Do not block booking on email. If email is missing, book the appointment and skip email confirmation.
786
+ - If the user already provided name, age, phone, and date and then confirms, call `book_appointment` immediately.
787
+ - If the user asks to cancel and only gives a phone number, cancel the single matching appointment if there is exactly one.
788
 
789
  LANGUAGE RULE:
790
  - Respond in the user’s language.
 
809
  "Use this memory for continuity. Do not repeat it unless asked."
810
  )
811
 
812
+ FORCED_TOOL_SYSTEM = """
813
+ The previous assistant turn failed to use a tool even though the user intent is clear.
814
+
815
+ You must now choose the correct tool instead of answering in prose:
816
+ - Use `find_doctors` or `search_doctor` for doctor/specialty/availability questions.
817
+ - Use `get_doctors_by_day` or `get_categories_by_day` for day-based availability.
818
+ - Use `book_appointment` when the user is confirming a booking.
819
+ - Use `delete_appointment` when the user is cancelling a booking.
820
+
821
+ Important booking rules:
822
+ - Email is optional. If the user did not provide email, book without it.
823
+ - If the user already gave name, age, phone, and visit date, do not ask for those again.
824
+ - If the user has already confirmed the details, book immediately.
825
+
826
+ Important cancellation rules:
827
+ - If the user gave only a phone number and there is exactly one matching appointment, cancel it directly.
828
+ - If multiple appointments match, ask only for the doctor name.
829
+
830
+ Do not give a normal conversational answer before the tool call.
831
+ """
832
+
833
 
834
  # ═══════════════════════════════════════════════════════════════════════════════
835
  # AGENT
 
934
  "messages": [RemoveMessage(id=m.id) for m in messages[:-2]],
935
  }
936
 
937
+ def _should_retry_tool_call(self, state: ChatState, response: AIMessage) -> bool:
938
+ if _has_tool_calls(response):
939
+ return False
940
+
941
+ messages = state["messages"]
942
+ latest_user = _last_human_text(messages)
943
+ previous_ai = _previous_ai_text(messages)
944
+
945
+ if not _looks_like_tool_turn(latest_user):
946
+ return False
947
+
948
+ previous_ai_lower = previous_ai.lower()
949
+ booking_clues = (
950
+ "name", "patient", "age", "phone", "email", "নাম", "বয়স", "ফোন", "ইমেইল",
951
+ )
952
+ cancellation_clues = (
953
+ "cancel", "বাতিল", "delete", "cancel করার", "কেনসেল", "appointment", "অ্যাপয়েন্ট",
954
+ )
955
+
956
+ if any(clue in latest_user.lower() for clue in booking_clues):
957
+ return True
958
+ if any(clue in previous_ai_lower for clue in booking_clues):
959
+ return True
960
+ if any(clue in latest_user.lower() for clue in cancellation_clues):
961
+ return True
962
+ if any(clue in previous_ai_lower for clue in cancellation_clues):
963
+ return True
964
+
965
+ return True
966
+
967
  # ── Chat node ──────────────────────────────────────────────────────────────
968
  async def chat_node(self, state: ChatState):
969
  """
 
989
 
990
  response = await self.llm_with_tools.ainvoke(full_messages)
991
 
992
+ if self._should_retry_tool_call(state, response):
993
+ retry_messages = full_messages + [
994
+ AIMessage(content=_message_text(response.content)),
995
+ SystemMessage(content=FORCED_TOOL_SYSTEM),
996
+ ]
997
+ retry_response = await self.llm_with_tools.ainvoke(retry_messages)
998
+ if _has_tool_calls(retry_response):
999
+ response = retry_response
1000
+
1001
  print(f"[AI]: {str(response.content)[:200]}")
1002
  print(">>>>>>>>>> CHAT NODE END <<<<<<<<<<")
1003
  return {"messages": [response]}
db_view/db.html CHANGED
@@ -121,7 +121,7 @@
121
  </div>
122
 
123
  <script>
124
- const API_BASE = "http://127.0.0.1:8000";
125
 
126
  function showTab(tabIndex) {
127
  const doctorsTab = document.getElementById('doctors-tab');
 
121
  </div>
122
 
123
  <script>
124
+ const API_BASE = new URL('/dbapi', window.location.origin).toString().replace(/\/$/, '');
125
 
126
  function showTab(tabIndex) {
127
  const doctorsTab = document.getElementById('doctors-tab');
db_view/dbapi.py CHANGED
@@ -10,6 +10,7 @@ from pydantic import BaseModel
10
  import aiosqlite
11
  import uvicorn
12
  import os
 
13
  from typing import List
14
 
15
  app = FastAPI(title="Doctor-Patient API")
@@ -25,7 +26,7 @@ app.add_middleware(
25
  # ===========================================================
26
 
27
  # Database connection
28
- DB_PATH = os.path.join(os.getcwd(), "core/daa.db")
29
 
30
  # Pydantic Models for Response
31
  class Doctor(BaseModel):
 
10
  import aiosqlite
11
  import uvicorn
12
  import os
13
+ from pathlib import Path
14
  from typing import List
15
 
16
  app = FastAPI(title="Doctor-Patient API")
 
26
  # ===========================================================
27
 
28
  # Database connection
29
+ DB_PATH = str(Path(__file__).resolve().parent.parent / "core" / "daa.db")
30
 
31
  # Pydantic Models for Response
32
  class Doctor(BaseModel):
frontend/script.js CHANGED
@@ -101,6 +101,7 @@ let brainRestartTimer = null;
101
  let brainAutoRestartTimer = null;
102
  let brainPendingAudio = null;
103
  let voicePendingPackets = [];
 
104
 
105
  // ─── Recording state ──────────────────────────────────────────────────────────
106
  let micStream = null;
@@ -319,6 +320,7 @@ function onVoiceMsg(ev) {
319
  aiTxt = '';
320
  _setCaption('');
321
  _brainSetSttBubble(msg.text);
 
322
  _brainModeSetSearch(true);
323
  appendThinking();
324
  setState('processing');
@@ -332,7 +334,8 @@ function onVoiceMsg(ev) {
332
  }
333
  _removeThinking();
334
  _setCaption(aiTxt + msg.token);
335
- _brainSetTtsBubble(aiTxt + msg.token);
 
336
  _brainModeSetSearch(true);
337
  if (!brainMode) {
338
  if (!aiEl) {
@@ -350,6 +353,7 @@ function onVoiceMsg(ev) {
350
  case 'end':
351
  _renderAiText(true);
352
  _removeThinking();
 
353
  aiEl = null;
354
  aiTxt = '';
355
  _setCaption('');
@@ -509,7 +513,7 @@ function _done() {
509
  isProcessing = false;
510
  isRecordingLocked = false;
511
  _brainModeSetSearch(false);
512
- _brainSetTtsBubble('', false);
513
  _inFlight = 0;
514
  _vizQ();
515
  micBtn.disabled = false;
@@ -1012,7 +1016,7 @@ function setBrainMode(on) {
1012
  if (voiceCaption) voiceCaption.textContent = '';
1013
  if (brainMode) {
1014
  brainBubbleSttText.textContent = 'Listening…';
1015
- brainBubbleTtsText.textContent = 'Waiting…';
1016
  brainVoiceActive = true;
1017
  sidebarEl.classList.add('collapsed');
1018
  sidebarToggle.textContent = '›';
@@ -1035,7 +1039,7 @@ function setBrainMode(on) {
1035
  sidebarToggle.textContent = '‹';
1036
  _brainModeSetSearch(false);
1037
  _brainSetSttBubble('');
1038
- _brainSetTtsBubble('');
1039
  }
1040
  }
1041
 
@@ -1055,8 +1059,8 @@ function _brainSetTtsBubble(text, active = true) {
1055
  if (!brainBubbleTts || !brainBubbleTtsText) return;
1056
  const value = (text || '').trim();
1057
  brainBubbleTtsText.textContent = value || 'Waiting…';
1058
- brainBubbleTts.classList.toggle('active', active || !!value);
1059
- brainBubbleTts.classList.toggle('speaking', active || !!value);
1060
  }
1061
 
1062
  function _brainResumeListening() {
 
101
  let brainAutoRestartTimer = null;
102
  let brainPendingAudio = null;
103
  let voicePendingPackets = [];
104
+ let brainLastResponse = '';
105
 
106
  // ─── Recording state ──────────────────────────────────────────────────────────
107
  let micStream = null;
 
320
  aiTxt = '';
321
  _setCaption('');
322
  _brainSetSttBubble(msg.text);
323
+ if (brainMode) _brainSetTtsBubble(brainLastResponse || '', false);
324
  _brainModeSetSearch(true);
325
  appendThinking();
326
  setState('processing');
 
334
  }
335
  _removeThinking();
336
  _setCaption(aiTxt + msg.token);
337
+ brainLastResponse = aiTxt + msg.token;
338
+ _brainSetTtsBubble(brainLastResponse);
339
  _brainModeSetSearch(true);
340
  if (!brainMode) {
341
  if (!aiEl) {
 
353
  case 'end':
354
  _renderAiText(true);
355
  _removeThinking();
356
+ if (brainMode) brainLastResponse = aiTxt || brainLastResponse;
357
  aiEl = null;
358
  aiTxt = '';
359
  _setCaption('');
 
513
  isProcessing = false;
514
  isRecordingLocked = false;
515
  _brainModeSetSearch(false);
516
+ _brainSetTtsBubble(brainLastResponse || '', false);
517
  _inFlight = 0;
518
  _vizQ();
519
  micBtn.disabled = false;
 
1016
  if (voiceCaption) voiceCaption.textContent = '';
1017
  if (brainMode) {
1018
  brainBubbleSttText.textContent = 'Listening…';
1019
+ brainBubbleTtsText.textContent = brainLastResponse || 'Waiting…';
1020
  brainVoiceActive = true;
1021
  sidebarEl.classList.add('collapsed');
1022
  sidebarToggle.textContent = '›';
 
1039
  sidebarToggle.textContent = '‹';
1040
  _brainModeSetSearch(false);
1041
  _brainSetSttBubble('');
1042
+ _brainSetTtsBubble('', false);
1043
  }
1044
  }
1045
 
 
1059
  if (!brainBubbleTts || !brainBubbleTtsText) return;
1060
  const value = (text || '').trim();
1061
  brainBubbleTtsText.textContent = value || 'Waiting…';
1062
+ brainBubbleTts.classList.toggle('active', !!value || !!active);
1063
+ brainBubbleTts.classList.toggle('speaking', !!active);
1064
  }
1065
 
1066
  function _brainResumeListening() {
frontend/style.css CHANGED
@@ -653,8 +653,8 @@ button {
653
  /* ── Brain mode ───────────────────────────────────────────────────────────── */
654
  .brain-stage {
655
  display: none;
656
- width: min(var(--max-chat), calc(100% - 32px));
657
- margin: 16px auto 0;
658
  flex: 1;
659
  align-items: center;
660
  justify-content: center;
@@ -663,13 +663,13 @@ button {
663
 
664
  .brain-shell {
665
  position: relative;
666
- width: min(760px, 100%);
667
- height: min(62vh, 620px);
668
- border-radius: 32px;
669
  background:
670
- radial-gradient(circle at 50% 45%, rgba(100, 181, 255, 0.18), transparent 18%),
671
- radial-gradient(circle at 50% 55%, rgba(139, 92, 246, 0.12), transparent 38%),
672
- radial-gradient(circle at 50% 50%, rgba(15, 23, 42, 0.92), rgba(5, 7, 13, 0.98) 72%);
673
  border: 1px solid rgba(255, 255, 255, 0.08);
674
  box-shadow: var(--shadow);
675
  overflow: hidden;
@@ -680,21 +680,23 @@ button {
680
  inset: 0;
681
  z-index: 3;
682
  pointer-events: none;
 
683
  }
684
 
685
  .brain-bubble {
686
  position: absolute;
687
- width: min(280px, 42vw);
688
- max-width: 320px;
689
- min-height: 96px;
690
- padding: 14px 16px;
691
- border-radius: 22px;
692
  border: 1px solid rgba(255, 255, 255, 0.08);
693
- background: rgba(8, 12, 20, 0.82);
 
 
694
  backdrop-filter: blur(18px);
695
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
696
  transform: translateY(8px) scale(0.96);
697
- opacity: 0.72;
698
  transition: transform 0.25s ease, opacity 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
699
  }
700
 
@@ -719,8 +721,8 @@ button {
719
  }
720
 
721
  .brain-bubble-text {
722
- font-size: 15px;
723
- line-height: 1.55;
724
  color: var(--text);
725
  min-height: 40px;
726
  white-space: pre-wrap;
@@ -728,13 +730,13 @@ button {
728
  }
729
 
730
  .brain-bubble-stt {
731
- left: 24px;
732
- top: 24px;
733
  }
734
 
735
  .brain-bubble-tts {
736
- right: 24px;
737
- bottom: 24px;
738
  }
739
 
740
  .brain-bubble.active {
@@ -1064,6 +1066,10 @@ body.brain-mode .brain-shell {
1064
  inset 0 0 0 1px rgba(255, 255, 255, 0.03);
1065
  }
1066
 
 
 
 
 
1067
  body.brain-mode .brain-pulse {
1068
  border-color: rgba(100, 181, 255, 0.35);
1069
  }
@@ -1128,7 +1134,7 @@ body.brain-mode .brain-stage[data-state='speaking'] .brain-node {
1128
  }
1129
 
1130
  .brain-shell {
1131
- height: min(56vh, 520px);
1132
  }
1133
  }
1134
 
@@ -1176,7 +1182,12 @@ body.brain-mode .brain-stage[data-state='speaking'] .brain-node {
1176
  }
1177
 
1178
  .brain-shell {
1179
- height: 50vh;
1180
- border-radius: 24px;
 
 
 
 
 
1181
  }
1182
  }
 
653
  /* ── Brain mode ───────────────────────────────────────────────────────────── */
654
  .brain-stage {
655
  display: none;
656
+ width: min(560px, calc(100% - 24px));
657
+ margin: 14px auto 0;
658
  flex: 1;
659
  align-items: center;
660
  justify-content: center;
 
663
 
664
  .brain-shell {
665
  position: relative;
666
+ width: min(560px, 100%);
667
+ height: min(74vh, 760px);
668
+ border-radius: 40px;
669
  background:
670
+ radial-gradient(circle at 50% 40%, rgba(100, 181, 255, 0.18), transparent 20%),
671
+ radial-gradient(circle at 50% 62%, rgba(139, 92, 246, 0.14), transparent 38%),
672
+ radial-gradient(circle at 50% 50%, rgba(15, 23, 42, 0.96), rgba(5, 7, 13, 1) 74%);
673
  border: 1px solid rgba(255, 255, 255, 0.08);
674
  box-shadow: var(--shadow);
675
  overflow: hidden;
 
680
  inset: 0;
681
  z-index: 3;
682
  pointer-events: none;
683
+ padding: 18px;
684
  }
685
 
686
  .brain-bubble {
687
  position: absolute;
688
+ width: min(320px, calc(100% - 36px));
689
+ min-height: 102px;
690
+ padding: 16px 18px;
691
+ border-radius: 24px;
 
692
  border: 1px solid rgba(255, 255, 255, 0.08);
693
+ background:
694
+ linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03)),
695
+ rgba(8, 12, 20, 0.82);
696
  backdrop-filter: blur(18px);
697
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
698
  transform: translateY(8px) scale(0.96);
699
+ opacity: 0.78;
700
  transition: transform 0.25s ease, opacity 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
701
  }
702
 
 
721
  }
722
 
723
  .brain-bubble-text {
724
+ font-size: 16px;
725
+ line-height: 1.6;
726
  color: var(--text);
727
  min-height: 40px;
728
  white-space: pre-wrap;
 
730
  }
731
 
732
  .brain-bubble-stt {
733
+ left: 18px;
734
+ top: 18px;
735
  }
736
 
737
  .brain-bubble-tts {
738
+ right: 18px;
739
+ bottom: 18px;
740
  }
741
 
742
  .brain-bubble.active {
 
1066
  inset 0 0 0 1px rgba(255, 255, 255, 0.03);
1067
  }
1068
 
1069
+ body.brain-mode .brain-bubble.active {
1070
+ transform: translateY(0) scale(1);
1071
+ }
1072
+
1073
  body.brain-mode .brain-pulse {
1074
  border-color: rgba(100, 181, 255, 0.35);
1075
  }
 
1134
  }
1135
 
1136
  .brain-shell {
1137
+ height: min(68vh, 620px);
1138
  }
1139
  }
1140
 
 
1182
  }
1183
 
1184
  .brain-shell {
1185
+ width: 100%;
1186
+ height: 64vh;
1187
+ border-radius: 28px;
1188
+ }
1189
+
1190
+ .brain-bubble {
1191
+ width: calc(100% - 30px);
1192
  }
1193
  }
services/tts.py CHANGED
@@ -16,7 +16,7 @@ import os, re, asyncio
16
 
17
  load_dotenv()
18
 
19
- USE_ELEVENLABS = True
20
  EDGE_VOICE = "bn-BD-NabanitaNeural"
21
  ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY", "")
22
  ELEVENLABS_VOICE_ID = os.getenv("ELEVENLABS_VOICE_ID", "21m00Tcm4TlvDq8ikWAM")
 
16
 
17
  load_dotenv()
18
 
19
+ USE_ELEVENLABS = False
20
  EDGE_VOICE = "bn-BD-NabanitaNeural"
21
  ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY", "")
22
  ELEVENLABS_VOICE_ID = os.getenv("ELEVENLABS_VOICE_ID", "21m00Tcm4TlvDq8ikWAM")