rakib72642 commited on
Commit
f2ea5fc
·
1 Parent(s): ccadc4d

project init

Browse files
Files changed (11) hide show
  1. .env +17 -0
  2. .gitattributes +31 -24
  3. .gitignore +11 -0
  4. app.py +95 -0
  5. core/backend.py +433 -0
  6. frontend/index.html +47 -0
  7. frontend/script.js +166 -0
  8. frontend/style.css +152 -0
  9. requirements.txt +35 -0
  10. services/stt.py +73 -0
  11. services/tts.py +12 -0
.env ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ HF_TOKEN=""
2
+ WEATHER_API_KEY="9e50616b95574a30dbc5a01579aa2b9f"
3
+ LANGCHAIN_TRACING_V2=true
4
+ LANGCHAIN_ENDPOINT='https://api.smith.langchain.com'
5
+ LANGCHAIN_API_KEY='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'
6
+ LANGCHAIN_PROJECT='Default'
7
+
8
+ TWILIO_ACCOUNT_SID="ACfafc0d2d007bdf14b21bb3e14a7a7b31"
9
+ TWILIO_AUTH_TOKEN="ed15fa98748c8c3d3d02cb54e431a187"
10
+ TWILIO_PHONE_NUMBER="+14343375085"
11
+
12
+ LIVEKIT_URL=wss://demo-wqwzjgsv.livekit.cloud
13
+ LIVEKIT_API_KEY=APIesfzMFdhmrb6
14
+ LIVEKIT_API_SECRET=kb7jLghH6Q3qLXxUHoYwREpYJdgX8qgAOHBDOG7q40G
15
+
16
+ GROQ_API_KEY=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
17
+ CARTESIA_API_KEY=sk_car_h3oyy6jPSJzx8KnEGJ1m5f
.gitattributes CHANGED
@@ -1,35 +1,42 @@
 
1
  *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
 
 
 
 
4
  *.bz2 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
5
  *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
  *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
 
 
 
14
  *.npy filter=lfs diff=lfs merge=lfs -text
15
  *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
  *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
 
 
20
  *.pickle filter=lfs diff=lfs merge=lfs -text
21
  *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
  *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ # ===== Archives =====
2
  *.7z filter=lfs diff=lfs merge=lfs -text
3
+ *.rar filter=lfs diff=lfs merge=lfs -text
4
+ *.zip filter=lfs diff=lfs merge=lfs -text
5
+ *.tar filter=lfs diff=lfs merge=lfs -text
6
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
7
+ *.tgz filter=lfs diff=lfs merge=lfs -text
8
+ *.gz filter=lfs diff=lfs merge=lfs -text
9
  *.bz2 filter=lfs diff=lfs merge=lfs -text
10
+ *.xz filter=lfs diff=lfs merge=lfs -text
11
+ *.zst filter=lfs diff=lfs merge=lfs -text
12
+
13
+ # ===== Machine Learning / Model Files =====
14
+ *.pt filter=lfs diff=lfs merge=lfs -text
15
+ *.pth filter=lfs diff=lfs merge=lfs -text
16
  *.ckpt filter=lfs diff=lfs merge=lfs -text
17
+ *.onnx filter=lfs diff=lfs merge=lfs -text
18
+ *.pb filter=lfs diff=lfs merge=lfs -text
19
+ *.tflite filter=lfs diff=lfs merge=lfs -text
20
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
 
 
21
  *.model filter=lfs diff=lfs merge=lfs -text
22
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
23
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
24
+
25
+ # ===== Data / Arrays / Artifacts =====
26
  *.npy filter=lfs diff=lfs merge=lfs -text
27
  *.npz filter=lfs diff=lfs merge=lfs -text
 
 
28
  *.parquet filter=lfs diff=lfs merge=lfs -text
29
+ *.arrow filter=lfs diff=lfs merge=lfs -text
30
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
31
+ *.joblib filter=lfs diff=lfs merge=lfs -text
32
  *.pickle filter=lfs diff=lfs merge=lfs -text
33
  *.pkl filter=lfs diff=lfs merge=lfs -text
34
+
35
+ # ===== Large Binary / Misc =====
36
+ *.bin filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
37
  *.wasm filter=lfs diff=lfs merge=lfs -text
38
+ *.ot filter=lfs diff=lfs merge=lfs -text
39
+ *.ftz filter=lfs diff=lfs merge=lfs -text
40
+
41
+ # ===== TensorFlow / Training Logs =====
42
  *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Database files (SQLite temp/journal files)
2
+ *.db
3
+ *.db-shm
4
+ *.db-wal
5
+
6
+ # Python bytecode
7
+ *.pyc
8
+ **/__pycache__/
9
+
10
+ # Binary files
11
+ *.bin
app.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.responses import StreamingResponse
3
+ from contextlib import asynccontextmanager
4
+ from pydantic import BaseModel
5
+ from core.backend import AIBackend
6
+ import uvicorn, json, os
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
9
+ from services.stt import StreamingSTT
10
+ from services.tts import text_to_speech_stream
11
+ from fastapi.staticfiles import StaticFiles
12
+
13
+
14
+ chatbot_obj = AIBackend()
15
+
16
+ @asynccontextmanager
17
+ async def lifespan(app: FastAPI):
18
+ await chatbot_obj.async_setup()
19
+ yield
20
+ if chatbot_obj.conn:
21
+ await chatbot_obj.conn.close()
22
+
23
+ app = FastAPI(lifespan=lifespan)
24
+
25
+ class UserRequest(BaseModel):
26
+ user_id: str
27
+ user_query: str
28
+
29
+ @app.post("/chat")
30
+ async def chat(request: UserRequest):
31
+ stream = await chatbot_obj.main(
32
+ user_id=request.user_id,
33
+ user_query=request.user_query,
34
+ )
35
+ return StreamingResponse(stream, media_type="text/event-stream")
36
+
37
+ @app.websocket("/ws/chat")
38
+ async def websocket_chat(websocket: WebSocket):
39
+ await websocket.accept()
40
+ try:
41
+ while True:
42
+ # receive frontend message
43
+ data = await websocket.receive_text()
44
+ payload = json.loads(data)
45
+
46
+ user_id = payload["user_id"]
47
+ user_query = payload["user_query"]
48
+
49
+ # stream AI response
50
+ stream = await chatbot_obj.main(
51
+ user_id=user_id,
52
+ user_query=user_query
53
+ )
54
+
55
+ async for chunk in stream:
56
+ await websocket.send_text(chunk)
57
+
58
+ # notify frontend response finished
59
+ await websocket.send_text("[[END]]")
60
+ except WebSocketDisconnect:
61
+ print("Client disconnected")
62
+
63
+ @app.websocket("/ws/voice")
64
+ async def voice_ws(websocket: WebSocket):
65
+ await websocket.accept()
66
+ stt = StreamingSTT()
67
+ try:
68
+ while True:
69
+ message = await websocket.receive()
70
+ # 🎤 AUDIO INPUT
71
+ if "bytes" in message:
72
+ audio_chunk = message["bytes"]
73
+ stt.add_audio(audio_chunk)
74
+ text = stt.transcribe_if_ready()
75
+ if not text:
76
+ continue
77
+ await websocket.send_text(f"[STT]: {text}")
78
+ # 🤖 LLM STREAM
79
+ stream = chatbot_obj.main(
80
+ user_id="voice_user",
81
+ user_query=text
82
+ )
83
+ full_response = ""
84
+ async for token in stream:
85
+ full_response += token
86
+ await websocket.send_text(f"[LLM]: {token}")
87
+ # 🔊 TTS STREAM
88
+ async for audio_chunk in text_to_speech_stream(full_response):
89
+ await websocket.send_bytes(audio_chunk)
90
+ await websocket.send_text("[END]")
91
+ except WebSocketDisconnect:
92
+ print("Voice client disconnected")
93
+
94
+ if __name__ == "__main__":
95
+ uvicorn.run("app:app", host="127.0.0.1", port=8679, reload=True)
core/backend.py ADDED
@@ -0,0 +1,433 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.graph import StateGraph, START, END
2
+ from typing import TypedDict, Annotated
3
+ from langchain_core.messages import BaseMessage
4
+ from langgraph.graph.message import add_messages
5
+ from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
6
+ from langchain_ollama import ChatOllama
7
+ from langgraph.prebuilt import ToolNode, tools_condition
8
+ from langchain_community.tools import DuckDuckGoSearchRun
9
+ from langchain_core.tools import tool
10
+ from langchain_core.messages import HumanMessage, AIMessage, ToolMessage, RemoveMessage, SystemMessage
11
+ import aiosqlite, uuid, os, httpx, asyncio
12
+ from twilio.rest import Client
13
+ from dotenv import load_dotenv
14
+ import json, pytz
15
+ from datetime import datetime
16
+
17
+ ######################### STATE #########################
18
+ class ChatState(TypedDict):
19
+ messages: Annotated[list[BaseMessage], add_messages]
20
+ summary: str
21
+
22
+ ######################### TOOLS #########################
23
+ def get_db_path():
24
+ return os.path.join(os.path.dirname(__file__), "daa.db")
25
+
26
+ def send_sms(to_number: str, message: str):
27
+ client = Client(os.getenv("TWILIO_ACCOUNT_SID"), os.getenv("TWILIO_AUTH_TOKEN"))
28
+ client.messages.create(
29
+ body=message,
30
+ from_=os.getenv("TWILIO_PHONE_NUMBER"),
31
+ to=to_number
32
+ )
33
+
34
+ def format_bd_number(num: str) -> str:
35
+ num = num.strip().replace(" ", "")
36
+ if num.startswith("01") and len(num) == 11:
37
+ return "+88" + num
38
+ if num.startswith("8801"):
39
+ return "+" + num
40
+ return num # already formatted or unknown
41
+
42
+ @tool
43
+ def get_bd_time() -> str:
44
+ """
45
+ Get current Bangladesh time (Asia/Dhaka) with weekday name
46
+ """
47
+ tz = pytz.timezone("Asia/Dhaka")
48
+ now = datetime.now(tz)
49
+ return now.strftime("%Y-%m-%d %H:%M:%S (%A, Bangladesh Time)")
50
+
51
+ @tool
52
+ async def search_doctor(name: str = "", category: str = "", visiting_days: str = "") -> str:
53
+ """
54
+ Search doctors by name, category, or visiting_days from SQLite database.
55
+ Any combination of filters is supported (OR logic for each field).
56
+ """
57
+ db_path = get_db_path()
58
+ query = "SELECT * FROM doctors WHERE 1=1"
59
+ params = []
60
+ conditions = []
61
+
62
+ if name:
63
+ conditions.append("LOWER(doctor_name) LIKE ?")
64
+ params.append(f"%{name.lower()}%")
65
+
66
+ if category:
67
+ conditions.append("LOWER(category) LIKE ?")
68
+ params.append(f"%{category.lower()}%")
69
+
70
+ if visiting_days:
71
+ conditions.append("LOWER(visiting_days) LIKE ?")
72
+ params.append(f"%{visiting_days.lower()}%")
73
+
74
+ if conditions:
75
+ query += " AND (" + " OR ".join(conditions) + ")"
76
+
77
+ async with aiosqlite.connect(db_path) as db:
78
+ db.row_factory = aiosqlite.Row
79
+ cursor = await db.execute(query, params)
80
+ rows = await cursor.fetchall()
81
+
82
+ if not rows:
83
+ return json.dumps({
84
+ "success": False,
85
+ "message": "No doctors found matching your search.",
86
+ "data": []
87
+ })
88
+
89
+ return json.dumps({
90
+ "success": True,
91
+ "count": len(rows),
92
+ "data": [dict(r) for r in rows]
93
+ })
94
+
95
+ @tool
96
+ async def search_appointment_by_phone(patient_num: str) -> str:
97
+ """
98
+ Search all appointments using patient phone number.
99
+ """
100
+ db_path = get_db_path()
101
+ patient_num = format_bd_number(patient_num)
102
+
103
+ async with aiosqlite.connect(db_path) as db:
104
+ db.row_factory = aiosqlite.Row
105
+
106
+ cursor = await db.execute("""
107
+ SELECT * FROM patients
108
+ WHERE patient_num = ?
109
+ ORDER BY visiting_date ASC
110
+ """, (patient_num,))
111
+
112
+ rows = await cursor.fetchall()
113
+
114
+ if not rows:
115
+ return json.dumps({
116
+ "success": False,
117
+ "message": "No appointments found for this phone number.",
118
+ "data": []
119
+ })
120
+
121
+ return json.dumps({
122
+ "success": True,
123
+ "count": len(rows),
124
+ "data": [dict(r) for r in rows]
125
+ })
126
+
127
+ @tool
128
+ async def book_appointment(doctor_id: int, patient_name: str, patient_age: str, patient_num: str, visiting_date: str) -> str:
129
+ """
130
+ Book a doctor appointment and save it to the patients table.
131
+
132
+ Args:
133
+ doctor_id: Doctor's ID from search_doctor results.
134
+ patient_name: Full name of the patient.
135
+ patient_age: Age of the patient (e.g. "32").
136
+ patient_num: Contact phone number of the patient.
137
+ visiting_date: Date of visit in YYYY-MM-DD format (e.g. 2025-06-15).
138
+
139
+ Returns a booking confirmation with the new record ID.
140
+ """
141
+ db_path = get_db_path()
142
+
143
+ async with aiosqlite.connect(db_path) as db:
144
+ db.row_factory = aiosqlite.Row
145
+ patient_num = format_bd_number(patient_num)
146
+
147
+ # Verify doctor exists
148
+ cursor = await db.execute("SELECT * FROM doctors WHERE id = ?", (doctor_id,))
149
+ doctor = await cursor.fetchone()
150
+ if not doctor:
151
+ return f"No doctor found with ID {doctor_id}. Please search for a doctor first."
152
+
153
+ doctor_data = dict(doctor)
154
+ doctor_name = doctor_data.get("doctor_name", "Unknown")
155
+ doctor_category = doctor_data.get("doctor_category", "Unknown")
156
+
157
+ # Check for conflicting booking (same doctor + same date)
158
+ cursor = await db.execute(
159
+ """SELECT id FROM patients
160
+ WHERE doctor_name = ? AND visiting_date = ? AND patient_num = ?""",
161
+ (doctor_name, visiting_date, patient_num),
162
+ )
163
+ conflict = await cursor.fetchone()
164
+ if conflict:
165
+ return (
166
+ f"A booking for {patient_name} with Dr. {doctor_name} "
167
+ f"on {visiting_date} already exists."
168
+ )
169
+
170
+ # Insert into patients table
171
+ cursor = await db.execute(
172
+ """INSERT INTO patients (doctor_name, doctor_category, patient_name, patient_age, patient_num, visiting_date)
173
+ VALUES (?, ?, ?, ?, ?, ?)""",
174
+ (doctor_name, doctor_category, patient_name, patient_age, patient_num, visiting_date),
175
+ )
176
+ await db.commit()
177
+
178
+ # Send SMS confirmation
179
+ sms_message = (
180
+ f"✅ Appointment Confirmed!\n"
181
+ f"Doctor : {doctor_name}\n"
182
+ f"Patient : {patient_name}\n"
183
+ f"Visit Date : {visiting_date}\n"
184
+ f"Please arrive 10 minutes early."
185
+ )
186
+ # try:
187
+ # send_sms(to_number=patient_num, message=sms_message)
188
+ # sms_status = "📱 SMS confirmation sent."
189
+ # except Exception as e:
190
+ # sms_status = f"⚠️ SMS failed: {str(e)}"
191
+
192
+ return (
193
+ f"✅ Appointment Booked!\n"
194
+ f"━━━━━━━━━━━━━━━━━━━━━━\n"
195
+ f"Doctor : {doctor_name}\n"
196
+ f"Patient : {patient_name}\n"
197
+ f"Age : {patient_age}\n"
198
+ f"Date : {visiting_date}\n"
199
+ f"Contact : {patient_num}\n"
200
+ f"━━━━━━━━━━━━━━━━━━━━━━\n"
201
+ f"Please arrive 10 minutes early."
202
+ # f"{sms_status}"
203
+ )
204
+
205
+ async def delete_appointment(patient_num: str, doctor_name: str) -> str:
206
+ """
207
+ Delete an appointment using patient phone number and doctor name.
208
+ """
209
+ db_path = get_db_path()
210
+ # normalize phone number
211
+ patient_num = format_bd_number(patient_num)
212
+
213
+ async with aiosqlite.connect(db_path) as db:
214
+ db.row_factory = aiosqlite.Row
215
+
216
+ # check if appointment exists first
217
+ cursor = await db.execute("""
218
+ SELECT * FROM patients
219
+ WHERE patient_num = ?
220
+ AND LOWER(doctor_name) = LOWER(?)
221
+ """, (patient_num, doctor_name))
222
+
223
+ row = await cursor.fetchone()
224
+ if not row:
225
+ return json.dumps({
226
+ "success": False,
227
+ "message": "No matching appointment found to delete."
228
+ })
229
+
230
+ # delete appointment
231
+ await db.execute("""
232
+ DELETE FROM patients
233
+ WHERE patient_num = ?
234
+ AND LOWER(doctor_name) = LOWER(?)
235
+ """, (patient_num, doctor_name))
236
+
237
+ await db.commit()
238
+
239
+ return json.dumps({
240
+ "success": True,
241
+ "message": f"Appointment with Dr. {doctor_name} deleted successfully."
242
+ })
243
+
244
+ ######################### MAIN AGENT CLASS #########################
245
+ class AIBackend:
246
+ def __init__(self):
247
+ load_dotenv()
248
+ os.environ["LANGCHAIN_PROJECT"] = "Doctor Appointment Automation"
249
+ self.llm = ChatOllama(model="gemma4:e4b", streaming=True) # qwen2.5:3b, gemma4:e4b
250
+ self.tools = [search_doctor, book_appointment, get_bd_time, search_appointment_by_phone, delete_appointment]
251
+ self.tool_node = ToolNode(self.tools)
252
+ self.llm_with_tools = self.llm.bind_tools(self.tools)
253
+
254
+ async def async_setup(self):
255
+ db_path = os.path.join(os.path.dirname(__file__), "daa.db")
256
+ self.conn = await aiosqlite.connect(db_path)
257
+ self.checkpointer = AsyncSqliteSaver(self.conn)
258
+ await self._create_user_table()
259
+ self.graph = self._build_graph()
260
+ self.summary_graph = self._build_summary_graph()
261
+
262
+ async def _create_user_table(self):
263
+ await self.conn.execute("""
264
+ CREATE TABLE IF NOT EXISTS userid_threadid (
265
+ userId TEXT UNIQUE NOT NULL,
266
+ threadId TEXT UNIQUE NOT NULL
267
+ )
268
+ """)
269
+ await self.conn.commit()
270
+
271
+ ######################### SUMMARIZE NODE #########################
272
+ async def summarize_conversation(self, state: ChatState):
273
+ existing_summary = state.get("summary", "")
274
+ messages = state["messages"]
275
+ prompt = (
276
+ f"""
277
+ You are maintaining a long-term conversation memory for a chatbot.
278
+
279
+ Existing summary:
280
+ {existing_summary}
281
+
282
+ Update and extend the summary using ONLY the new conversation messages above.
283
+
284
+ Instructions:
285
+ - Preserve important existing context.
286
+ - Add new facts, decisions, preferences, goals, issues, and ongoing tasks.
287
+ - Keep technical details concise but meaningful.
288
+ - Track unresolved problems or follow-up actions.
289
+ - Avoid repetition and remove outdated or redundant information when appropriate.
290
+ - Maintain chronological consistency.
291
+ - Write the summary in clear bullet points.
292
+ - Focus on information useful for future conversations and contextual continuity.
293
+ - Do NOT include casual greetings or temporary small talk unless important.
294
+ - Keep the summary compact but information-dense.
295
+ """
296
+ if existing_summary
297
+ else
298
+ """
299
+ You are creating a long-term conversation memory summary for a chatbot.
300
+
301
+ Summarize the conversation above.
302
+
303
+ Instructions:
304
+ - Capture important user information, goals, preferences, projects, and decisions.
305
+ - Include technical issues, debugging progress, and solutions discussed.
306
+ - Track ongoing tasks or unresolved questions.
307
+ - Ignore casual greetings and low-value chatter.
308
+ - Write concise, structured bullet points.
309
+ - Keep the summary compact but highly informative for future context retention.
310
+ """
311
+ )
312
+ messages_for_summary = messages + [HumanMessage(content=prompt)]
313
+ response = await self.llm.ainvoke(messages_for_summary)
314
+ return {
315
+ "summary": response.content,
316
+ "messages": [RemoveMessage(id=m.id) for m in messages[:-2]],
317
+ }
318
+
319
+ async def should_summarize(self, state: ChatState):
320
+ if len(state["messages"]) > 10:
321
+ return "summarize_node"
322
+ return "chat_node"
323
+
324
+ ######################### CHAT NODE #########################
325
+ async def chat_node(self, state: ChatState):
326
+ summary = state.get("summary", "")
327
+ messages = state["messages"]
328
+
329
+ print('#'*50)
330
+ print(">>>>>>>>>> CHAT NODE START <<<<<<<<<<")
331
+ if summary:
332
+ print(f"[SUMMARY]:\n{summary}\n")
333
+ else:
334
+ print("[NO SUMMARY YET]\n")
335
+
336
+ print('$'*50)
337
+ print("[MESSAGES]:")
338
+ for m in messages:
339
+ role = m.__class__.__name__
340
+ print(f" [{role}]: {m.content[:200]}")
341
+ print('$'*50,'\n')
342
+
343
+ if summary:
344
+ summary_message = SystemMessage(
345
+ content=(
346
+ "You are provided with a condensed memory of previous conversations.\n\n"
347
+ f"Conversation Memory:\n{summary}\n\n"
348
+ "Instructions:\n"
349
+ "- Use this memory as long-term conversational context.\n"
350
+ "- Maintain continuity with the user's previous discussions, projects, goals, and preferences.\n"
351
+ "- Prioritize recent and relevant information when generating responses.\n"
352
+ "- Do not repeat the summary unless necessary.\n"
353
+ "- If new information conflicts with old memory, prefer the latest context.\n"
354
+ "- Use the memory naturally to improve personalization, reasoning, and follow-up responses.\n"
355
+ "- Treat unresolved issues, active projects, and pending tasks as ongoing unless stated otherwise."
356
+ )
357
+ )
358
+ messages = [summary_message] + messages
359
+ response = await self.llm_with_tools.ainvoke(messages)
360
+ print(f"Final [{response.__class__.__name__}]: {response.content[:200]}")
361
+ print(">>>>>>>>>> CHAT NODE END <<<<<<<<<<")
362
+ print('#'*50)
363
+ return {"messages": [response]}
364
+
365
+ ######################### GRAPH #########################
366
+ def _build_graph(self):
367
+ g = StateGraph(ChatState)
368
+ g.add_node("chat_node", self.chat_node)
369
+ g.add_node("tools", self.tool_node)
370
+
371
+ g.add_edge(START, "chat_node")
372
+ g.add_conditional_edges("chat_node", tools_condition)
373
+ g.add_edge("tools", "chat_node")
374
+
375
+ return g.compile(checkpointer=self.checkpointer)
376
+
377
+ def _build_summary_graph(self):
378
+ g = StateGraph(ChatState)
379
+ g.add_node("summarize_node", self.summarize_conversation)
380
+ g.add_edge(START, "summarize_node")
381
+ g.add_edge("summarize_node", END)
382
+ return g.compile(checkpointer=self.checkpointer)
383
+
384
+ ######################### STREAMING #########################
385
+ async def ai_only_stream(self, initial_state: dict, config: dict):
386
+ async for message_chunk, metadata in self.graph.astream(initial_state, config=config, stream_mode="messages"):
387
+ if isinstance(message_chunk, AIMessage) and message_chunk.content:
388
+ yield message_chunk.content
389
+
390
+ # Auto Summarization Execute
391
+ current_state = await self.graph.aget_state(config)
392
+ if len(current_state.values.get("messages", [])) > 10:
393
+ asyncio.create_task(
394
+ self.summary_graph.ainvoke(current_state.values, config=config)
395
+ )
396
+ print('@'*20,'Summarization Execute','@'*20)
397
+
398
+ ######################### THREAD ID #########################
399
+ @staticmethod
400
+ def generate_thread_id() -> str:
401
+ return str(uuid.uuid4())
402
+
403
+ ######################### RETRIEVE ALL THREADS #########################
404
+ async def retrieve_all_threads(self):
405
+ all_threads = set()
406
+ async for checkpoint in self.checkpointer.alist(None):
407
+ all_threads.add(checkpoint.config["configurable"]["thread_id"])
408
+ return list(all_threads)
409
+
410
+ ######################### MAIN ENTRY POINT #########################
411
+ async def main(self, user_id: str, user_query: str):
412
+ async with self.conn.execute(
413
+ "SELECT userId, threadId FROM userid_threadid WHERE userId = ?", (user_id,)
414
+ ) as cursor:
415
+ result = await cursor.fetchone()
416
+
417
+ if result is None:
418
+ thread_id = user_id + self.generate_thread_id()
419
+ await self.conn.execute(
420
+ "INSERT INTO userid_threadid (userId, threadId) VALUES (?, ?)",
421
+ (user_id, thread_id),
422
+ )
423
+ await self.conn.commit()
424
+ else:
425
+ thread_id = result[1]
426
+
427
+ initial_state = {"messages": [HumanMessage(content=user_query)]}
428
+ config = {
429
+ "configurable": {"thread_id": thread_id},
430
+ "metadata": {"thread_id": thread_id},
431
+ "run_name": "chat_turn",
432
+ }
433
+ return self.ai_only_stream(initial_state, config)
frontend/index.html ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>Realtime AI Voice Assistant</title>
7
+
8
+ <link rel="stylesheet" href="style.css" />
9
+ </head>
10
+ <body>
11
+
12
+ <div class="container">
13
+
14
+ <div class="topbar">
15
+ <h1>🎙️ AI Voice Assistant</h1>
16
+ </div>
17
+
18
+ <div id="chat-box"></div>
19
+
20
+ <div class="controls">
21
+
22
+ <div class="text-section">
23
+ <input
24
+ type="text"
25
+ id="text-input"
26
+ placeholder="Type your message..."
27
+ />
28
+
29
+ <button id="send-btn">
30
+ Send
31
+ </button>
32
+ </div>
33
+
34
+ <div class="voice-section">
35
+ <button id="mic-btn">
36
+ 🎤 Start Voice
37
+ </button>
38
+ </div>
39
+
40
+ </div>
41
+
42
+ </div>
43
+
44
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
45
+ <script src="script.js"></script>
46
+ </body>
47
+ </html>
frontend/script.js ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const chatBox = document.getElementById("chat-box");
2
+ const sendBtn = document.getElementById("send-btn");
3
+ const textInput = document.getElementById("text-input");
4
+ const micBtn = document.getElementById("mic-btn");
5
+
6
+ const userId = "walid";
7
+
8
+
9
+ // =======================
10
+ // CHAT WEBSOCKET
11
+ // =======================
12
+
13
+ const chatSocket = new WebSocket("ws://127.0.0.1:8679/ws/chat");
14
+
15
+ chatSocket.onmessage = (event) => {
16
+
17
+ const data = event.data;
18
+
19
+ if (data === "[[END]]") {
20
+ return;
21
+ }
22
+
23
+ appendMessage(data, "ai");
24
+ };
25
+
26
+
27
+ sendBtn.onclick = () => {
28
+ sendTextMessage();
29
+ };
30
+
31
+ textInput.addEventListener("keydown", (e) => {
32
+ if (e.key === "Enter") {
33
+ sendTextMessage();
34
+ }
35
+ });
36
+
37
+
38
+ function sendTextMessage() {
39
+
40
+ const message = textInput.value.trim();
41
+
42
+ if (!message) return;
43
+
44
+ appendMessage(message, "user");
45
+
46
+ chatSocket.send(JSON.stringify({
47
+ user_id: userId,
48
+ user_query: message
49
+ }));
50
+
51
+ textInput.value = "";
52
+ }
53
+
54
+
55
+ // =======================
56
+ // VOICE WEBSOCKET
57
+ // =======================
58
+
59
+ const voiceSocket = new WebSocket("ws://127.0.0.1:8679/ws/voice");
60
+
61
+ voiceSocket.binaryType = "arraybuffer";
62
+
63
+ let mediaRecorder;
64
+ let audioChunks = [];
65
+ let isRecording = false;
66
+
67
+ voiceSocket.onmessage = async (event) => {
68
+
69
+ // TEXT MESSAGE
70
+ if (typeof event.data === "string") {
71
+
72
+ const text = event.data;
73
+
74
+ if (text.startsWith("[STT]:")) {
75
+ appendMessage("🎤 " + text.replace("[STT]:", ""), "user");
76
+ }
77
+
78
+ else if (text.startsWith("[LLM]:")) {
79
+ appendMessage(
80
+ text.replace("[LLM]:", ""),
81
+ "ai"
82
+ );
83
+ }
84
+
85
+ return;
86
+ }
87
+
88
+ // AUDIO MESSAGE
89
+ const audioBlob = new Blob([event.data], { type: "audio/mp3" });
90
+
91
+ const audioUrl = URL.createObjectURL(audioBlob);
92
+
93
+ const audio = new Audio(audioUrl);
94
+
95
+ audio.play();
96
+ };
97
+
98
+
99
+ micBtn.onclick = async () => {
100
+
101
+ if (!isRecording) {
102
+ startRecording();
103
+ } else {
104
+ stopRecording();
105
+ }
106
+ };
107
+
108
+
109
+ async function startRecording() {
110
+
111
+ const stream = await navigator.mediaDevices.getUserMedia({
112
+ audio: true
113
+ });
114
+
115
+ mediaRecorder = new MediaRecorder(stream, {
116
+ mimeType: "audio/webm"
117
+ });
118
+
119
+ mediaRecorder.start(250);
120
+
121
+ mediaRecorder.ondataavailable = async (event) => {
122
+
123
+ if (event.data.size > 0 &&
124
+ voiceSocket.readyState === WebSocket.OPEN) {
125
+
126
+ const arrayBuffer = await event.data.arrayBuffer();
127
+
128
+ voiceSocket.send(arrayBuffer);
129
+ }
130
+ };
131
+
132
+ isRecording = true;
133
+
134
+ micBtn.innerText = "⏹ Stop Voice";
135
+ micBtn.classList.add("recording");
136
+ }
137
+
138
+
139
+ function stopRecording() {
140
+
141
+ mediaRecorder.stop();
142
+
143
+ isRecording = false;
144
+
145
+ micBtn.innerText = "🎤 Start Voice";
146
+ micBtn.classList.remove("recording");
147
+ }
148
+
149
+
150
+ // =======================
151
+ // UI
152
+ // =======================
153
+
154
+ function appendMessage(text, sender) {
155
+
156
+ const div = document.createElement("div");
157
+
158
+ div.classList.add("message");
159
+ div.classList.add(sender);
160
+
161
+ div.innerHTML = marked.parse(text);
162
+
163
+ chatBox.appendChild(div);
164
+
165
+ chatBox.scrollTop = chatBox.scrollHeight;
166
+ }
frontend/style.css ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ background: #0f172a;
9
+ color: white;
10
+ font-family: Arial, Helvetica, sans-serif;
11
+ height: 100vh;
12
+ display: flex;
13
+ justify-content: center;
14
+ align-items: center;
15
+ }
16
+
17
+ .container {
18
+ width: 90%;
19
+ max-width: 900px;
20
+ height: 90vh;
21
+ background: #111827;
22
+ border-radius: 20px;
23
+ overflow: hidden;
24
+ display: flex;
25
+ flex-direction: column;
26
+ }
27
+
28
+ .topbar {
29
+ padding: 20px;
30
+ background: #1e293b;
31
+ border-bottom: 1px solid #334155;
32
+ }
33
+
34
+ .topbar h1 {
35
+ font-size: 24px;
36
+ }
37
+
38
+ #chat-box {
39
+ flex: 1;
40
+ overflow-y: auto;
41
+ padding: 20px;
42
+ }
43
+
44
+ /* .message {
45
+ margin-bottom: 16px;
46
+ padding: 12px 16px;
47
+ border-radius: 14px;
48
+ width: fit-content;
49
+ max-width: 80%;
50
+ line-height: 1.5;
51
+ } */
52
+
53
+ .user {
54
+ background: #2563eb;
55
+ margin-left: auto;
56
+ }
57
+
58
+ .ai {
59
+ background: #374151;
60
+ }
61
+
62
+ .controls {
63
+ padding: 20px;
64
+ border-top: 1px solid #334155;
65
+ }
66
+
67
+ .text-section {
68
+ display: flex;
69
+ gap: 10px;
70
+ }
71
+
72
+ #text-input {
73
+ flex: 1;
74
+ padding: 14px;
75
+ border-radius: 12px;
76
+ border: none;
77
+ outline: none;
78
+ background: #1e293b;
79
+ color: white;
80
+ font-size: 16px;
81
+ }
82
+
83
+ button {
84
+ padding: 14px 20px;
85
+ border: none;
86
+ border-radius: 12px;
87
+ cursor: pointer;
88
+ background: #2563eb;
89
+ color: white;
90
+ font-size: 16px;
91
+ }
92
+
93
+ button:hover {
94
+ opacity: 0.9;
95
+ }
96
+
97
+ .voice-section {
98
+ margin-top: 15px;
99
+ }
100
+
101
+ #mic-btn.recording {
102
+ background: red;
103
+ }
104
+
105
+ .message {
106
+ max-width: 80%;
107
+ padding: 12px 14px;
108
+ margin: 8px 0;
109
+ border-radius: 12px;
110
+
111
+ line-height: 1.6;
112
+ font-size: 15px;
113
+
114
+ word-wrap: break-word;
115
+ overflow-wrap: break-word;
116
+
117
+ white-space: normal;
118
+ }
119
+
120
+ .message.ai {
121
+ background: #2d3748;
122
+ color: #fff;
123
+ text-align: left;
124
+ }
125
+
126
+ .message.user {
127
+ background: #4a5568;
128
+ color: #fff;
129
+ text-align: left;
130
+ margin-left: auto;
131
+ }
132
+
133
+ .message ul,
134
+ .message ol {
135
+ padding-left: 20px;
136
+ margin: 8px 0;
137
+ }
138
+
139
+ .message li {
140
+ margin-bottom: 6px;
141
+ }
142
+
143
+ .message p {
144
+ margin: 6px 0;
145
+ }
146
+
147
+ #chat-box {
148
+ display: flex;
149
+ flex-direction: column;
150
+ padding: 10px;
151
+ gap: 6px;
152
+ }
requirements.txt ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ python-dotenv
2
+ fastapi
3
+ uvicorn
4
+ requests
5
+ langchain
6
+ langchain-chroma
7
+ langchain-classic
8
+ langchain-community
9
+ langchain-core
10
+ langchain-experimental
11
+ langchain-google-genai
12
+ langchain-huggingface
13
+ langchain-mcp-adapters
14
+ langchain-ollama
15
+ langchain-openai
16
+ langchain-protocol
17
+ langchain-text-splitters
18
+ langgraph
19
+ langgraph-checkpoint
20
+ langgraph-checkpoint-sqlite
21
+ langgraph-prebuilt
22
+ langgraph-sdk
23
+ langsmith
24
+ aiosqlite
25
+ colorama
26
+ faster-whisper
27
+ mcp
28
+ numpy
29
+ ollama
30
+ pydantic
31
+ twilio
32
+ uuid_utils
33
+ uv
34
+ uvicorn
35
+
services/stt.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # from faster_whisper import WhisperModel
2
+ # import tempfile
3
+
4
+ # model = WhisperModel("small", device="cpu", compute_type="int8")
5
+ # class StreamingSTT:
6
+ # def __init__(self):
7
+ # self.audio_buffer = bytearray()
8
+
9
+ # def add_audio(self, chunk: bytes):
10
+ # self.audio_buffer.extend(chunk)
11
+
12
+ # def transcribe_if_ready(self):
13
+ # # simple chunk trigger (1.5–3 sec buffer recommended)
14
+ # if len(self.audio_buffer) < 48000 * 2 * 2:
15
+ # return None
16
+
17
+ # with tempfile.NamedTemporaryFile(suffix=".wav", delete=True) as f:
18
+ # f.write(self.audio_buffer)
19
+ # f.flush()
20
+ # segments, _ = model.transcribe(f.name, language="bn", task="translate", beam_size=1)
21
+ # text = " ".join([s.text for s in segments])
22
+
23
+ # self.audio_buffer.clear()
24
+ # return text
25
+
26
+
27
+
28
+
29
+ from faster_whisper import WhisperModel
30
+ import tempfile
31
+
32
+ model = WhisperModel(
33
+ "small",
34
+ device="cpu",
35
+ compute_type="int8"
36
+ )
37
+
38
+ class StreamingSTT:
39
+
40
+ def __init__(self):
41
+ self.audio_buffer = bytearray()
42
+
43
+ def add_audio(self, chunk: bytes):
44
+ self.audio_buffer.extend(chunk)
45
+
46
+ def transcribe_if_ready(self):
47
+ # wait enough audio
48
+ if len(self.audio_buffer) < 50000:
49
+ return None
50
+
51
+ # SAVE AS WEBM
52
+ with tempfile.NamedTemporaryFile(
53
+ suffix=".webm",
54
+ delete=True
55
+ ) as f:
56
+
57
+ f.write(self.audio_buffer)
58
+ f.flush()
59
+
60
+ segments, _ = model.transcribe(
61
+ f.name,
62
+ language="bn",
63
+ beam_size=1
64
+ )
65
+
66
+ text = " ".join(
67
+ [segment.text for segment in segments]
68
+ )
69
+
70
+
71
+ self.audio_buffer.clear()
72
+
73
+ return text.strip()
services/tts.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import edge_tts
2
+ import asyncio
3
+ import tempfile
4
+
5
+ VOICE = "en-US-AriaNeural"
6
+
7
+ async def text_to_speech_stream(text: str):
8
+ communicate = edge_tts.Communicate(text, VOICE)
9
+
10
+ async for chunk in communicate.stream():
11
+ if chunk["type"] == "audio":
12
+ yield chunk["data"]