Spaces:
Running
Running
Thử nghiệm chống trigger on_chat_end khi websocket disconnect
Browse files
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 =
|
| 15 |
|
| 16 |
|
| 17 |
@dataclass
|
|
@@ -37,97 +36,34 @@ class ConversationState:
|
|
| 37 |
|
| 38 |
|
| 39 |
class StateManager:
|
| 40 |
-
"""
|
| 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
|
| 50 |
-
"""
|
| 51 |
-
if session_id in StateManager.
|
| 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
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
@staticmethod
|
| 88 |
def cleanup_session(session_id: str):
|
| 89 |
-
"""
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 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
|
| 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
|
| 139 |
-
print(f"Clear memory response: {response.status_code}
|
| 140 |
except Exception as e:
|
| 141 |
-
print(f"Warning: clear_memory failed
|
| 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
|
| 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
|
| 502 |
-
|
| 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 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
|
|
|
|
|
|
|
|
|
| 513 |
except Exception as e:
|
| 514 |
-
print(f"⚠️ Error
|
| 515 |
return None
|
| 516 |
|
| 517 |
|
| 518 |
-
def
|
| 519 |
-
"""Get
|
| 520 |
try:
|
| 521 |
-
|
| 522 |
-
|
| 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 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
return state
|
| 535 |
except Exception as e:
|
| 536 |
-
print(f"⚠️ Error
|
| 537 |
return None
|
| 538 |
|
| 539 |
|
| 540 |
@cl.on_chat_start
|
| 541 |
async def on_chat_start():
|
| 542 |
-
"""Initialize the chat session
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 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 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 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
|
| 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 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
if app_state:
|
| 605 |
-
# Clear chat state via API
|
| 606 |
await StateManager.clear_chat_state(app_state)
|
| 607 |
-
|
|
|
|
| 608 |
else:
|
| 609 |
-
print(
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 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 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"🆕 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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|