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(""" """, 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"""