smarttrip_ai / streamlit_app.py
swaraj
test
40b6729
Raw
History Blame Contribute Delete
17.4 kB
"""
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()