Spaces:
No application file
No application file
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 | |
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() |