anhkhoiphan commited on
Commit
aead1ee
·
verified ·
1 Parent(s): c1cbc59

Bổ sung1 hour delay cho clear state và session clean do trigger on_chat_end

Browse files
Files changed (1) hide show
  1. app.py +167 -31
app.py CHANGED
@@ -8,6 +8,8 @@ from typing import Dict, List, Any, Optional, Callable
8
  from dataclasses import dataclass, field
9
  import os
10
  import uuid
 
 
11
 
12
 
13
  API_BASE_URL = "https://sale-agent-m179.onrender.com"
@@ -23,6 +25,10 @@ class ConversationState:
23
  outputs: Optional[Dict[str, Any]] = None
24
  selected_model: str = "Gemini 2.0 Flash"
25
  product_model_search: bool = False
 
 
 
 
26
 
27
  def reset(self):
28
  """Reset state to initial values"""
@@ -33,37 +39,118 @@ class ConversationState:
33
  self.outputs = None
34
  self.selected_model = "Gemini 2.0 Flash"
35
  self.product_model_search = False
 
 
 
 
 
 
 
 
 
 
36
 
37
 
38
  class StateManager:
39
- """Manages conversation state operations with per-session isolation"""
40
 
41
  # CLASS-LEVEL session storage for isolation between different browser sessions
42
  _session_states: Dict[str, ConversationState] = {}
 
43
 
44
  @staticmethod
45
  def get_or_create_session_state(session_id: str) -> ConversationState:
46
  """Get existing session state or create new one"""
47
- if session_id not in StateManager._session_states:
48
- state = ConversationState()
49
- state.session_id = session_id
50
- StateManager._session_states[session_id] = state
51
- print(f"🆕 Created new session state for: {session_id}")
52
- else:
53
- print(f"🔄 Retrieved existing session state for: {session_id}")
54
-
55
- return StateManager._session_states[session_id]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
  @staticmethod
58
- def cleanup_session(session_id: str):
59
- """Clean up session state when user disconnects"""
60
- if session_id in StateManager._session_states:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  del StateManager._session_states[session_id]
62
- print(f"🗑️ Cleaned up session state for: {session_id}")
 
 
 
 
 
 
 
 
 
63
 
64
  @staticmethod
65
  async def clear_chat_state(state: ConversationState):
66
- """Clear all conversation history and reset state via API"""
67
  if state.session_id is not None and API_BASE_URL:
68
  try:
69
  payload = {
@@ -71,12 +158,12 @@ class StateManager:
71
  "reset_model": False,
72
  "session_id": state.session_id
73
  }
74
- response = requests.post(f"{API_BASE_URL}/clear_memory", json=payload)
75
  print(f"Clear memory response: {response.status_code}")
76
  except Exception as e:
77
  print(f"Warning: clear_memory failed: {e}")
78
 
79
- # Reset state but keep session_id
80
  session_id = state.session_id
81
  state.reset()
82
  state.session_id = session_id
@@ -85,11 +172,28 @@ class StateManager:
85
  def change_model(state: ConversationState, model_name: str):
86
  """Change the selected model"""
87
  state.selected_model = model_name
 
88
 
89
  @staticmethod
90
  def toggle_product_model_search(state: ConversationState):
91
  """Toggle product model search mode"""
92
  state.product_model_search = not state.product_model_search
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
 
95
  class ChatService:
@@ -103,6 +207,10 @@ class ChatService:
103
  ) -> str:
104
  """Handle chat responses with image support"""
105
  print(f"🔄 === DEBUG STATE ===\n Chat request with model: {state.selected_model}, Product Model Search: {state.product_model_search}, Session ID: {state.session_id}")
 
 
 
 
106
  start = time.perf_counter()
107
 
108
  if not API_BASE_URL:
@@ -441,10 +549,8 @@ def ensure_session_state() -> Optional[ConversationState]:
441
  session_id = cl.user_session.get("session_id")
442
 
443
  if not session_id:
444
- # # Chỉ tạo mới khi chưa có (tức là lần đầu mở tab)
445
- # session_id = str(uuid.uuid4())
446
- # cl.user_session.set("session_id", session_id)
447
  print(f"Lỗi: Không lấy được session id ở ensure_session_state")
 
448
 
449
  return StateManager.get_or_create_session_state(session_id)
450
 
@@ -457,7 +563,7 @@ def get_current_session_state() -> Optional[ConversationState]:
457
  """Get current session state using Chainlit's session system"""
458
  try:
459
  # Use Chainlit's user session to get unique session ID
460
- chainlit_session_id = cl.user_session.get("session_id") # Fixed: added key parameter
461
 
462
  if chainlit_session_id:
463
  return StateManager.get_or_create_session_state(chainlit_session_id)
@@ -476,6 +582,9 @@ async def on_chat_start():
476
  if not session_id:
477
  session_id = str(uuid.uuid4())
478
  cl.user_session.set("session_id", session_id)
 
 
 
479
 
480
  app_state = StateManager.get_or_create_session_state(session_id)
481
 
@@ -498,21 +607,21 @@ async def on_chat_start():
498
 
499
  @cl.on_chat_end
500
  async def on_chat_end():
501
- """Clean up when chat session ends (user closes tab, new chat, exit UI)"""
502
  try:
503
  session_id = cl.user_session.get("session_id")
504
- print(f"on_chat_end triggered for session {session_id}")
505
 
506
  if session_id:
507
- app_state = StateManager.get_or_create_session_state(session_id)
508
- await StateManager.clear_chat_state(app_state)
509
- StateManager.cleanup_session(session_id)
510
- print(f" Properly cleaned session {session_id}")
511
  else:
512
- print("⚠️ No session_id found in on_chat_end (maybe reconnect case)")
513
  except Exception as e:
514
- print(f"⚠️ Error during cleanup: {e}")
515
-
516
 
517
  # ACTION CALLBACKS - All use ensure_session_state() for better reliability
518
  @cl.action_callback("show_specs")
@@ -657,6 +766,30 @@ async def on_select_model_2(action):
657
  await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.0 Flash Lite**", app_state, author="assistant")
658
 
659
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
  @cl.on_message
661
  async def main(message: cl.Message):
662
  """Main message handler"""
@@ -683,4 +816,7 @@ async def main(message: cl.Message):
683
  typing_msg.content = response
684
  typing_msg.actions = UIService.create_action_buttons(app_state)
685
  typing_msg.author = "assistant"
686
- await typing_msg.update()
 
 
 
 
8
  from dataclasses import dataclass, field
9
  import os
10
  import uuid
11
+ import threading
12
+ from datetime import datetime, timedelta
13
 
14
 
15
  API_BASE_URL = "https://sale-agent-m179.onrender.com"
 
25
  outputs: Optional[Dict[str, Any]] = None
26
  selected_model: str = "Gemini 2.0 Flash"
27
  product_model_search: bool = False
28
+ # New fields for delayed cleanup
29
+ pending_cleanup: bool = False
30
+ cleanup_timer: Optional[threading.Timer] = None
31
+ last_activity: datetime = field(default_factory=datetime.now)
32
 
33
  def reset(self):
34
  """Reset state to initial values"""
 
39
  self.outputs = None
40
  self.selected_model = "Gemini 2.0 Flash"
41
  self.product_model_search = False
42
+ # Reset cleanup fields but don't touch timers
43
+ self.pending_cleanup = False
44
+ self.last_activity = datetime.now()
45
+
46
+ def cancel_cleanup_timer(self):
47
+ """Cancel pending cleanup timer if exists"""
48
+ if self.cleanup_timer:
49
+ self.cleanup_timer.cancel()
50
+ self.cleanup_timer = None
51
+ print(f"🚫 Cancelled cleanup timer for session: {self.session_id}")
52
 
53
 
54
  class StateManager:
55
+ """Manages conversation state operations with per-session isolation and delayed cleanup"""
56
 
57
  # CLASS-LEVEL session storage for isolation between different browser sessions
58
  _session_states: Dict[str, ConversationState] = {}
59
+ _lock = threading.Lock() # Thread safety for timer operations
60
 
61
  @staticmethod
62
  def get_or_create_session_state(session_id: str) -> ConversationState:
63
  """Get existing session state or create new one"""
64
+ with StateManager._lock:
65
+ if session_id not in StateManager._session_states:
66
+ state = ConversationState()
67
+ state.session_id = session_id
68
+ StateManager._session_states[session_id] = state
69
+ print(f"🆕 Created new session state for: {session_id}")
70
+ else:
71
+ state = StateManager._session_states[session_id]
72
+ print(f"🔄 Retrieved existing session state for: {session_id}")
73
+
74
+ # CRITICAL: If session was pending cleanup, cancel it because user is active again
75
+ if state.pending_cleanup:
76
+ state.cancel_cleanup_timer()
77
+ state.pending_cleanup = False
78
+ print(f"♻️ User activity detected! Cancelled pending cleanup for: {session_id}")
79
+
80
+ # Update activity timestamp
81
+ state.last_activity = datetime.now()
82
+ return state
83
+
84
+ @staticmethod
85
+ def schedule_delayed_cleanup(session_id: str, delay_seconds: int = 3600):
86
+ """Schedule delayed cleanup for a session (default 1 hours for disconnect tolerance)"""
87
+ with StateManager._lock:
88
+ if session_id not in StateManager._session_states:
89
+ print(f"⚠️ Cannot schedule cleanup for non-existent session: {session_id}")
90
+ return
91
+
92
+ state = StateManager._session_states[session_id]
93
+
94
+ # Cancel existing timer if any
95
+ state.cancel_cleanup_timer()
96
+
97
+ # Mark as pending cleanup
98
+ state.pending_cleanup = True
99
+
100
+ # Schedule new cleanup
101
+ def delayed_cleanup():
102
+ print(f"⏰ Executing delayed cleanup for session: {session_id}")
103
+ StateManager._perform_actual_cleanup(session_id)
104
+
105
+ state.cleanup_timer = threading.Timer(delay_seconds, delayed_cleanup)
106
+ state.cleanup_timer.start()
107
+
108
+ print(f"⏱️ Scheduled cleanup in {delay_seconds}s for session: {session_id} (likely disconnect)")
109
 
110
  @staticmethod
111
+ def _perform_actual_cleanup(session_id: str):
112
+ """Perform the actual cleanup after delay"""
113
+ with StateManager._lock:
114
+ if session_id not in StateManager._session_states:
115
+ print(f"⚠️ Session already cleaned or doesn't exist: {session_id}")
116
+ return
117
+
118
+ state = StateManager._session_states[session_id]
119
+
120
+ # Double-check if session is still pending cleanup (user might have sent message)
121
+ if not state.pending_cleanup:
122
+ print(f"🚫 Cleanup cancelled - user activity detected for: {session_id}")
123
+ return
124
+
125
+ # Perform API cleanup
126
+ try:
127
+ if API_BASE_URL:
128
+ payload = {
129
+ "reset_cache": True,
130
+ "reset_model": False,
131
+ "session_id": session_id
132
+ }
133
+ response = requests.post(f"{API_BASE_URL}/clear_memory", json=payload, timeout=30)
134
+ print(f"Clear memory response for {session_id}: {response.status_code}")
135
+ except Exception as e:
136
+ print(f"Warning: clear_memory failed for {session_id}: {e}")
137
+
138
+ # Remove from memory
139
  del StateManager._session_states[session_id]
140
+ print(f"🗑️ Successfully cleaned up session: {session_id}")
141
+
142
+ @staticmethod
143
+ def cleanup_session_immediate(session_id: str):
144
+ """Immediate cleanup (for testing or forced cleanup)"""
145
+ with StateManager._lock:
146
+ if session_id in StateManager._session_states:
147
+ state = StateManager._session_states[session_id]
148
+ state.cancel_cleanup_timer()
149
+ StateManager._perform_actual_cleanup(session_id)
150
 
151
  @staticmethod
152
  async def clear_chat_state(state: ConversationState):
153
+ """Clear all conversation history and reset state via API (but keep session alive)"""
154
  if state.session_id is not None and API_BASE_URL:
155
  try:
156
  payload = {
 
158
  "reset_model": False,
159
  "session_id": state.session_id
160
  }
161
+ response = requests.post(f"{API_BASE_URL}/clear_memory", json=payload, timeout=30)
162
  print(f"Clear memory response: {response.status_code}")
163
  except Exception as e:
164
  print(f"Warning: clear_memory failed: {e}")
165
 
166
+ # Reset state but keep session_id and don't trigger cleanup
167
  session_id = state.session_id
168
  state.reset()
169
  state.session_id = session_id
 
172
  def change_model(state: ConversationState, model_name: str):
173
  """Change the selected model"""
174
  state.selected_model = model_name
175
+ state.last_activity = datetime.now()
176
 
177
  @staticmethod
178
  def toggle_product_model_search(state: ConversationState):
179
  """Toggle product model search mode"""
180
  state.product_model_search = not state.product_model_search
181
+ state.last_activity = datetime.now()
182
+
183
+ @staticmethod
184
+ def get_session_status() -> Dict[str, Dict[str, Any]]:
185
+ """Get status of all sessions (for debugging)"""
186
+ with StateManager._lock:
187
+ status = {}
188
+ for session_id, state in StateManager._session_states.items():
189
+ status[session_id] = {
190
+ "pending_cleanup": state.pending_cleanup,
191
+ "has_timer": state.cleanup_timer is not None,
192
+ "last_activity": state.last_activity.isoformat(),
193
+ "selected_model": state.selected_model,
194
+ "product_model_search": state.product_model_search
195
+ }
196
+ return status
197
 
198
 
199
  class ChatService:
 
207
  ) -> str:
208
  """Handle chat responses with image support"""
209
  print(f"🔄 === DEBUG STATE ===\n Chat request with model: {state.selected_model}, Product Model Search: {state.product_model_search}, Session ID: {state.session_id}")
210
+
211
+ # Update activity timestamp - this is KEY to prevent cleanup during active use
212
+ state.last_activity = datetime.now()
213
+
214
  start = time.perf_counter()
215
 
216
  if not API_BASE_URL:
 
549
  session_id = cl.user_session.get("session_id")
550
 
551
  if not session_id:
 
 
 
552
  print(f"Lỗi: Không lấy được session id ở ensure_session_state")
553
+ return None
554
 
555
  return StateManager.get_or_create_session_state(session_id)
556
 
 
563
  """Get current session state using Chainlit's session system"""
564
  try:
565
  # Use Chainlit's user session to get unique session ID
566
+ chainlit_session_id = cl.user_session.get("session_id")
567
 
568
  if chainlit_session_id:
569
  return StateManager.get_or_create_session_state(chainlit_session_id)
 
582
  if not session_id:
583
  session_id = str(uuid.uuid4())
584
  cl.user_session.set("session_id", session_id)
585
+ print(f"🆕 Generated new session_id: {session_id}")
586
+ else:
587
+ print(f"🔄 Reusing existing session_id: {session_id}")
588
 
589
  app_state = StateManager.get_or_create_session_state(session_id)
590
 
 
607
 
608
  @cl.on_chat_end
609
  async def on_chat_end():
610
+ """Handle chat session end with delayed cleanup mechanism"""
611
  try:
612
  session_id = cl.user_session.get("session_id")
613
+ print(f"📤 on_chat_end triggered for session {session_id}")
614
 
615
  if session_id:
616
+ # Schedule delayed cleanup instead of immediate cleanup
617
+ # Use shorter delay (30s) since this is likely just a temporary disconnect
618
+ StateManager.schedule_delayed_cleanup(session_id, delay_seconds=3600)
619
+ print(f" Scheduled delayed cleanup for session {session_id} (1h delay for disconnect tolerance)")
620
  else:
621
+ print("⚠️ No session_id found in on_chat_end")
622
  except Exception as e:
623
+ print(f"⚠️ Error during on_chat_end: {e}")
624
+
625
 
626
  # ACTION CALLBACKS - All use ensure_session_state() for better reliability
627
  @cl.action_callback("show_specs")
 
766
  await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.0 Flash Lite**", app_state, author="assistant")
767
 
768
 
769
+ # DEBUG ENDPOINTS (optional - for monitoring session status)
770
+ @cl.action_callback("debug_sessions")
771
+ async def on_debug_sessions(action):
772
+ """Debug action to show session status (can be added to debug builds)"""
773
+ try:
774
+ status = StateManager.get_session_status()
775
+ debug_content = "🔍 **Debug: Session Status**\n\n"
776
+
777
+ if not status:
778
+ debug_content += "No active sessions."
779
+ else:
780
+ for session_id, info in status.items():
781
+ debug_content += f"**Session: {session_id[:8]}...**\n"
782
+ debug_content += f"- Pending cleanup: {info['pending_cleanup']}\n"
783
+ debug_content += f"- Has timer: {info['has_timer']}\n"
784
+ debug_content += f"- Last activity: {info['last_activity']}\n"
785
+ debug_content += f"- Model: {info['selected_model']}\n"
786
+ debug_content += f"- Product search: {info['product_model_search']}\n\n"
787
+
788
+ await cl.Message(content=debug_content, author="assistant").send()
789
+ except Exception as e:
790
+ await cl.Message(content=f"Debug error: {e}", author="assistant").send()
791
+
792
+
793
  @cl.on_message
794
  async def main(message: cl.Message):
795
  """Main message handler"""
 
816
  typing_msg.content = response
817
  typing_msg.actions = UIService.create_action_buttons(app_state)
818
  typing_msg.author = "assistant"
819
+ await typing_msg.update()
820
+
821
+
822
+