Skip to content

Commit 8bb7709

Browse files
committed
Merge branch 'main' of https://github.com/Yugansh5013/resources-ai-chatbot-plugin into feature/context-aware-UI
2 parents 49e1d19 + 6e2b711 commit 8bb7709

File tree

7 files changed

+216
-54
lines changed

7 files changed

+216
-54
lines changed

chatbot-core/api/routes/chatbot.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
UploadFile,
2727
File,
2828
Form,
29+
BackgroundTasks
2930
)
3031

3132
# =========================
@@ -45,8 +46,9 @@
4546
)
4647
from api.services.memory import (
4748
delete_session,
48-
init_session,
4949
session_exists,
50+
persist_session,
51+
init_session,
5052
)
5153
from api.services.file_service import (
5254
process_uploaded_file,
@@ -82,7 +84,7 @@
8284
async def chatbot_stream(websocket: WebSocket, session_id: str):
8385
"""
8486
WebSocket endpoint for real-time token streaming.
85-
87+
8688
Accepts WebSocket connections and streams chatbot responses
8789
token-by-token for a more interactive user experience.
8890
"""
@@ -153,7 +155,7 @@ async def chatbot_stream(websocket: WebSocket, session_id: str):
153155
def start_chat(response: Response):
154156
"""
155157
Create a new chat session.
156-
158+
157159
Returns a unique session ID that can be used for subsequent
158160
chatbot interactions.
159161
"""
@@ -163,15 +165,14 @@ def start_chat(response: Response):
163165
)
164166
return SessionResponse(session_id=session_id)
165167

166-
167168
@router.delete(
168169
"/sessions/{session_id}",
169170
response_model=DeleteResponse,
170171
)
171172
def delete_chat(session_id: str):
172173
"""
173174
Delete an existing chat session.
174-
175+
175176
Removes all conversation history and resources associated
176177
with the specified session.
177178
"""
@@ -186,14 +187,10 @@ def delete_chat(session_id: str):
186187
)
187188

188189

189-
# =========================
190-
# Chat Endpoints
191-
# =========================
192-
@router.post(
193-
"/sessions/{session_id}/message",
194-
response_model=ChatResponse,
195-
)
196-
def chatbot_reply(session_id: str, request: ChatRequest):
190+
# Chat Endpoint
191+
@router.post("/sessions/{session_id}/message", response_model=ChatResponse)
192+
def chatbot_reply(session_id: str, request: ChatRequest, _background_tasks: BackgroundTasks):
193+
197194
"""
198195
POST endpoint to handle chatbot replies.
199196
@@ -212,11 +209,13 @@ def chatbot_reply(session_id: str, request: ChatRequest):
212209
status_code=404,
213210
detail="Session not found.",
214211
)
215-
216-
return get_chatbot_reply(
212+
reply = get_chatbot_reply(session_id, request.message)
213+
_background_tasks.add_task(
214+
persist_session,
217215
session_id,
218-
request.message,
219-
)
216+
)
217+
218+
return reply
220219

221220

222221
@router.post(

chatbot-core/api/services/memory.py

Lines changed: 87 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,25 @@
88
from threading import Lock
99
from langchain.memory import ConversationBufferMemory
1010
from api.config.loader import CONFIG
11-
11+
from api.services.sessionmanager import(
12+
delete_session_file,
13+
load_session,
14+
session_exists_in_json,
15+
append_message
16+
)
1217
# sessionId --> {"memory": ConversationBufferMemory, "last_accessed": datetime}
18+
19+
1320
_sessions = {}
1421
_lock = Lock()
1522

23+
1624
def init_session() -> str:
1725
"""
1826
Initialize a new chat session and store its memory object.
1927
2028
Returns:
21-
str: A newly generated UUID representing the session ID.
29+
str: A newly generated UUID representing the session ID.
2230
"""
2331
session_id = str(uuid.uuid4())
2432
with _lock:
@@ -28,54 +36,99 @@ def init_session() -> str:
2836
}
2937
return session_id
3038

39+
3140
def get_session(session_id: str) -> ConversationBufferMemory | None:
3241
"""
33-
Retrieve the conversation memory for a given session ID.
42+
Retrieve the chat session memory for the given session ID.
43+
Lazily restores from disk if missing in memory.
3444
3545
Args:
3646
session_id (str): The session identifier.
3747
3848
Returns:
3949
ConversationBufferMemory | None: The memory object if found, else None.
4050
"""
51+
4152
with _lock:
53+
4254
session_data = _sessions.get(session_id)
43-
if session_data:
44-
# Update last accessed timestamp
55+
56+
if session_data :
4557
session_data["last_accessed"] = datetime.now()
4658
return session_data["memory"]
47-
return None
4859

49-
def delete_session(session_id: str) -> bool:
60+
history = load_session(session_id)
61+
if not history:
62+
return None
63+
64+
memory = ConversationBufferMemory(return_messages=True)
65+
for msg in history:
66+
memory.chat_memory.add_message(# pylint: disable=no-member
67+
{
68+
"role": msg["role"],
69+
"content": msg["content"],
70+
}
71+
)
72+
73+
_sessions[session_id] = {
74+
"memory": memory,
75+
"last_accessed": datetime.now()
76+
}
77+
78+
return memory
79+
80+
81+
def persist_session(session_id: str)-> None:
5082
"""
51-
Delete an existing chat session and its memory.
83+
Persist the current session messages to disk.
5284
5385
Args:
5486
session_id (str): The session identifier.
87+
"""
88+
session_data = get_session(session_id)
89+
if session_data:
90+
messages = list(session_data.chat_memory.messages)
91+
append_message(session_id, messages)
92+
93+
94+
95+
def delete_session(session_id: str) -> bool:
96+
"""
97+
Delete a chat session and its persisted data.
98+
99+
Args:
100+
session_id (str): The session identifier.
55101
56102
Returns:
57103
bool: True if the session existed and was deleted, False otherwise.
58104
"""
59105
with _lock:
60-
deleted = _sessions.pop(session_id, None) is not None
61-
return deleted
106+
if session_id is None:
107+
return True
108+
in_memory_deleted = _sessions.pop(session_id, None) is not None
109+
110+
if in_memory_deleted:
111+
delete_session_file(session_id)
112+
113+
return in_memory_deleted
114+
62115

63116
def session_exists(session_id: str) -> bool:
64117
"""
65-
Check if a chat session with the given ID exists.
118+
Check if a chat session exists in memory.
66119
67120
Args:
68-
session_id (str): The session identifier.
121+
session_id (str): The session identifier.
69122
70123
Returns:
71124
bool: True if the session exists, False otherwise.
72125
"""
73126
with _lock:
74-
exists = session_id in _sessions
75-
return exists
127+
return session_id in _sessions
128+
76129

77130
def reset_sessions():
78-
"""Helper fucntion to clear all sessions. Useful for testing."""
131+
"""Helper function to clear all sessions. Useful for testing."""
79132
with _lock:
80133
_sessions.clear()
81134

@@ -91,9 +144,15 @@ def get_last_accessed(session_id: str) -> datetime | None:
91144
"""
92145
with _lock:
93146
session_data = _sessions.get(session_id)
94-
if session_data:
147+
if session_data is not None:
95148
return session_data["last_accessed"]
96-
return None
149+
150+
history = load_session(session_id)
151+
if not history:
152+
return None
153+
154+
155+
return history["last_accessed"]
97156

98157
def set_last_accessed(session_id: str, timestamp: datetime) -> bool:
99158
"""
@@ -111,6 +170,14 @@ def set_last_accessed(session_id: str, timestamp: datetime) -> bool:
111170
if session_data:
112171
session_data["last_accessed"] = timestamp
113172
return True
173+
174+
history = load_session(session_id)
175+
if not history:
176+
return False
177+
178+
history["last_accessed"] = timestamp
179+
return True
180+
114181
return False
115182

116183
def get_session_count() -> int:
@@ -142,6 +209,8 @@ def cleanup_expired_sessions() -> int:
142209
]
143210

144211
for session_id in expired_session_ids:
145-
del _sessions[session_id]
212+
in_memory_deleted = _sessions.pop(session_id, None) is not None
213+
if in_memory_deleted and session_exists_in_json(session_id):
214+
delete_session_file(session_id)
146215

147216
return len(expired_session_ids)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Session management utilities."""
2+
import os
3+
import json
4+
import uuid
5+
from threading import Lock
6+
7+
8+
9+
_SESSION_DIRECTORY = os.getenv("SESSION_FILE_PATH", "data/sessions")
10+
11+
os.makedirs(_SESSION_DIRECTORY,mode = 0o755, exist_ok=True)
12+
13+
_FILE_LOCK = Lock()
14+
15+
16+
def _get_session_file_path(session_id: str) -> str:
17+
"""
18+
Returns the full path for a session file.
19+
Example: data/sessions/<session_id>.json
20+
"""
21+
22+
try:
23+
uuid.UUID(session_id)
24+
except ValueError:
25+
return ""
26+
return os.path.join(_SESSION_DIRECTORY, f"{session_id}.json")
27+
28+
29+
def _load_session_from_json(session_id: str) -> list:
30+
"""
31+
Load a session's history from disk.
32+
"""
33+
path = _get_session_file_path(session_id)
34+
if not os.path.exists(path):
35+
return []
36+
37+
with open(path, "r", encoding="utf-8") as f:
38+
return json.load(f)
39+
40+
41+
def _append_message_to_json(session_id: str, messages:list) -> None:
42+
"""
43+
Persist the current session messages as a full snapshot using atomic write.
44+
"""
45+
path = _get_session_file_path(session_id)
46+
if os.path.exists(path):
47+
tmp_path = f"{path}.tmp"
48+
49+
with _FILE_LOCK:
50+
51+
with open(tmp_path, "w", encoding="utf-8") as f:
52+
json.dump(messages, f, indent=2, ensure_ascii=False)
53+
54+
os.replace(tmp_path, path)
55+
56+
57+
58+
def _delete_session(session_id: str) -> bool:
59+
"""
60+
Delete the persisted session file.
61+
"""
62+
path = _get_session_file_path(session_id)
63+
64+
with _FILE_LOCK:
65+
if os.path.exists(path):
66+
os.remove(path)
67+
return True
68+
return False
69+
70+
def session_exists_in_json(session_id: str) -> bool:
71+
"""
72+
Check if a session file exists on disk.
73+
"""
74+
path = _get_session_file_path(session_id)
75+
return os.path.exists(path)
76+
77+
# Public API functions
78+
79+
def append_message(session_id: str, messages: list) -> None:
80+
"""
81+
Public function to append messages to a session's JSON file.
82+
"""
83+
_append_message_to_json(session_id, messages)
84+
85+
def load_session(session_id: str) -> list:
86+
"""
87+
Public function to load a session's history from its JSON file.
88+
"""
89+
return _load_session_from_json(session_id)
90+
91+
def delete_session_file(session_id: str) -> bool:
92+
"""
93+
Public function to delete a session's JSON file.
94+
"""
95+
return _delete_session(session_id)

0 commit comments

Comments
 (0)