anhkhoiphan commited on
Commit
eecfd26
·
verified ·
1 Parent(s): 3861400

Thử nghiệm chống trigger on_chat_end khi websocket disconnect

Browse files
Files changed (1) hide show
  1. app.py +114 -276
app.py CHANGED
@@ -4,14 +4,13 @@ import chainlit as cl
4
  import pandas as pd
5
  import requests
6
  import asyncio
7
- import threading
8
  from typing import Dict, List, Any, Optional, Callable
9
  from dataclasses import dataclass, field
10
  import os
11
  import uuid
12
 
13
 
14
- API_BASE_URL = "https://sale-agent-m179.onrender.com"
15
 
16
 
17
  @dataclass
@@ -37,97 +36,34 @@ class ConversationState:
37
 
38
 
39
  class StateManager:
40
- """Thread-safe StateManager with duplicate prevention"""
41
 
 
42
  _session_states: Dict[str, ConversationState] = {}
43
- _cleanup_flags: Dict[str, bool] = {}
44
- _creation_locks: Dict[str, threading.Lock] = {}
45
- _cleanup_locks: Dict[str, threading.Lock] = {}
46
- _global_lock = threading.Lock()
47
 
48
  @staticmethod
49
- def get_session_state(session_id: str) -> Optional[ConversationState]:
50
- """Thread-safe get session state"""
51
- if session_id in StateManager._cleanup_flags:
52
- print(f"⚠️ Session {session_id[:8]}... is being cleaned up, returning None")
53
- return None
54
-
55
- return StateManager._session_states.get(session_id)
56
-
57
- @staticmethod
58
- def create_session_state(session_id: str) -> ConversationState:
59
- """Thread-safe create session state with duplicate prevention"""
60
- # Get or create lock for this session
61
- with StateManager._global_lock:
62
- if session_id not in StateManager._creation_locks:
63
- StateManager._creation_locks[session_id] = threading.Lock()
64
- lock = StateManager._creation_locks[session_id]
65
-
66
- # Use session-specific lock to prevent duplicate creation
67
- with lock:
68
- # Double-check if session already exists
69
- if session_id in StateManager._session_states:
70
- print(f"🔄 Session {session_id[:8]}... already exists, returning existing")
71
- return StateManager._session_states[session_id]
72
-
73
- # Check if being cleaned up
74
- if session_id in StateManager._cleanup_flags:
75
- print(f"⚠️ Session {session_id[:8]}... is being cleaned up, waiting...")
76
- # Wait a bit and retry
77
- time.sleep(0.1)
78
- return StateManager.create_session_state(session_id)
79
-
80
- # Create new session
81
  state = ConversationState()
82
  state.session_id = session_id
83
  StateManager._session_states[session_id] = state
84
- print(f"🆕 Created new session state for: {session_id[:8]}...")
85
- return state
 
 
 
86
 
87
  @staticmethod
88
  def cleanup_session(session_id: str):
89
- """Thread-safe cleanup with duplicate prevention"""
90
- # Get or create lock for this session
91
- with StateManager._global_lock:
92
- if session_id not in StateManager._cleanup_locks:
93
- StateManager._cleanup_locks[session_id] = threading.Lock()
94
- lock = StateManager._cleanup_locks[session_id]
95
-
96
- # Use session-specific lock to prevent duplicate cleanup
97
- with lock:
98
- # Check if already being cleaned up
99
- if session_id in StateManager._cleanup_flags:
100
- print(f"⚠️ Session {session_id[:8]}... already being cleaned up, skipping")
101
- return
102
-
103
- # Mark as being cleaned up
104
- StateManager._cleanup_flags[session_id] = True
105
-
106
- try:
107
- # Cleanup session data
108
- if session_id in StateManager._session_states:
109
- del StateManager._session_states[session_id]
110
- print(f"🗑️ Cleaned up session state for: {session_id[:8]}...")
111
- else:
112
- print(f"⚠️ No session state found for cleanup: {session_id[:8]}...")
113
-
114
- # Cleanup locks
115
- if session_id in StateManager._creation_locks:
116
- del StateManager._creation_locks[session_id]
117
-
118
- finally:
119
- # Always remove cleanup flag
120
- if session_id in StateManager._cleanup_flags:
121
- del StateManager._cleanup_flags[session_id]
122
-
123
- # Remove cleanup lock
124
- with StateManager._global_lock:
125
- if session_id in StateManager._cleanup_locks:
126
- del StateManager._cleanup_locks[session_id]
127
 
128
  @staticmethod
129
  async def clear_chat_state(state: ConversationState):
130
- """Clear chat state with better error handling"""
131
  if state.session_id is not None and API_BASE_URL:
132
  try:
133
  payload = {
@@ -135,10 +71,10 @@ class StateManager:
135
  "reset_model": False,
136
  "session_id": state.session_id
137
  }
138
- response = requests.post(f"{API_BASE_URL}/clear_memory", json=payload, timeout=30)
139
- print(f"Clear memory response: {response.status_code} for session: {state.session_id[:8]}...")
140
  except Exception as e:
141
- print(f"Warning: clear_memory failed for session {state.session_id[:8]}...: {e}")
142
 
143
  # Reset state but keep session_id
144
  session_id = state.session_id
@@ -166,7 +102,7 @@ class ChatService:
166
  image_path: Optional[str] = None
167
  ) -> str:
168
  """Handle chat responses with image support"""
169
- print(f"🔄 === DEBUG STATE ===\n Chat request with model: {state.selected_model}, Product Model Search: {state.product_model_search}, Session ID: {state.session_id[:8] if state.session_id else 'None'}...")
170
  start = time.perf_counter()
171
 
172
  if not API_BASE_URL:
@@ -498,134 +434,92 @@ class UIService:
498
  return msg
499
 
500
 
501
- # HELPER FUNCTIONS - Improved session management with duplicate prevention
502
- _message_processing = set() # Track processing messages to prevent duplicates
503
-
504
- def get_current_session_state() -> Optional[ConversationState]:
505
- """Get current session state with better error handling"""
506
  try:
507
  session_id = cl.user_session.get("session_id")
 
508
  if not session_id:
509
- print("⚠️ No session ID found in Chainlit session")
510
- return None
511
-
512
- return StateManager.get_session_state(session_id)
 
 
 
513
  except Exception as e:
514
- print(f"⚠️ Error getting session state: {e}")
515
  return None
516
 
517
 
518
- def ensure_session_state() -> Optional[ConversationState]:
519
- """Get existing session state or create new one if needed"""
520
  try:
521
- session_id = cl.user_session.get("session_id")
522
- if not session_id:
523
- # Create new session ID
524
- session_id = str(uuid.uuid4())
525
- cl.user_session.set("session_id", session_id)
526
- print(f"🔄 Created new session ID: {session_id[:8]}...")
527
 
528
- # Try to get existing session first
529
- state = StateManager.get_session_state(session_id)
530
- if state is None:
531
- # Create new session if not exists
532
- state = StateManager.create_session_state(session_id)
533
-
534
- return state
535
  except Exception as e:
536
- print(f"⚠️ Error ensuring session state: {e}")
537
  return None
538
 
539
 
540
  @cl.on_chat_start
541
  async def on_chat_start():
542
- """Initialize the chat session with duplicate prevention"""
543
- try:
544
- # Check if session already exists
545
- existing_session_id = cl.user_session.get("session_id")
546
- if existing_session_id:
547
- existing_state = StateManager.get_session_state(existing_session_id)
548
- if existing_state:
549
- print(f"🔄 Reusing existing session: {existing_session_id[:8]}...")
550
- session_id = existing_session_id
551
- app_state = existing_state
552
- else:
553
- # Session ID exists but state doesn't, create new state
554
- print(f"🔄 Recreating state for existing session: {existing_session_id[:8]}...")
555
- app_state = StateManager.create_session_state(existing_session_id)
556
- session_id = existing_session_id
557
- else:
558
- # Generate completely new session
559
- session_id = str(uuid.uuid4())
560
- cl.user_session.set("session_id", session_id)
561
- app_state = StateManager.create_session_state(session_id)
562
-
563
- print(f"📋 Session initialized: {session_id[:8]}...")
564
-
565
- await cl.Message(
566
- content=f"🛍️ **RangDong Sales Agent** (Session: {session_id[:8]}...)\n\n"
567
  f"Xin 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"
568
  f"- Tìm sản phẩm bình giữ nhiệt dung tích dưới 2 lít\n"
569
  f"- Tìm sản phẩm ổ cắm thông minh\n"
570
  f"- 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",
571
- author="assistant"
572
- ).send()
573
-
574
- # Create initial action buttons
575
- actions = UIService.create_start_buttons(app_state)
576
- await cl.Message(
577
- content="Sử dụng nút bên dưới để cấu hình:",
578
- actions=actions,
579
- author="assistant"
580
- ).send()
581
-
582
- except Exception as e:
583
- print(f"⚠️ Error in on_chat_start: {e}")
584
- await cl.Message(
585
- content="❌ Lỗi khởi tạo session. Vui lòng refresh trang.",
586
- author="assistant"
587
- ).send()
588
 
589
 
590
  @cl.on_chat_end
591
  async def on_chat_end():
592
- """Clean up when chat session ends with duplicate prevention"""
593
  try:
594
  session_id = cl.user_session.get("session_id")
595
- if not session_id:
596
- print("⚠️ No session ID found during cleanup")
597
- return
598
-
599
- print(f"🔚 Starting cleanup for session: {session_id[:8]}...")
600
 
601
- # Get existing session without creating new one
602
- app_state = StateManager.get_session_state(session_id)
603
-
604
- if app_state:
605
- # Clear chat state via API
606
  await StateManager.clear_chat_state(app_state)
607
- print(f"✅ Chat state cleared for session: {session_id[:8]}...")
 
608
  else:
609
- print(f"⚠️ No app state found for session: {session_id[:8]}...")
610
-
611
- # Always cleanup session from memory
612
- StateManager.cleanup_session(session_id)
613
- print(f"✅ Session cleanup completed for: {session_id[:8]}...")
614
-
615
  except Exception as e:
616
  print(f"⚠️ Error during cleanup: {e}")
 
617
 
618
-
619
- # ACTION CALLBACKS - Improved error handling
620
  @cl.action_callback("show_specs")
621
  async def on_show_specs(action):
622
  """Handle show specifications action"""
623
- app_state = get_current_session_state()
624
  if app_state is None:
625
- await cl.Message(
626
- content="❌ Session không tồn tại hoặc đã bị đóng. Vui lòng refresh trang để tạo session mới.",
627
- author="assistant"
628
- ).send()
629
  return
630
 
631
  specs_content = DisplayService.show_specs(app_state)
@@ -635,12 +529,9 @@ async def on_show_specs(action):
635
  @cl.action_callback("show_advantages")
636
  async def on_show_advantages(action):
637
  """Handle show advantages action"""
638
- app_state = get_current_session_state()
639
  if app_state is None:
640
- await cl.Message(
641
- content="❌ Session không tồn tại hoặc đã bị đóng. Vui lòng refresh trang để tạo session mới.",
642
- author="assistant"
643
- ).send()
644
  return
645
 
646
  adv_content = DisplayService.show_advantages(app_state)
@@ -650,42 +541,31 @@ async def on_show_advantages(action):
650
  @cl.action_callback("show_packages")
651
  async def on_show_packages(action):
652
  """Handle show packages action"""
653
- app_state = get_current_session_state()
654
  if app_state is None:
655
- await cl.Message(
656
- content="❌ Session không tồn tại hoặc đã bị đóng. Vui lòng refresh trang để tạo session mới.",
657
- author="assistant"
658
- ).send()
659
  return
660
 
661
  pkg_content = DisplayService.show_solution_packages(app_state)
662
  await UIService.send_message_with_buttons(pkg_content, app_state, author="assistant")
663
 
664
-
665
  @cl.action_callback("show_all_products")
666
  async def on_show_all_products(action):
667
  """Handle show all products action"""
668
- app_state = get_current_session_state()
669
  if app_state is None:
670
- await cl.Message(
671
- content="❌ Session không tồn tại hoặc đã bị đóng. Vui lòng refresh trang để tạo session mới.",
672
- author="assistant"
673
- ).send()
674
  return
675
 
676
  all_products_content = DisplayService.show_all_products_table(app_state)
677
  await UIService.send_message_with_buttons(all_products_content, app_state, author="assistant")
678
 
679
-
680
  @cl.action_callback("toggle_product_search")
681
  async def on_toggle_product_search(action):
682
  """Handle toggle product model search action"""
683
- app_state = get_current_session_state()
684
  if app_state is None:
685
- await cl.Message(
686
- content="❌ Session không tồn tại hoặc đã bị đóng. Vui lòng refresh trang để tạo session mới.",
687
- author="assistant"
688
- ).send()
689
  return
690
 
691
  StateManager.toggle_product_model_search(app_state)
@@ -704,12 +584,9 @@ async def on_toggle_product_search(action):
704
  @cl.action_callback("change_model")
705
  async def on_change_model(action):
706
  """Handle model change action"""
707
- app_state = get_current_session_state()
708
  if app_state is None:
709
- await cl.Message(
710
- content="❌ Session không tồn tại hoặc đã bị đóng. Vui lòng refresh trang để tạo session mới.",
711
- author="assistant"
712
- ).send()
713
  return
714
 
715
  models = ["Gemini 2.0 Flash", "Gemini 2.5 Flash Lite", "Gemini 2.0 Flash Lite"]
@@ -733,12 +610,9 @@ async def on_change_model(action):
733
  @cl.action_callback("back_to_main")
734
  async def on_back_to_main(action):
735
  """Handle back to main menu action"""
736
- app_state = get_current_session_state()
737
  if app_state is None:
738
- await cl.Message(
739
- content="❌ Session không tồn tại hoặc đã bị đóng. Vui lòng refresh trang để tạo session mới.",
740
- author="assistant"
741
- ).send()
742
  return
743
 
744
  actions = UIService.create_action_buttons(app_state)
@@ -751,12 +625,9 @@ async def on_back_to_main(action):
751
 
752
  @cl.action_callback("select_model_0")
753
  async def on_select_model_0(action):
754
- app_state = get_current_session_state()
755
  if app_state is None:
756
- await cl.Message(
757
- content="❌ Session không tồn tại hoặc đã bị đóng. Vui lòng refresh trang để tạo session mới.",
758
- author="assistant"
759
- ).send()
760
  return
761
 
762
  StateManager.change_model(app_state, "Gemini 2.0 Flash")
@@ -765,12 +636,9 @@ async def on_select_model_0(action):
765
 
766
  @cl.action_callback("select_model_1")
767
  async def on_select_model_1(action):
768
- app_state = get_current_session_state()
769
  if app_state is None:
770
- await cl.Message(
771
- content="❌ Session không tồn tại hoặc đã bị đóng. Vui lòng refresh trang để tạo session mới.",
772
- author="assistant"
773
- ).send()
774
  return
775
 
776
  StateManager.change_model(app_state, "Gemini 2.5 Flash Lite")
@@ -779,12 +647,9 @@ async def on_select_model_1(action):
779
 
780
  @cl.action_callback("select_model_2")
781
  async def on_select_model_2(action):
782
- app_state = get_current_session_state()
783
  if app_state is None:
784
- await cl.Message(
785
- content="❌ Session không tồn tại hoặc đã bị đóng. Vui lòng refresh trang để tạo session mới.",
786
- author="assistant"
787
- ).send()
788
  return
789
 
790
  StateManager.change_model(app_state, "Gemini 2.0 Flash Lite")
@@ -793,55 +658,28 @@ async def on_select_model_2(action):
793
 
794
  @cl.on_message
795
  async def main(message: cl.Message):
796
- """Main message handler with duplicate prevention"""
797
- try:
798
- # Create unique message ID to prevent duplicate processing
799
- message_id = f"{message.content[:50]}_{hash(message.content)}_{time.time()}"
800
-
801
- if message_id in _message_processing:
802
- print(f"⚠️ Message already being processed: {message_id}")
803
- return
804
-
805
- _message_processing.add(message_id)
806
-
807
- try:
808
- app_state = ensure_session_state()
809
- if app_state is None:
810
- await cl.Message(
811
- content="❌ Session không tồn tại hoặc đã bị đóng. Vui lòng refresh trang để tạo session mới.",
812
- author="assistant"
813
- ).send()
814
- return
815
-
816
- print(f"📝 Processing message for session: {app_state.session_id[:8]}...")
817
-
818
- # Handle images if present
819
- image_path = None
820
- if message.elements:
821
- for element in message.elements:
822
- if isinstance(element, cl.Image):
823
- image_path = element.path
824
- break
825
-
826
- # Show typing animation
827
- typing_msg = await UIService.create_typing_animation()
828
-
829
- # Get response from API
830
- response = await ChatService.respond_to_chat(app_state, message.content, image_path)
831
-
832
- # Update the typing message with final response and buttons
833
- typing_msg.content = response
834
- typing_msg.actions = UIService.create_action_buttons(app_state)
835
- typing_msg.author = "assistant"
836
- await typing_msg.update()
837
-
838
- finally:
839
- # Always remove from processing set
840
- _message_processing.discard(message_id)
841
-
842
- except Exception as e:
843
- print(f"⚠️ Error in message handler: {e}")
844
- await cl.Message(
845
- content="❌ Lỗi xử lý tin nhắn. Vui lòng thử lại.",
846
- author="assistant"
847
- ).send()
 
4
  import pandas as pd
5
  import requests
6
  import asyncio
 
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 = os.getenv("API_BASE_URL")
14
 
15
 
16
  @dataclass
 
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
  "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
 
102
  image_path: Optional[str] = None
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:
 
434
  return msg
435
 
436
 
437
+ # HELPER FUNCTIONS: Session management with proper error handling
438
+ def ensure_session_state() -> Optional[ConversationState]:
439
+ """Ensure session state exists, create if not"""
 
 
440
  try:
441
  session_id = cl.user_session.get("session_id")
442
+
443
  if not session_id:
444
+ # Chỉ tạo mới khi chưa (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"🆕 Created new session_id for new chat: {session_id}")
448
+
449
+ return StateManager.get_or_create_session_state(session_id)
450
+
451
  except Exception as e:
452
+ print(f"⚠️ Error ensuring session state: {e}")
453
  return None
454
 
455
 
456
+ 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)
464
+ else:
465
+ print("⚠️ No Chainlit session ID found")
466
+ return None
 
 
467
  except Exception as e:
468
+ print(f"⚠️ Error getting session state: {e}")
469
  return None
470
 
471
 
472
  @cl.on_chat_start
473
  async def on_chat_start():
474
+ """Initialize the chat session"""
475
+ session_id = cl.user_session.get("session_id")
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
+
482
+ await cl.Message(
483
+ content=f"🛍️ **RangDong Sales Agent** (Session: {session_id[:8]}...)\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  f"Xin 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"
485
  f"- Tìm sản phẩm bình giữ nhiệt dung tích dưới 2 lít\n"
486
  f"- Tìm sản phẩm ổ cắm thông minh\n"
487
  f"- 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",
488
+ author="assistant"
489
+ ).send()
490
+
491
+ actions = UIService.create_start_buttons(app_state)
492
+ await cl.Message(
493
+ content="Sử dụng nút bên dưới để cấu hình:",
494
+ actions=actions,
495
+ author="assistant"
496
+ ).send()
 
 
 
 
 
 
 
 
497
 
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
 
505
+ if session_id:
506
+ app_state = StateManager.get_or_create_session_state(session_id)
 
 
 
507
  await StateManager.clear_chat_state(app_state)
508
+ StateManager.cleanup_session(session_id)
509
+ print(f"✅ Properly cleaned session {session_id}")
510
  else:
511
+ print("⚠️ No session_id found in on_chat_end (maybe reconnect case)")
 
 
 
 
 
512
  except Exception as e:
513
  print(f"⚠️ Error during cleanup: {e}")
514
+
515
 
516
+ # ACTION CALLBACKS - All use ensure_session_state() for better reliability
 
517
  @cl.action_callback("show_specs")
518
  async def on_show_specs(action):
519
  """Handle show specifications action"""
520
+ app_state = ensure_session_state()
521
  if app_state is None:
522
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
 
 
 
523
  return
524
 
525
  specs_content = DisplayService.show_specs(app_state)
 
529
  @cl.action_callback("show_advantages")
530
  async def on_show_advantages(action):
531
  """Handle show advantages action"""
532
+ app_state = ensure_session_state()
533
  if app_state is None:
534
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
 
 
 
535
  return
536
 
537
  adv_content = DisplayService.show_advantages(app_state)
 
541
  @cl.action_callback("show_packages")
542
  async def on_show_packages(action):
543
  """Handle show packages action"""
544
+ app_state = ensure_session_state()
545
  if app_state is None:
546
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
 
 
 
547
  return
548
 
549
  pkg_content = DisplayService.show_solution_packages(app_state)
550
  await UIService.send_message_with_buttons(pkg_content, app_state, author="assistant")
551
 
 
552
  @cl.action_callback("show_all_products")
553
  async def on_show_all_products(action):
554
  """Handle show all products action"""
555
+ app_state = ensure_session_state()
556
  if app_state is None:
557
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
 
 
 
558
  return
559
 
560
  all_products_content = DisplayService.show_all_products_table(app_state)
561
  await UIService.send_message_with_buttons(all_products_content, app_state, author="assistant")
562
 
 
563
  @cl.action_callback("toggle_product_search")
564
  async def on_toggle_product_search(action):
565
  """Handle toggle product model search 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
  StateManager.toggle_product_model_search(app_state)
 
584
  @cl.action_callback("change_model")
585
  async def on_change_model(action):
586
  """Handle model change action"""
587
+ app_state = ensure_session_state()
588
  if app_state is None:
589
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
 
 
 
590
  return
591
 
592
  models = ["Gemini 2.0 Flash", "Gemini 2.5 Flash Lite", "Gemini 2.0 Flash Lite"]
 
610
  @cl.action_callback("back_to_main")
611
  async def on_back_to_main(action):
612
  """Handle back to main menu action"""
613
+ app_state = ensure_session_state()
614
  if app_state is None:
615
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
 
 
 
616
  return
617
 
618
  actions = UIService.create_action_buttons(app_state)
 
625
 
626
  @cl.action_callback("select_model_0")
627
  async def on_select_model_0(action):
628
+ app_state = ensure_session_state()
629
  if app_state is None:
630
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
 
 
 
631
  return
632
 
633
  StateManager.change_model(app_state, "Gemini 2.0 Flash")
 
636
 
637
  @cl.action_callback("select_model_1")
638
  async def on_select_model_1(action):
639
+ app_state = ensure_session_state()
640
  if app_state is None:
641
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
 
 
 
642
  return
643
 
644
  StateManager.change_model(app_state, "Gemini 2.5 Flash Lite")
 
647
 
648
  @cl.action_callback("select_model_2")
649
  async def on_select_model_2(action):
650
+ app_state = ensure_session_state()
651
  if app_state is None:
652
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
 
 
 
653
  return
654
 
655
  StateManager.change_model(app_state, "Gemini 2.0 Flash Lite")
 
658
 
659
  @cl.on_message
660
  async def main(message: cl.Message):
661
+ """Main message handler"""
662
+ app_state = ensure_session_state()
663
+ if app_state is None:
664
+ await cl.Message(content="Error: Session state not found", author="assistant").send()
665
+ return
666
+
667
+ # Handle images if present
668
+ image_path = None
669
+ if message.elements:
670
+ for element in message.elements:
671
+ if isinstance(element, cl.Image):
672
+ image_path = element.path
673
+ break
674
+
675
+ # Show typing animation
676
+ typing_msg = await UIService.create_typing_animation()
677
+
678
+ # Get response from API
679
+ response = await ChatService.respond_to_chat(app_state, message.content, image_path)
680
+
681
+ # Update the typing message with final response and buttons
682
+ typing_msg.content = response
683
+ typing_msg.actions = UIService.create_action_buttons(app_state)
684
+ typing_msg.author = "assistant"
685
+ await typing_msg.update()