anhkhoiphan commited on
Commit
626eb43
·
verified ·
1 Parent(s): f9d327a

Bổ sung phân chia/quản lý session cho UI

Browse files
Files changed (1) hide show
  1. app.py +170 -27
app.py CHANGED
@@ -7,6 +7,7 @@ import asyncio
7
  from typing import Dict, List, Any, Optional, Callable
8
  from dataclasses import dataclass, field
9
  import os
 
10
 
11
 
12
  API_BASE_URL = "https://sale-agent-m179.onrender.com"
@@ -15,6 +16,7 @@ API_BASE_URL = "https://sale-agent-m179.onrender.com"
15
  @dataclass
16
  class ConversationState:
17
  """Data class to hold conversation state"""
 
18
  specs_advantages: Dict[str, Any] = field(default_factory=dict)
19
  solution_packages: List[str] = field(default_factory=list)
20
  raw_documents: Optional[Dict[str, Any]] = None
@@ -24,6 +26,7 @@ class ConversationState:
24
 
25
  def reset(self):
26
  """Reset state to initial values"""
 
27
  self.specs_advantages = {}
28
  self.solution_packages = []
29
  self.raw_documents = None
@@ -33,34 +36,58 @@ class ConversationState:
33
 
34
 
35
  class StateManager:
36
- """Manages conversation state operations"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  @staticmethod
39
- def create_initial_state() -> ConversationState:
40
- """Create initial conversation state"""
41
- return ConversationState()
 
 
42
 
43
  @staticmethod
44
  async def clear_chat_state(state: ConversationState):
45
  """Clear all conversation history and reset state via API"""
46
- try:
47
- requests.post(f"{API_BASE_URL}/clear_memory",
48
- json={"reset_cache": True, "reset_model": False})
49
- except Exception as e:
50
- print(f"Warning: clear_memory failed: {e}")
 
51
 
52
- # Reset state
 
53
  state.reset()
 
54
 
55
  @staticmethod
56
  def change_model(state: ConversationState, model_name: str):
57
  """Change the selected model"""
58
- try:
59
- requests.post(f"{API_BASE_URL}/set_model",
60
- json={"model_name": model_name})
 
 
 
 
 
61
  state.selected_model = model_name
62
- except Exception as e:
63
- print(f"Warning: set_model failed: {e}")
64
 
65
  @staticmethod
66
  def toggle_product_model_search(state: ConversationState):
@@ -78,9 +105,15 @@ class ChatService:
78
  image_path: Optional[str] = None
79
  ) -> str:
80
  """Handle chat responses with image support"""
81
- print(f"🔄 === DEBUG STATE ===\n Chat request with model: {state.selected_model}, Product Model Search: {state.product_model_search}")
82
  start = time.perf_counter()
83
 
 
 
 
 
 
 
84
  # Call API
85
  try:
86
  if image_path:
@@ -88,13 +121,15 @@ class ChatService:
88
  files = {"image": f}
89
  data = {
90
  "message": message,
91
- "product_model_search": str(state.product_model_search).lower()
 
92
  }
93
  resp = requests.post(
94
  f"{API_BASE_URL}/chat_with_image", files=files, data=data, timeout=120)
95
  else:
96
  payload = {
97
  "message": message,
 
98
  "debug": "Normal",
99
  "product_model_search": state.product_model_search
100
  }
@@ -123,7 +158,7 @@ class ChatService:
123
  state.raw_documents = raw_documents
124
  state.outputs = outputs
125
 
126
- # Filter products based on query - FIX: Call static method correctly
127
  if state.specs_advantages is not None:
128
  ChatService.get_specific_product_from_query(message, state)
129
 
@@ -154,6 +189,7 @@ class ChatService:
154
 
155
  state.specs_advantages = new_specs_advantages
156
 
 
157
  class DisplayService:
158
  """Handles display-related operations"""
159
 
@@ -310,7 +346,6 @@ class UIService:
310
  @staticmethod
311
  def create_action_buttons(state: ConversationState):
312
  """Create persistent action buttons"""
313
- # Determine the product search button text and icon
314
  search_status = "🔍 Tìm theo mã sản phẩm (Đang tắt)" if not state.product_model_search else "🔍 Tìm theo mã sản phẩm (Đang bật)"
315
 
316
  return [
@@ -360,16 +395,57 @@ class UIService:
360
  return msg
361
 
362
 
363
- # Application state - using dependency injection pattern
364
- app_state = StateManager.create_initial_state()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
 
366
 
367
  @cl.on_chat_start
368
  async def on_chat_start():
369
  """Initialize the chat session"""
370
- await StateManager.clear_chat_state(app_state)
 
 
 
 
 
 
 
 
 
 
371
  await cl.Message(
372
- content="🛍️ **RangDong Sales Agent**\n\nXin chào! Tôi có thể giúp bạn tìm kiếm và tư vấn sản phẩm RangDong. Hãy thử các câu hỏi mẫu:\n\n- Tìm sản phẩm bình giữ nhiệt dung tích dưới 2 lít\n- Tìm sản phẩm ổ cắm thông minh\n- Tư vấn cho tôi đèn học chống cận cho con gái của tôi học lớp 6",
373
  author="assistant"
374
  ).send()
375
 
@@ -382,9 +458,33 @@ async def on_chat_start():
382
  ).send()
383
 
384
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  @cl.action_callback("show_specs")
386
  async def on_show_specs(action):
387
  """Handle show specifications action"""
 
 
 
 
 
388
  specs_content = DisplayService.show_specs(app_state)
389
  await UIService.send_message_with_buttons(specs_content, app_state, author="assistant")
390
 
@@ -392,6 +492,11 @@ async def on_show_specs(action):
392
  @cl.action_callback("show_advantages")
393
  async def on_show_advantages(action):
394
  """Handle show advantages action"""
 
 
 
 
 
395
  adv_content = DisplayService.show_advantages(app_state)
396
  await UIService.send_message_with_buttons(adv_content, app_state, author="assistant")
397
 
@@ -399,6 +504,11 @@ async def on_show_advantages(action):
399
  @cl.action_callback("show_packages")
400
  async def on_show_packages(action):
401
  """Handle show packages action"""
 
 
 
 
 
402
  pkg_content = DisplayService.show_solution_packages(app_state)
403
  await UIService.send_message_with_buttons(pkg_content, app_state, author="assistant")
404
 
@@ -406,6 +516,11 @@ async def on_show_packages(action):
406
  @cl.action_callback("toggle_product_search")
407
  async def on_toggle_product_search(action):
408
  """Handle toggle product model search action"""
 
 
 
 
 
409
  StateManager.toggle_product_model_search(app_state)
410
 
411
  status_message = (
@@ -422,15 +537,18 @@ async def on_toggle_product_search(action):
422
  @cl.action_callback("change_model")
423
  async def on_change_model(action):
424
  """Handle model change action"""
 
 
 
 
 
425
  models = ["Gemini 2.0 Flash", "Gemini 2.5 Flash Lite", "Gemini 2.0 Flash Lite"]
426
 
427
- # Create model selection actions
428
  model_actions = [
429
  cl.Action(name=f"select_model_{i}", value=model, label=model, payload={"model": model})
430
  for i, model in enumerate(models)
431
  ]
432
 
433
- # Add back button to return to main actions
434
  model_actions.append(
435
  cl.Action(name="back_to_main", value="back", label="🔙 Quay lại", payload={"action": "back"})
436
  )
@@ -445,6 +563,11 @@ async def on_change_model(action):
445
  @cl.action_callback("back_to_main")
446
  async def on_back_to_main(action):
447
  """Handle back to main menu action"""
 
 
 
 
 
448
  actions = UIService.create_action_buttons(app_state)
449
  await cl.Message(
450
  content="📋 **Menu chính**\n\nSử dụng các nút bên dưới để:",
@@ -455,18 +578,33 @@ async def on_back_to_main(action):
455
 
456
  @cl.action_callback("select_model_0")
457
  async def on_select_model_0(action):
 
 
 
 
 
458
  StateManager.change_model(app_state, "Gemini 2.0 Flash")
459
  await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.0 Flash**", app_state, author="assistant")
460
 
461
 
462
  @cl.action_callback("select_model_1")
463
  async def on_select_model_1(action):
 
 
 
 
 
464
  StateManager.change_model(app_state, "Gemini 2.5 Flash Lite")
465
  await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.5 Flash Lite**", app_state, author="assistant")
466
 
467
 
468
  @cl.action_callback("select_model_2")
469
  async def on_select_model_2(action):
 
 
 
 
 
470
  StateManager.change_model(app_state, "Gemini 2.0 Flash Lite")
471
  await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.0 Flash Lite**", app_state, author="assistant")
472
 
@@ -474,6 +612,11 @@ async def on_select_model_2(action):
474
  @cl.on_message
475
  async def main(message: cl.Message):
476
  """Main message handler"""
 
 
 
 
 
477
  # Handle images if present
478
  image_path = None
479
  if message.elements:
@@ -485,11 +628,11 @@ async def main(message: cl.Message):
485
  # Show typing animation
486
  typing_msg = await UIService.create_typing_animation()
487
 
488
- # Get response from API - injecting state as dependency
489
  response = await ChatService.respond_to_chat(app_state, message.content, image_path)
490
 
491
  # Update the typing message with final response and buttons
492
  typing_msg.content = response
493
  typing_msg.actions = UIService.create_action_buttons(app_state)
494
- typing_msg.author = "assistant" # Ensure author is set
495
  await typing_msg.update()
 
7
  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"
 
16
  @dataclass
17
  class ConversationState:
18
  """Data class to hold conversation state"""
19
+ session_id: Optional[str] = None
20
  specs_advantages: Dict[str, Any] = field(default_factory=dict)
21
  solution_packages: List[str] = field(default_factory=list)
22
  raw_documents: Optional[Dict[str, Any]] = None
 
26
 
27
  def reset(self):
28
  """Reset state to initial values"""
29
+ self.session_id = None
30
  self.specs_advantages = {}
31
  self.solution_packages = []
32
  self.raw_documents = None
 
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
+ requests.post(f"{API_BASE_URL}/clear_memory",
70
+ json={"reset_cache": True, "reset_model": False, "session_id": state.session_id})
71
+ except Exception as e:
72
+ print(f"Warning: clear_memory failed: {e}")
73
 
74
+ # Reset state but keep session_id
75
+ session_id = state.session_id
76
  state.reset()
77
+ state.session_id = session_id
78
 
79
  @staticmethod
80
  def change_model(state: ConversationState, model_name: str):
81
  """Change the selected model"""
82
+ if API_BASE_URL:
83
+ try:
84
+ requests.post(f"{API_BASE_URL}/set_model",
85
+ json={"model_name": model_name})
86
+ state.selected_model = model_name
87
+ except Exception as e:
88
+ print(f"Warning: set_model failed: {e}")
89
+ else:
90
  state.selected_model = model_name
 
 
91
 
92
  @staticmethod
93
  def toggle_product_model_search(state: ConversationState):
 
105
  image_path: Optional[str] = None
106
  ) -> str:
107
  """Handle chat responses with image support"""
108
+ print(f"🔄 === DEBUG STATE ===\n Chat request with model: {state.selected_model}, Product Model Search: {state.product_model_search}, Session ID: {state.session_id}")
109
  start = time.perf_counter()
110
 
111
+ if not API_BASE_URL:
112
+ return "Error: API_BASE_URL not configured"
113
+
114
+ if not state.session_id:
115
+ return "Error: Session ID not initialized"
116
+
117
  # Call API
118
  try:
119
  if image_path:
 
121
  files = {"image": f}
122
  data = {
123
  "message": message,
124
+ "product_model_search": str(state.product_model_search).lower(),
125
+ "session_id": state.session_id
126
  }
127
  resp = requests.post(
128
  f"{API_BASE_URL}/chat_with_image", files=files, data=data, timeout=120)
129
  else:
130
  payload = {
131
  "message": message,
132
+ "session_id": state.session_id,
133
  "debug": "Normal",
134
  "product_model_search": state.product_model_search
135
  }
 
158
  state.raw_documents = raw_documents
159
  state.outputs = outputs
160
 
161
+ # Filter products based on query
162
  if state.specs_advantages is not None:
163
  ChatService.get_specific_product_from_query(message, state)
164
 
 
189
 
190
  state.specs_advantages = new_specs_advantages
191
 
192
+
193
  class DisplayService:
194
  """Handles display-related operations"""
195
 
 
346
  @staticmethod
347
  def create_action_buttons(state: ConversationState):
348
  """Create persistent action buttons"""
 
349
  search_status = "🔍 Tìm theo mã sản phẩm (Đang tắt)" if not state.product_model_search else "🔍 Tìm theo mã sản phẩm (Đang bật)"
350
 
351
  return [
 
395
  return msg
396
 
397
 
398
+ # HELPER FUNCTIONS: Session management with proper error handling
399
+ def ensure_session_state() -> Optional[ConversationState]:
400
+ """Ensure session state exists, create if not"""
401
+ try:
402
+ session_id = cl.user_session.get("session_id")
403
+
404
+ if not session_id:
405
+ # Tạo session ID mới nếu không có
406
+ session_id = str(uuid.uuid4())
407
+ cl.user_session.set("session_id", session_id)
408
+ print(f"🔄 Created fallback session ID: {session_id}")
409
+
410
+ return StateManager.get_or_create_session_state(session_id)
411
+
412
+ except Exception as e:
413
+ print(f"⚠️ Error ensuring session state: {e}")
414
+ return None
415
+
416
+
417
+ def get_current_session_state() -> Optional[ConversationState]:
418
+ """Get current session state using Chainlit's session system"""
419
+ try:
420
+ # Use Chainlit's user session to get unique session ID
421
+ chainlit_session_id = cl.user_session.get("session_id") # Fixed: added key parameter
422
+
423
+ if chainlit_session_id:
424
+ return StateManager.get_or_create_session_state(chainlit_session_id)
425
+ else:
426
+ print("⚠️ No Chainlit session ID found")
427
+ return None
428
+ except Exception as e:
429
+ print(f"⚠️ Error getting session state: {e}")
430
+ return None
431
 
432
 
433
  @cl.on_chat_start
434
  async def on_chat_start():
435
  """Initialize the chat session"""
436
+ # Generate unique session ID for this browser tab/session
437
+ session_id = str(uuid.uuid4())
438
+
439
+ # Store session ID in Chainlit's user session
440
+ cl.user_session.set("session_id", session_id)
441
+
442
+ # Get or create session state
443
+ app_state = StateManager.get_or_create_session_state(session_id)
444
+
445
+ # await StateManager.clear_chat_state(app_state)
446
+
447
  await cl.Message(
448
+ content=f"🛍️ **RangDong Sales Agent** (Session: {session_id[:8]}...)\n\nXin chào! Tôi có thể giúp bạn tìm kiếm và tư vấn sản phẩm RangDong. Hãy thử các câu hỏi mẫu:\n\n- Tìm sản phẩm bình giữ nhiệt dung tích dưới 2 lít\n- Tìm sản phẩm ổ cắm thông minh\n- Tư vấn cho tôi đèn học chống cận cho con gái của tôi học lớp 6",
449
  author="assistant"
450
  ).send()
451
 
 
458
  ).send()
459
 
460
 
461
+ @cl.on_chat_end
462
+ async def on_chat_end():
463
+ """Clean up when chat session ends"""
464
+ try:
465
+ # Fixed: get session_id with specific key
466
+ session_id = cl.user_session.get("session_id")
467
+
468
+ if session_id:
469
+ # Get or create session state
470
+ app_state = StateManager.get_or_create_session_state(session_id)
471
+ await StateManager.clear_chat_state(app_state)
472
+ StateManager.cleanup_session(session_id)
473
+
474
+ except Exception as e:
475
+ print(f"⚠️ Error during cleanup: {e}")
476
+
477
+
478
+
479
+ # ACTION CALLBACKS - All use ensure_session_state() for better reliability
480
  @cl.action_callback("show_specs")
481
  async def on_show_specs(action):
482
  """Handle show specifications action"""
483
+ app_state = ensure_session_state()
484
+ if app_state is None:
485
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
486
+ return
487
+
488
  specs_content = DisplayService.show_specs(app_state)
489
  await UIService.send_message_with_buttons(specs_content, app_state, author="assistant")
490
 
 
492
  @cl.action_callback("show_advantages")
493
  async def on_show_advantages(action):
494
  """Handle show advantages action"""
495
+ app_state = ensure_session_state()
496
+ if app_state is None:
497
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
498
+ return
499
+
500
  adv_content = DisplayService.show_advantages(app_state)
501
  await UIService.send_message_with_buttons(adv_content, app_state, author="assistant")
502
 
 
504
  @cl.action_callback("show_packages")
505
  async def on_show_packages(action):
506
  """Handle show packages action"""
507
+ app_state = ensure_session_state()
508
+ if app_state is None:
509
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
510
+ return
511
+
512
  pkg_content = DisplayService.show_solution_packages(app_state)
513
  await UIService.send_message_with_buttons(pkg_content, app_state, author="assistant")
514
 
 
516
  @cl.action_callback("toggle_product_search")
517
  async def on_toggle_product_search(action):
518
  """Handle toggle product model search action"""
519
+ app_state = ensure_session_state()
520
+ if app_state is None:
521
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
522
+ return
523
+
524
  StateManager.toggle_product_model_search(app_state)
525
 
526
  status_message = (
 
537
  @cl.action_callback("change_model")
538
  async def on_change_model(action):
539
  """Handle model change action"""
540
+ app_state = ensure_session_state()
541
+ if app_state is None:
542
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
543
+ return
544
+
545
  models = ["Gemini 2.0 Flash", "Gemini 2.5 Flash Lite", "Gemini 2.0 Flash Lite"]
546
 
 
547
  model_actions = [
548
  cl.Action(name=f"select_model_{i}", value=model, label=model, payload={"model": model})
549
  for i, model in enumerate(models)
550
  ]
551
 
 
552
  model_actions.append(
553
  cl.Action(name="back_to_main", value="back", label="🔙 Quay lại", payload={"action": "back"})
554
  )
 
563
  @cl.action_callback("back_to_main")
564
  async def on_back_to_main(action):
565
  """Handle back to main menu action"""
566
+ app_state = ensure_session_state()
567
+ if app_state is None:
568
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
569
+ return
570
+
571
  actions = UIService.create_action_buttons(app_state)
572
  await cl.Message(
573
  content="📋 **Menu chính**\n\nSử dụng các nút bên dưới để:",
 
578
 
579
  @cl.action_callback("select_model_0")
580
  async def on_select_model_0(action):
581
+ app_state = ensure_session_state()
582
+ if app_state is None:
583
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
584
+ return
585
+
586
  StateManager.change_model(app_state, "Gemini 2.0 Flash")
587
  await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.0 Flash**", app_state, author="assistant")
588
 
589
 
590
  @cl.action_callback("select_model_1")
591
  async def on_select_model_1(action):
592
+ app_state = ensure_session_state()
593
+ if app_state is None:
594
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
595
+ return
596
+
597
  StateManager.change_model(app_state, "Gemini 2.5 Flash Lite")
598
  await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.5 Flash Lite**", app_state, author="assistant")
599
 
600
 
601
  @cl.action_callback("select_model_2")
602
  async def on_select_model_2(action):
603
+ app_state = ensure_session_state()
604
+ if app_state is None:
605
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
606
+ return
607
+
608
  StateManager.change_model(app_state, "Gemini 2.0 Flash Lite")
609
  await UIService.send_message_with_buttons("✅ Đã chuyển sang **Gemini 2.0 Flash Lite**", app_state, author="assistant")
610
 
 
612
  @cl.on_message
613
  async def main(message: cl.Message):
614
  """Main message handler"""
615
+ app_state = ensure_session_state()
616
+ if app_state is None:
617
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
618
+ return
619
+
620
  # Handle images if present
621
  image_path = None
622
  if message.elements:
 
628
  # Show typing animation
629
  typing_msg = await UIService.create_typing_animation()
630
 
631
+ # Get response from API
632
  response = await ChatService.respond_to_chat(app_state, message.content, image_path)
633
 
634
  # Update the typing message with final response and buttons
635
  typing_msg.content = response
636
  typing_msg.actions = UIService.create_action_buttons(app_state)
637
+ typing_msg.author = "assistant"
638
  await typing_msg.update()