import streamlit as st from transformers import ( pipeline, AutoModelForSequenceClassification, AutoTokenizer ) import torch import re # ===== CONSTANTS ===== MAX_CHARS = 1500 # Increased character limit SUPPORTED_LANGUAGES = { 'en': 'English', 'zh': 'Chinese', 'yue': 'Cantonese', 'ja': 'Japanese', 'ko': 'Korean' } # ===== ASPECT CONFIGURATION ===== aspect_map = { # Location related "location": ["location", "near", "close", "access", "transport", "distance", "area", "tsim sha tsui", "kowloon"], "view": ["view", "scenery", "vista", "panorama", "outlook", "skyline"], "parking": ["parking", "valet", "garage", "car park", "vehicle"], # Room related "room comfort": ["comfortable", "bed", "pillows", "mattress", "linens", "cozy", "hard", "soft"], "room cleanliness": ["clean", "dirty", "spotless", "stains", "hygiene", "sanitation", "dusty"], "room amenities": ["amenities", "minibar", "coffee", "tea", "fridge", "facilities", "tv", "kettle"], "bathroom": ["bathroom", "shower", "toilet", "sink", "towel", "faucet", "toiletries"], # Service related "staff service": ["staff", "friendly", "helpful", "rude", "welcoming", "employee", "manager"], "reception": ["reception", "check-in", "check-out", "front desk", "welcome", "registration"], "housekeeping": ["housekeeping", "maid", "cleaning", "towels", "service", "turndown"], "concierge": ["concierge", "recommendation", "advice", "tips", "guidance", "directions"], "room service": ["room service", "food delivery", "order", "meal", "tray"], # Facilities "dining": ["breakfast", "dinner", "restaurant", "meal", "food", "buffet", "lunch"], "bar": ["bar", "drinks", "cocktail", "wine", "lounge", "happy hour"], "pool": ["pool", "swimming", "jacuzzi", "sun lounger", "deck", "towels"], "spa": ["spa", "massage", "treatment", "relax", "wellness", "sauna"], "fitness": ["gym", "fitness", "exercise", "workout", "training", "weights"], # Technical "Wi-Fi": ["wifi", "internet", "connection", "online", "network", "speed"], "AC": ["air conditioning", "AC", "temperature", "heating", "cooling", "ventilation"], "elevator": ["elevator", "lift", "escalator", "vertical transport", "wait"], # Value "pricing": ["price", "expensive", "cheap", "value", "rate", "cost", "worth"], "extra charges": ["charge", "fee", "bill", "surcharge", "additional", "hidden"] } aspect_responses = { "location": "We're delighted you enjoyed our prime location in the heart of Tsim Sha Tsui, with convenient access to Nathan Road shopping and the Star Ferry pier.", "view": "It's wonderful to hear you appreciated the beautiful harbor or city skyline views from your room.", "room comfort": "Our housekeeping team takes special care with our pillow menu and mattress toppers to ensure your comfort.", "room cleanliness": "Your commendation of our cleanliness standards means a lot to our dedicated housekeeping staff.", "staff service": "Your kind words about our team, especially {staff_name}, have been shared with them - such recognition means everything to us.", "reception": "We're pleased our front desk team made your arrival and departure experience seamless.", "spa": "Our award-winning spa therapists will be delighted you enjoyed their signature treatments.", "pool": "We're glad you had a refreshing time at our rooftop pool with its stunning city views.", "dining": "Thank you for appreciating our culinary offerings at The Burgeroom and Chinese Restaurant - we've shared your feedback with Executive Chef Wong.", "concierge": "We're happy our concierge team could enhance your stay with their local expertise and recommendations.", "fitness": "It's great to hear you made use of our 24-hour fitness center with its panoramic views.", "room service": "We're pleased our 24-hour in-room dining met your expectations for both quality and timeliness.", "parking": "We're glad our convenient valet parking service made your arrival experience hassle-free.", "bathroom": "Our housekeeping team takes special pride in maintaining our marble bathrooms with premium amenities." } improvement_actions = { "AC": "completed a comprehensive inspection and maintenance of all air conditioning units", "housekeeping": "implemented additional training for our housekeeping team and revised cleaning schedules", "bathroom": "conducted deep cleaning of all bathrooms and replenished premium toiletries", "parking": "introduced new digital key management with our valet service to reduce wait times", "dining": "reviewed all menu pricing and quality standards with our culinary leadership team", "reception": "provided enhanced customer service training focused on cultural sensitivity", "elevator": "performed full servicing of all elevators and adjusted peak-time scheduling", "room amenities": "begun upgrading in-room amenities including new coffee machines and smart TVs", "Wi-Fi": "upgraded our network infrastructure to provide faster and more reliable internet", "noise": "initiated soundproofing improvements in corridors and between rooms", "pricing": "started a comprehensive review of our pricing structure and value proposition", "room service": "revised our in-room dining operations to improve delivery times", "view": "scheduled window cleaning and tree trimming to maintain optimal views", "fitness": "upgraded gym equipment based on guest feedback about variety" } # ===== MODEL LOADING ===== @st.cache_resource def load_sentiment_model(): model = AutoModelForSequenceClassification.from_pretrained("smtsead/fine_tuned_bertweet_hotel") tokenizer = AutoTokenizer.from_pretrained('finiteautomata/bertweet-base-sentiment-analysis') return model, tokenizer @st.cache_resource def load_aspect_classifier(): return pipeline("zero-shot-classification", model="MoritzLaurer/deberta-v3-base-zeroshot-v1.1-all-33") # ===== CORE FUNCTIONS ===== def analyze_sentiment(text, model, tokenizer): inputs = tokenizer(text, padding=True, truncation=True, max_length=512, return_tensors='pt') with torch.no_grad(): outputs = model(**inputs) probs = torch.nn.functional.softmax(outputs.logits, dim=-1) predicted_label = torch.argmax(probs).item() confidence = torch.max(probs).item() return { 'label': predicted_label, 'confidence': f"{confidence:.0%}", 'sentiment': 'POSITIVE' if predicted_label else 'NEGATIVE' } def detect_aspects(text, aspect_classifier): relevant_aspects = [] text_lower = text.lower() for aspect, keywords in aspect_map.items(): if any(re.search(rf'\b{kw}\b', text_lower) for kw in keywords): relevant_aspects.append(aspect) if relevant_aspects: result = aspect_classifier( text, candidate_labels=relevant_aspects, multi_label=True, hypothesis_template="This review discusses the hotel's {}." ) return [(aspect, f"{score:.0%}") for aspect, score in zip(result['labels'], result['scores']) if score > 0.6] return [] def generate_response(sentiment, aspects, original_text): # Personalization guest_name = "" name_match = re.search(r"(Mr\.|Ms\.|Mrs\.)\s(\w+)", original_text, re.IGNORECASE) if name_match: guest_name = f" {name_match.group(2)}" # Staff name extraction staff_name = "" staff_match = re.search(r"(receptionist|manager|concierge|chef)\s(\w+)", original_text, re.IGNORECASE) if staff_match: staff_name = staff_match.group(2) if sentiment['label'] == 1: response = f"""Dear{guest_name if guest_name else ' Valued Guest'}, Thank you for choosing The Kimberley Hotel Hong Kong and for sharing your wonderful feedback!""" # Add relevant aspect responses added_aspects = set() for aspect, _ in aspects: if aspect in aspect_responses: response_text = aspect_responses[aspect] if "{staff_name}" in response_text and staff_name: response_text = response_text.format(staff_name=staff_name) response += "\n\n" + response_text added_aspects.add(aspect) if len(added_aspects) >= 3: # Limit to 3 main points break # Special offers if "room" in added_aspects or "dining" in added_aspects: response += "\n\nAs a token of our appreciation, we'd like to offer you a complimentary room upgrade or dining credit on your next stay. Simply mention code VIP2024 when booking." response += "\n\nWe look forward to welcoming you back to your home in Hong Kong!\n\nWarm regards," else: response = f"""Dear{guest_name if guest_name else ' Guest'}, Thank you for your valuable feedback - we sincerely apologize that your experience didn't meet our usual high standards.""" # Add improvement actions added_improvements = set() for aspect, _ in aspects: if aspect in improvement_actions: response += f"\n\nRegarding your comments about the {aspect}, we've {improvement_actions[aspect]}." added_improvements.add(aspect) if len(added_improvements) >= 2: # Limit to 2 main improvements break # Recovery offer recovery_offer = "\n\nTo make amends, we'd like to offer you:" if "room" in added_improvements: recovery_offer += "\n- One night complimentary room upgrade" if "dining" in added_improvements: recovery_offer += "\n- HKD 300 dining credit at our restaurants" if not ("room" in added_improvements or "dining" in added_improvements): recovery_offer += "\n- 15% discount on your next stay" response += recovery_offer response += "\n\nPlease contact our Guest Relations Manager Ms. Chan directly at gr@kimberleyhotel.com.hk to arrange this." response += "\n\nWe hope for another opportunity to provide you with the exceptional experience we're known for.\n\nSincerely," return response + "\nMichael Wong\nGuest Experience Manager\nThe Kimberley Hotel Hong Kong\n+852 1234 5678" # ===== STREAMLIT UI ===== def main(): # Page Config st.set_page_config( page_title="Kimberley Review Assistant", page_icon="🏨", layout="centered" ) # Custom CSS st.markdown(""" """, unsafe_allow_html=True) # Header st.markdown('
The Kimberley Hotel Hong Kong
', unsafe_allow_html=True) st.markdown('
Guest Review Analysis System
', unsafe_allow_html=True) # Supported Languages st.markdown("**Supported Review Languages:**") lang_cols = st.columns(5) for i, (code, name) in enumerate(SUPPORTED_LANGUAGES.items()): lang_cols[i].markdown(f'
{name}
', unsafe_allow_html=True) # Review Input with Character Counter review = st.text_area("**Paste Guest Review:**", height=250, max_chars=MAX_CHARS, placeholder=f"Enter review in any supported language (max {MAX_CHARS} characters)...", key="review_input") char_count = len(st.session_state.review_input) if 'review_input' in st.session_state else 0 char_class = "warning" if char_count > MAX_CHARS else "" st.markdown(f'
{char_count}/{MAX_CHARS} characters
', unsafe_allow_html=True) if st.button("Analyze & Generate Response", type="primary"): if not review.strip(): st.error("Please enter a review") return if char_count > MAX_CHARS: st.warning(f"Review truncated to {MAX_CHARS} characters for analysis") review = review[:MAX_CHARS] with st.spinner("Analyzing feedback..."): # Load models sentiment_model, tokenizer = load_sentiment_model() aspect_classifier = load_aspect_classifier() # Process review sentiment = analyze_sentiment(review, sentiment_model, tokenizer) aspects = detect_aspects(review, aspect_classifier) response = generate_response(sentiment, aspects, review) # Display results st.divider() # Sentiment and Aspects col1, col2 = st.columns(2) with col1: st.markdown("### Sentiment Analysis") sentiment_icon = "✅" if sentiment['label'] == 1 else "⚠️" st.markdown(f"{sentiment_icon} **{sentiment['sentiment']}**") st.caption(f"Confidence level: {sentiment['confidence']}") with col2: st.markdown("### Key Aspects Detected") if aspects: for aspect, score in sorted(aspects, key=lambda x: float(x[1][:-1]), reverse=True): st.markdown(f'
{aspect} ({score})
', unsafe_allow_html=True) else: st.markdown("_No specific aspects detected_") # Generated Response st.divider() st.markdown("### Draft Response") st.markdown(f'
{response}
', unsafe_allow_html=True) # Copy button if st.button("Copy Response to Clipboard"): st.session_state.copied = True st.rerun() if st.session_state.get("copied", False): st.success("Response copied to clipboard!") st.session_state.copied = False if __name__ == "__main__": main()