""" SmartTrip AI — Agentic Travel Planner Streamlit chatbot interface with login, multi-turn chat, admin history view, per-user chat persistence (SQLite), and daily rotating logs. """ import os import uuid import streamlit as st from dotenv import load_dotenv from langchain_core.messages import HumanMessage from agent.agentic_workflow import GraphBuilder from utils.auth import authenticate, get_all_users, get_demo_credentials_md from utils.chat_store import ( init_db, save_chat, get_user_chats, get_all_chats, get_users_with_chats, get_chat_stats, ) from logger.logging import get_logger load_dotenv() logger = get_logger("smarttrip_ui") # Initialise DB tables on startup (idempotent) init_db() # ───────────────────────────────────────────────────────────────────────────── # Page config (must be the very first Streamlit call) # ───────────────────────────────────────────────────────────────────────────── st.set_page_config( page_title="🌍 SmartTrip AI", page_icon="🌍", layout="wide", initial_sidebar_state="expanded", ) # ───────────────────────────────────────────────────────────────────────────── # Environment check — called once per session to warn about missing keys # ───────────────────────────────────────────────────────────────────────────── REQUIRED_ENV_VARS = [ "MODEL_PROVIDER", "MODEL_NAME", "MODEL_BASE_URL", "OLLAMA_API_KEY", "OPENWEATHERMAP_API_KEY", "TAVILY_API_KEY", ] def get_missing_keys() -> list: return [k for k in REQUIRED_ENV_VARS if not os.getenv(k)] # ───────────────────────────────────────────────────────────────────────────── # Agent graph — cached but ONLY when successfully built (never cache None) # ───────────────────────────────────────────────────────────────────────────── @st.cache_resource(show_spinner=False) def get_agent_graph(): """Build and compile the LangGraph agent once; reuse for all users.""" missing = get_missing_keys() if missing: logger.error(f"Cannot build agent — missing env vars: {missing}") raise RuntimeError( f"Missing required environment variables: {', '.join(missing)}\n\n" "On Hugging Face Spaces: go to **Settings → Variables and secrets** " "and add each key listed above." ) provider = os.getenv("MODEL_PROVIDER", "ollama") logger.info(f"Building LangGraph agent with provider='{provider}'") gb = GraphBuilder(model_provider=provider) return gb() # ───────────────────────────────────────────────────────────────────────────── # Session state helpers # ───────────────────────────────────────────────────────────────────────────── def init_session() -> None: defaults = { "authenticated": False, "username": None, "role": None, "display_name": None, "messages": [], "session_id": str(uuid.uuid4()), "admin_view": False, } for key, value in defaults.items(): if key not in st.session_state: st.session_state[key] = value def logout() -> None: for key in list(st.session_state.keys()): del st.session_state[key] st.rerun() # ───────────────────────────────────────────────────────────────────────────── # Sidebar # ───────────────────────────────────────────────────────────────────────────── def render_sidebar() -> None: with st.sidebar: st.markdown("## 🌍 SmartTrip AI") st.markdown("---") st.markdown(f"**👤 {st.session_state.display_name}**") st.markdown(f"Role: `{st.session_state.role}`") st.markdown("---") if st.button("🗑️ New Chat", use_container_width=True): st.session_state.messages = [] st.session_state.session_id = str(uuid.uuid4()) st.session_state.admin_view = False st.rerun() if st.button("🚪 Logout", use_container_width=True): logout() st.markdown("---") st.markdown("**🛠️ Available Tools**") st.markdown( "- 🌤️ Weather & Forecast\n" "- 📍 Attractions & Places\n" "- 🍽️ Restaurants\n" "- 🏄 Activities\n" "- 🚌 Transportation\n" "- 💰 Expense Calculator\n" "- 💱 Currency Converter" ) if st.session_state.role == "admin": st.markdown("---") st.markdown("**🛡️ Admin Panel**") if not st.session_state.admin_view: if st.button("📊 View User Chat History", use_container_width=True): st.session_state.admin_view = True st.rerun() else: if st.button("💬 Back to Chat", use_container_width=True): st.session_state.admin_view = False st.rerun() st.markdown("---") st.caption("SmartTrip AI © 2025 | Demo Mode") # ───────────────────────────────────────────────────────────────────────────── # Login page # ───────────────────────────────────────────────────────────────────────────── def render_login_page() -> None: # Top-right demo credentials panel title_col, creds_col = st.columns([3, 1]) with title_col: st.markdown("## 🌍 SmartTrip AI — Your Agentic Travel Planner") st.markdown( "Plan personalised trips with real-time weather, places, restaurants, " "activities, and smart expense breakdowns — all in one conversation." ) with creds_col: with st.expander("🔑 Demo Credentials", expanded=True): st.markdown(get_demo_credentials_md()) st.markdown("---") _, center, _ = st.columns([1, 2, 1]) with center: st.subheader("🔐 Login to get started") with st.form("login_form", clear_on_submit=False): username = st.text_input("Username", placeholder="e.g. user1") password = st.text_input("Password", type="password", placeholder="Enter password") submitted = st.form_submit_button("Login →", use_container_width=True) if submitted: if not username.strip() or not password.strip(): st.error("Please enter both username and password.") else: user_data = authenticate(username, password) if user_data: st.session_state.authenticated = True st.session_state.username = user_data["username"] st.session_state.role = user_data["role"] st.session_state.display_name = user_data["display_name"] st.session_state.messages = [] st.session_state.session_id = str(uuid.uuid4()) st.session_state.admin_view = False logger.info( f"Login successful: '{username}' (role={user_data['role']})" ) st.rerun() else: st.error( "❌ Invalid username or password. " "Use the demo credentials shown at the top-right." ) logger.warning( f"Failed login attempt for username='{username}'" ) # ───────────────────────────────────────────────────────────────────────────── # Admin: chat history viewer # ───────────────────────────────────────────────────────────────────────────── def render_admin_page() -> None: st.title("🛡️ Admin Panel — User Chat History") stats = get_chat_stats() c1, c2, c3 = st.columns(3) c1.metric("💬 Total Messages", stats["total_messages"]) c2.metric("👥 Unique Users", stats["unique_users"]) c3.metric("🔗 Sessions", stats["unique_sessions"]) st.markdown("---") all_usernames = get_all_users() filter_choice = st.selectbox( "👤 Filter by user", ["— All Users —"] + all_usernames, index=0, ) if filter_choice == "— All Users —": chats = get_all_chats() header = "All Users" else: chats = get_user_chats(filter_choice) header = filter_choice if not chats: st.info( f"No chat history found for **{header}** yet." if header != "All Users" else "No chat history yet — users haven't started chatting." ) return st.markdown(f"### 📋 {len(chats)} message(s) — **{header}**") for chat in chats: ts = chat["timestamp"][:19] session_short = chat["session_id"][:8] user = chat["username"] status_badge = "✅" if chat.get("status") == "success" else "❌" with st.expander( f"{status_badge} `{ts}` | 👤 **{user}** | session `{session_short}…`" ): st.markdown("**🧑 User asked:**") st.info(chat["user_message"]) st.markdown("**🤖 SmartTrip AI replied:**") st.text_area( label="Response", value=chat["bot_response"], height=200, disabled=True, label_visibility="collapsed", ) if chat.get("status") != "success": st.error(f"Status: {chat['status']}") # ───────────────────────────────────────────────────────────────────────────── # Chat page # ───────────────────────────────────────────────────────────────────────────── def render_chat_page() -> None: # Header with top-right credentials reminder left_col, right_col = st.columns([3, 1]) with left_col: st.markdown("## 🌍 SmartTrip AI — Your Agentic Travel Planner") with right_col: with st.expander("🔑 Demo Credentials", expanded=False): st.markdown(get_demo_credentials_md()) st.markdown("---") # ── Warn immediately if any required env vars are missing ───────────────── missing = get_missing_keys() if missing: st.error( f"⚠️ **Missing environment variables:** `{'`, `'.join(missing)}`\n\n" "The AI cannot respond until these are set.\n\n" "**On Hugging Face Spaces:** go to **Settings → Variables and secrets** " "and add each key listed above, then restart the Space." ) return # Don't render the chat box if keys are missing # Welcome message when no conversation yet if not st.session_state.messages: with st.chat_message("assistant"): st.markdown( "👋 Hi! I'm **SmartTrip AI**, your intelligent travel planning companion!\n\n" "I can help you with:\n" "- 🗺️ Complete day-by-day itineraries (mainstream + off-beat)\n" "- 🌤️ Real-time weather & forecasts\n" "- 📍 Attractions, restaurants & activities\n" "- 🚌 Transportation options\n" "- 💰 Detailed cost breakdowns & daily budgets\n" "- 💱 Live currency conversion\n\n" "**Try:** *\"Plan a 5-day trip to Bali for 2 people with a budget of $1500\"*" ) # Render conversation history for msg in st.session_state.messages: with st.chat_message(msg["role"]): st.markdown(msg["content"]) # Chat input (Streamlit pins this to the bottom automatically) user_input = st.chat_input( "Ask me about your trip… e.g. Plan a 4-day trip to Rajasthan under ₹25,000" ) if not user_input: return # Show user bubble st.session_state.messages.append({"role": "user", "content": user_input}) with st.chat_message("user"): st.markdown(user_input) # Build multi-turn context: include last 6 messages (3 pairs) prior_msgs = st.session_state.messages[:-1] if prior_msgs: context_lines = [] for m in prior_msgs[-6:]: prefix = "User" if m["role"] == "user" else "Assistant" context_lines.append(f"{prefix}: {m['content']}") full_query = ( "Previous conversation:\n" + "\n".join(context_lines) + f"\n\nCurrent question: {user_input}" ) else: full_query = user_input # Invoke agent and stream response with st.chat_message("assistant"): with st.spinner("SmartTrip AI is planning your trip… 🗺️"): try: graph = get_agent_graph() output = graph.invoke({"messages": [HumanMessage(content=full_query)]}) if isinstance(output, dict) and "messages" in output: response = output["messages"][-1].content else: response = str(output) st.markdown(response) status = "success" logger.info( f"Query answered | user='{st.session_state.username}' " f"session='{st.session_state.session_id[:8]}'" ) except Exception as exc: err_msg = str(exc) response = ( f"⚠️ **Agent error:** {err_msg}\n\n" "Check that all API keys are set in **HF Space → Settings → Variables and secrets**." ) st.markdown(response) status = "error" logger.error( f"Agent error | user='{st.session_state.username}' | {exc}" ) logger.error( f"Agent error | user='{st.session_state.username}' | {exc}" ) st.session_state.messages.append({"role": "assistant", "content": response}) # Persist to SQLite save_chat( session_id=st.session_state.session_id, username=st.session_state.username, role=st.session_state.role, user_message=user_input, bot_response=response, status=status, ) # ───────────────────────────────────────────────────────────────────────────── # Entry point... # ───────────────────────────────────────────────────────────────────────────── def main() -> None: init_session() if not st.session_state.authenticated: render_login_page() return render_sidebar() if st.session_state.role == "admin" and st.session_state.admin_view: render_admin_page() else: render_chat_page() if __name__ == "__main__": main()