tinyChat / app_streamlit.py
aliMohammad16's picture
Rename app.py to app_streamlit.py
1fac276 verified
import streamlit as st
import pandas as pd
import json
from openai import OpenAI
import os
import uuid
import time
# Sample data - you'll need to create data.py or embed this data
restaurants_data = [
{"id": "r001", "name": "Spice Garden", "locality": "Downtown", "cuisine": "Indian", "price_range": "800-1200"},
{"id": "r002", "name": "Pizza Palace", "locality": "Mall Road", "cuisine": "Italian", "price_range": "400-800"},
{"id": "r003", "name": "Dragon House", "locality": "City Center", "cuisine": "Chinese", "price_range": "600-1000"},
{"id": "r004", "name": "Burger Junction", "locality": "Food Street", "cuisine": "American", "price_range": "300-600"},
{"id": "r005", "name": "Sushi Bar", "locality": "Downtown", "cuisine": "Japanese", "price_range": "1000-1500"},
]
reservation_data = [
{"reservation_id": 31005202500001, "restaurant_id": "r001", "user_name": "John Doe", "party_size": 4, "date": "2025-06-15", "time": "19:00", "special_requests": "", "status": "Confirmed"},
]
# Streamlit UI setup
st.set_page_config(page_title="foodieSpot", layout="centered")
class BookingState:
def __init__(self):
self.data = {
"reservation_id": None,
"state": None,
"cuisine_preference": None,
"location": None,
"date": None,
"time": None,
"party_size": None,
"special_requests": None,
"restaurant_id": None,
"user_name": None
}
def update(self, **kwargs):
for key, value in kwargs.items():
if key in self.data:
self.data[key] = value
else:
raise KeyError(f"Invalid key: '{key}' not in booking data.")
return self.check_state()
def check_state(self):
return {k: v for k, v in self.data.items() if v is not None}
def is_complete(self):
required = [
"cuisine_preference", "location", "date", "time", "party_size",
"restaurant_id", "user_name"
]
return all(self.data.get(k) is not None for k in required)
def reset(self):
for key in self.data:
self.data[key] = None
def to_dict(self):
return self.data.copy()
class ReservationManager:
def __init__(self, restaurants_df, reservation_df):
self.restaurants_df = restaurants_df
self.reservations_df = reservation_df
self.reservation_counter = 31005202500001
def _generate_reservation_id(self):
self.reservation_counter += 1
return self.reservation_counter
def is_valid_booking(self, booking_state):
required = ["restaurant_id", "user_name", "party_size", "date", "time"]
return all(booking_state.data.get(k) for k in required)
def add_reservation(self, booking_state):
if not self.is_valid_booking(booking_state):
missing = [
k for k in
["restaurant_id", "user_name", "party_size", "date", "time"]
if booking_state.data.get(k) is None
]
return {
"success": False,
"message": "Reservation could not be created. Missing fields.",
"missing_fields": missing
}
reservation_id = self._generate_reservation_id()
reservation = {
"reservation_id": reservation_id,
"restaurant_id": booking_state.data["restaurant_id"],
"user_name": booking_state.data["user_name"],
"party_size": booking_state.data["party_size"],
"date": booking_state.data["date"],
"time": booking_state.data["time"],
"special_requests": booking_state.data.get("special_requests", ""),
"status": "Confirmed"
}
# Add to DataFrame
new_row = pd.DataFrame([reservation])
self.reservations_df = pd.concat([self.reservations_df, new_row], ignore_index=True)
return {
"success": True,
"message": "Reservation confirmed!",
"reservation_details": reservation
}
def get_all_reservations(self):
return self.reservations_df.to_dict(orient="records")
class RestaurantQueryEngine:
def __init__(self, df):
self.df = df
def get_options(self, column_name):
if column_name in self.df.columns:
return sorted(self.df[column_name].dropna().unique().tolist())
return []
def filter_by(self, column_name, value):
result = self.df.copy()
if column_name in result.columns and value is not None:
result = result[result[column_name] == value]
return result[["id", "name", "locality", "cuisine", "price_range"]].to_dict(orient="records")
# Initialize OpenAI client
@st.cache_resource
def get_openai_client():
api_key = os.environ.get('OPENAI_API_KEY')
if not api_key:
st.error("❌ OPENAI_API_KEY environment variable is required")
st.info("Please set your OpenAI API key in the Hugging Face Spaces settings")
st.stop()
return OpenAI(api_key=api_key)
# Initialize session state
if "messages" not in st.session_state:
st.session_state.messages = []
if "session_id" not in st.session_state:
st.session_state.session_id = str(uuid.uuid4())
if "page" not in st.session_state:
st.session_state.page = "chat"
if "booking_state" not in st.session_state:
st.session_state.booking_state = BookingState()
if "reservation_manager" not in st.session_state:
restaurants_df = pd.DataFrame(restaurants_data)
reservations_df = pd.DataFrame(reservation_data)
st.session_state.reservation_manager = ReservationManager(restaurants_df, reservations_df)
if "query_engine" not in st.session_state:
restaurants_df = pd.DataFrame(restaurants_data)
st.session_state.query_engine = RestaurantQueryEngine(restaurants_df)
if "conversation_history" not in st.session_state:
st.session_state.conversation_history = []
# Tools definition
tools = [{
"type": "function",
"function": {
"name": "get_column_options",
"description": "Get unique available values for a column like cuisine, locality, or price_range.",
"parameters": {
"type": "object",
"properties": {
"column_name": {
"type": "string",
"description": "The column to get unique values from. Common options: 'cuisine', 'locality', 'price_range'."
}
},
"required": ["column_name"]
}
}
}, {
"type": "function",
"function": {
"name": "filter_restaurants",
"description": "Filter the list of restaurants based on a specific attribute like cuisine, location, or price range.",
"parameters": {
"type": "object",
"properties": {
"column_name": {
"type": "string",
"description": "The column to filter by. Common values: 'cuisine', 'locality', 'price_range'."
},
"value": {
"type": "string",
"description": "The value to match in the specified column."
}
},
"required": ["column_name", "value"]
}
}
}, {
"type": "function",
"function": {
"name": "update_booking_state",
"description": "Update the booking information with user's reservation details.",
"parameters": {
"type": "object",
"properties": {
"cuisine_preference": {"type": "string"},
"location": {"type": "string"},
"date": {"type": "string", "description": "Date of reservation in YYYY-MM-DD format."},
"time": {"type": "string", "description": "Time of reservation in HH:MM format."},
"party_size": {"type": "integer"},
"special_requests": {"type": "string"},
"restaurant_id": {"type": "string"},
"user_name": {"type": "string"}
},
"required": []
}
}
}, {
"type": "function",
"function": {
"name": "finalize_booking",
"description": "Check if all necessary booking information is filled. If complete, return all data.",
"parameters": {
"type": "object",
"properties": {}
}
}
}, {
"type": "function",
"function": {
"name": "make_reservation",
"description": "Create a confirmed reservation using current booking state and return reservation ID and details.",
"parameters": {
"type": "object",
"properties": {}
}
}
}]
SYSTEM_PROMPT = """
You are a friendly and efficient restaurant reservation assistant.
Your role is to help users find and reserve a restaurant based on their preferences like cuisine, location, date, time, and party size. If needed, collect this information in a polite and conversational way.
Recommendation and suggestion:
- ask user politely what they want the suggestions to be based on, location, cuisine, or price_range.
- when the user gives the value for a suggestion, then show him available restaurants for that value.
- **DO NOT SHOW MORE THAN 4 OPTIONS AT A TIME**
Information Collection:
- Reservation Details needed to complete a booking: [cuisine_preference, location, date, time, party_size, special_requests, restaurant_id, user_name]
- **ASK FOR ONE DETAIL ONLY AT A TIME**
Once all information is gathered, confirm the booking by calling the `make_reservation` tool. Be proactive in guiding the user. Do not hallucinate values. Rely on tools to fetch available options or complete bookings.
Always be warm and polite, like a concierge at a high-end restaurant. Use natural and welcoming phrases like:
- "Great! Let me note that down."
- "Could you please tell me…?"
- "Absolutely, I can help with that."
"""
def call_tool(tool_name, args):
"""Direct function calls instead of Flask endpoints"""
if tool_name == "get_column_options":
return st.session_state.query_engine.get_options(**args)
elif tool_name == "update_booking_state":
return st.session_state.booking_state.update(**args)
elif tool_name == "make_reservation":
result = st.session_state.reservation_manager.add_reservation(
st.session_state.booking_state
)
if result['success']:
st.session_state.booking_state.reset()
return result
elif tool_name == "filter_restaurants":
return st.session_state.query_engine.filter_by(**args)
elif tool_name == "finalize_booking":
return st.session_state.booking_state.check_state()
else:
return {"error": f"Unknown tool: {tool_name}"}
def process_chat_message(message):
"""Process chat message with OpenAI - replaces Flask /chat endpoint"""
client = get_openai_client()
st.session_state.conversation_history.append({
"role": "user",
"content": message
})
messages = [{
"role": "system",
"content": SYSTEM_PROMPT
}] + st.session_state.conversation_history
continue_processing = True
final_response = ""
while continue_processing:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
tool_choice="auto"
)
message_obj = response.choices[0].message
if message_obj.content:
final_response = message_obj.content
st.session_state.conversation_history.append({
"role": "assistant",
"content": message_obj.content
})
continue_processing = False
if message_obj.tool_calls:
st.session_state.conversation_history.append({
"role": "assistant",
"content": "",
"tool_calls": message_obj.tool_calls
})
for tool_call in message_obj.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
tool_output = call_tool(tool_name, tool_args)
st.session_state.conversation_history.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_name,
"content": json.dumps(tool_output)
})
messages = [{
"role": "system",
"content": SYSTEM_PROMPT
}] + st.session_state.conversation_history
continue_processing = True
return final_response
# Custom CSS
st.markdown("""
<style>
.backend-button {
position: fixed;
top: 20px;
right: 20px;
z-index: 999;
background: linear-gradient(45deg, #ff6b9d, #ff8a9b);
color: white;
padding: 10px 20px;
border: none;
border-radius: 25px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 4px 12px rgba(255, 107, 157, 0.3);
transition: all 0.3s ease;
}
.backend-button:hover {
background: linear-gradient(45deg, #ff5588, #ff7799);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(255, 107, 157, 0.4);
}
.restaurant-tile {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 15px;
padding: 15px;
margin: 10px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-left: 4px solid #ff6b9d;
transition: all 0.3s ease;
}
.restaurant-tile:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.restaurant-name {
font-weight: bold;
color: #333;
font-size: 16px;
margin-bottom: 8px;
}
.restaurant-detail {
color: #666;
font-size: 14px;
margin: 4px 0;
}
.restaurant-price {
color: #ff6b9d;
font-weight: bold;
font-size: 14px;
}
</style>
""", unsafe_allow_html=True)
# Top navigation
col1, col2 = st.columns([6, 1])
with col2:
if st.button("πŸ”§ Backend", key="backend_btn", help="View reservations dashboard"):
st.session_state.page = "backend"
st.rerun()
def show_chat_page():
st.title("πŸ’¬ foodieSpot")
st.markdown("Restaurant Reservations made easy!")
# System ready indicator
st.success("βœ… System ready")
# Display chat history
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
# Input and send button
user_input = st.chat_input("Type your message...")
if user_input:
# Handle exit command
if user_input.lower() in ['exit', 'quit', 'bye']:
bot_reply = 'Thanks for using foodieSpot! Have a great day! 🍽️'
# Save messages
st.session_state.messages.append({"role": "user", "content": user_input})
st.session_state.messages.append({"role": "assistant", "content": bot_reply})
# Display messages
with st.chat_message("user"):
st.markdown(user_input)
with st.chat_message("assistant"):
st.markdown(bot_reply)
st.stop()
# Save user message
st.session_state.messages.append({"role": "user", "content": user_input})
with st.chat_message("user"):
st.markdown(user_input)
# Show typing indicator
with st.chat_message("assistant"):
message_placeholder = st.empty()
message_placeholder.markdown("πŸ€” Thinking...")
try:
# Direct function call instead of HTTP request
bot_reply = process_chat_message(user_input)
except Exception as e:
bot_reply = f"❌ An error occurred: {str(e)}"
# Update the message placeholder with the actual response
message_placeholder.markdown(bot_reply)
# Save bot message
st.session_state.messages.append({"role": "assistant", "content": bot_reply})
# Sidebar with additional info
with st.sidebar:
st.header("ℹ️ App Info")
st.write("**Session ID:**", st.session_state.session_id[:8] + "...")
st.write("**Messages:**", len(st.session_state.messages))
if st.button("πŸ”„ New Session"):
st.session_state.messages = []
st.session_state.conversation_history = []
st.session_state.booking_state.reset()
st.session_state.session_id = str(uuid.uuid4())
st.rerun()
if st.button("🧹 Clear Chat"):
st.session_state.messages = []
st.session_state.conversation_history = []
st.rerun()
st.header("🍽️ Available Restaurants")
restaurants = restaurants_data
for restaurant in restaurants[:8]: # Show only first 8 restaurants
restaurant_tile = f"""
<div class="restaurant-tile">
<div class="restaurant-name">{restaurant['name']}</div>
<div class="restaurant-detail">🍜 {restaurant['cuisine']}</div>
<div class="restaurant-detail">πŸ“ {restaurant['locality']}</div>
<div class="restaurant-price">πŸ’° β‚Ή{restaurant['price_range']}</div>
</div>
"""
st.markdown(restaurant_tile, unsafe_allow_html=True)
if len(restaurants) > 8:
st.markdown(f"<div style='text-align: center; color: #666; font-style: italic; margin-top: 10px;'>...and {len(restaurants) - 8} more restaurants</div>", unsafe_allow_html=True)
st.header("πŸ’‘ Tips")
st.write("Try asking:")
st.write("- 'Show me Chinese restaurants'")
st.write("- 'I want to book a table'")
st.write("- 'What cuisines are available?'")
st.write("- 'Book for 4 people tomorrow at 7 PM'")
def show_backend_page():
st.title("πŸ”§ Backend Dashboard")
st.markdown("Real-time view of restaurant reservations")
if st.button("← Back to Chat"):
st.session_state.page = "chat"
st.rerun()
if "last_refresh" not in st.session_state:
st.session_state.last_refresh = time.time()
# Auto-refresh every 5 seconds
current_time = time.time()
if current_time - st.session_state.last_refresh > 5:
st.session_state.last_refresh = current_time
st.rerun()
st.markdown(f"πŸ”„ Auto-refreshing every 5 seconds | Last updated: {time.strftime('%H:%M:%S')}")
try:
# Get reservations data directly from session state
reservations_data_list = st.session_state.reservation_manager.get_all_reservations()
if reservations_data_list:
# Convert to DataFrame for better display
df = pd.DataFrame(reservations_data_list)
st.subheader(f"πŸ“Š Total Reservations: {len(df)}")
# Display metrics
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Total Reservations", len(df))
with col2:
if 'status' in df.columns:
confirmed = len(df[df['status'] == 'Confirmed'])
st.metric("Confirmed", confirmed)
with col3:
if 'party_size' in df.columns:
total_guests = df['party_size'].sum()
st.metric("Total Guests", total_guests)
# Display the table
st.subheader("πŸ“‹ Reservations Table")
st.dataframe(
df,
use_container_width=True,
hide_index=True
)
# Download button
csv = df.to_csv(index=False)
st.download_button(
label="πŸ“₯ Download CSV",
data=csv,
file_name=f"reservations_{time.strftime('%Y%m%d_%H%M%S')}.csv",
mime="text/csv"
)
else:
st.info("πŸ“­ No reservations found")
except Exception as e:
st.error(f"❌ Error fetching data: {str(e)}")
# Main app logic
if st.session_state.page == "chat":
show_chat_page()
elif st.session_state.page == "backend":
show_backend_page()