Spaces:
Runtime error
Runtime error
| """ | |
| 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) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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() |