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() |