Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| """ | |
| Final Enhanced Gradio Web UI with Auto-Loading User Conversations | |
| """ | |
| import os | |
| import gradio as gr | |
| from dotenv import load_dotenv | |
| from graph import ArchitectureAssistant | |
| from user_state_manager import user_state_manager | |
| import json | |
| import hashlib | |
| from datetime import datetime | |
| class GradioArchitectureApp: | |
| def __init__(self): | |
| load_dotenv() | |
| # For Hugging Face Spaces deployment - get API key from environment | |
| api_key = os.environ.get("OPENAI_API_KEY") | |
| # Make API key optional for HF Spaces - users can provide their own | |
| self.api_key = api_key | |
| self.assistant = None | |
| self.current_user_id = None | |
| self.conversation_history = [] | |
| self.state_manager = user_state_manager | |
| def initialize_user_session(self, user_identifier: str = None, api_key: str = None): | |
| """Initialize user session with persistent state""" | |
| if not user_identifier: | |
| # Generate user ID from timestamp for demo | |
| user_identifier = hashlib.md5(str(datetime.now()).encode()).hexdigest()[:12] | |
| # Use provided API key or fallback to environment | |
| effective_api_key = api_key or self.api_key | |
| if not effective_api_key: | |
| raise ValueError("No API key provided") | |
| self.current_user_id = user_identifier | |
| self.assistant = ArchitectureAssistant(effective_api_key, user_identifier) | |
| return user_identifier | |
| def change_user_id(self, new_user_id: str, api_key: str = None): | |
| """Change user ID and auto-load their conversation history""" | |
| if not new_user_id.strip(): | |
| return [], "β Please enter a valid User ID", "", "Start chatting to see your requirements here!" | |
| try: | |
| # Use provided API key or fallback to environment | |
| effective_api_key = api_key or self.api_key | |
| if not effective_api_key: | |
| return [], "β Please provide your OpenAI API key", "", "API key required" | |
| # Set new user ID | |
| self.current_user_id = new_user_id.strip() | |
| # Create new assistant instance for this user | |
| self.assistant = ArchitectureAssistant(effective_api_key, self.current_user_id) | |
| # Try to load their latest conversation | |
| history = self.state_manager.get_user_history(self.current_user_id) | |
| if history: | |
| # Load most recent conversation | |
| latest_session = history[0]["session_id"] | |
| success = self.assistant.load_previous_state(latest_session) | |
| if success: | |
| # Convert state messages to chat history | |
| chat_history = [] | |
| messages = self.assistant.state.get("messages", []) | |
| current_pair = [None, None] | |
| for msg in messages: | |
| if msg["role"] == "user": | |
| current_pair = [msg["content"], None] | |
| elif msg["role"] == "assistant": | |
| if current_pair[0] is not None: | |
| current_pair[1] = msg["content"] | |
| chat_history.append(current_pair) | |
| current_pair = [None, None] | |
| status_msg = f"β Loaded {len(history)} previous conversation(s) for user {new_user_id}" | |
| summary = self.get_conversation_summary() | |
| return chat_history, status_msg, new_user_id, summary | |
| else: | |
| status_msg = f"β User {new_user_id} found but couldn't load conversation. Starting fresh." | |
| summary = self.get_conversation_summary() | |
| return [], status_msg, new_user_id, summary | |
| else: | |
| # New user | |
| status_msg = f"β New user {new_user_id} - starting fresh conversation" | |
| summary = self.get_conversation_summary() | |
| return [], status_msg, new_user_id, summary | |
| except Exception as e: | |
| return [], f"β Error loading user {new_user_id}: {str(e)}", "", "Error loading user" | |
| def chat_with_assistant(self, message, history, user_id_input="", api_key=""): | |
| """Process user message and return response with state saving""" | |
| if not message.strip(): | |
| return history, "", user_id_input, self.get_conversation_summary() | |
| # Check for API key first | |
| effective_api_key = api_key or self.api_key | |
| if not effective_api_key: | |
| error_msg = "β Please provide your OpenAI API key to use the Architecture Assistant." | |
| history.append([message, error_msg]) | |
| return history, "", user_id_input, "API key required" | |
| # Ensure we have an assistant instance | |
| if not self.assistant: | |
| if user_id_input.strip(): | |
| self.current_user_id = user_id_input.strip() | |
| self.assistant = ArchitectureAssistant(effective_api_key, self.current_user_id) | |
| else: | |
| user_id = self.initialize_user_session(api_key=effective_api_key) | |
| return history, "", user_id, self.get_conversation_summary() | |
| # Handle special commands | |
| if message.lower() == 'reset': | |
| self.assistant.reset_conversation() | |
| self.conversation_history = [] | |
| status_msg = "π Conversation reset! New session started." | |
| return [[status_msg, ""]], "", self.current_user_id or "", self.get_conversation_summary() | |
| # Get response from assistant | |
| try: | |
| response = self.assistant.chat(message) | |
| # Add response to history | |
| history.append([message, response]) | |
| # Add to conversation history | |
| self.conversation_history.append({"user": message, "assistant": response}) | |
| # Get updated summary | |
| summary = self.get_conversation_summary() | |
| return history, "", self.current_user_id or "", summary | |
| except Exception as e: | |
| error_msg = f"β Error: {str(e)}" | |
| history.append([message, error_msg]) | |
| return history, "", self.current_user_id or "", self.get_conversation_summary() | |
| def get_conversation_summary(self): | |
| """Get formatted conversation summary""" | |
| if not self.assistant: | |
| return "Enter a User ID above to start or resume a conversation!" | |
| summary = self.assistant.get_conversation_summary() | |
| summary_text = f"π **CONVERSATION SUMMARY**\n" | |
| summary_text += f"π€ **User ID:** {self.current_user_id or 'Not set'}\n\n" | |
| # User requirements | |
| reqs = summary["user_requirements"] | |
| summary_text += "**USER REQUIREMENTS:**\n" | |
| if reqs["budget"]: | |
| summary_text += f"β’ Budget: ${reqs['budget']:,.0f}\n" | |
| if reqs["location"]: | |
| summary_text += f"β’ Location: {reqs['location']}\n" | |
| if reqs["family_size"]: | |
| summary_text += f"β’ Family size: {reqs['family_size']}\n" | |
| if reqs["lifestyle_preferences"]: | |
| summary_text += f"β’ Preferences: {', '.join(reqs['lifestyle_preferences'])}\n" | |
| # Floorplan requirements | |
| floor_reqs = summary["floorplan_requirements"] | |
| summary_text += "\n**FLOORPLAN REQUIREMENTS:**\n" | |
| if floor_reqs["num_floors"]: | |
| summary_text += f"β’ Floors: {floor_reqs['num_floors']}\n" | |
| if floor_reqs["total_sqft"]: | |
| summary_text += f"β’ Total sq ft: {floor_reqs['total_sqft']}\n" | |
| if floor_reqs["lot_shape"]: | |
| summary_text += f"β’ Lot shape: {floor_reqs['lot_shape']}\n" | |
| if floor_reqs["lot_dimensions"]: | |
| summary_text += f"β’ Lot dimensions: {floor_reqs['lot_dimensions']}\n" | |
| if floor_reqs["rooms"]: | |
| rooms_str = ", ".join([f"{r['count']}x {r['type']}" for r in floor_reqs["rooms"]]) | |
| summary_text += f"β’ Rooms: {rooms_str}\n" | |
| # Project progress | |
| agent_memory = self.assistant.state.get("agent_memory", {}) | |
| completed_phases = [] | |
| if self.assistant.state.get("detailed_floorplan", {}).get("detailed_rooms"): | |
| completed_phases.append("Architectural Design") | |
| if self.assistant.state.get("budget_breakdown", {}).get("total_construction_cost"): | |
| completed_phases.append("Budget Analysis") | |
| if agent_memory.get("structural_analysis"): | |
| completed_phases.append("Structural Analysis") | |
| if agent_memory.get("sustainability"): | |
| completed_phases.append("Sustainability Review") | |
| if agent_memory.get("permits"): | |
| completed_phases.append("Permit Planning") | |
| if agent_memory.get("interior_design"): | |
| completed_phases.append("Interior Design") | |
| summary_text += f"\n**PROJECT PROGRESS:**\n" | |
| summary_text += f"β’ Completed: {len(completed_phases)}/6 phases\n" | |
| if completed_phases: | |
| summary_text += f"β’ Phases: {', '.join(completed_phases)}\n" | |
| summary_text += f"β’ Total messages: {summary['total_messages']}\n" | |
| return summary_text | |
| def get_user_history_display(self, user_id: str): | |
| """Get formatted user history for display""" | |
| if not user_id.strip(): | |
| return "Please enter a User ID to view history." | |
| try: | |
| history = self.state_manager.get_user_history(user_id.strip()) | |
| if not history: | |
| return f"No conversation history found for user: {user_id}" | |
| display_text = f"π **CONVERSATION HISTORY FOR USER: {user_id}**\n\n" | |
| for i, conv in enumerate(history, 1): | |
| timestamp = conv["timestamp"] | |
| session_id = conv["session_id"] | |
| summary = conv.get("summary", {}) | |
| display_text += f"**#{i} - {timestamp}**\n" | |
| display_text += f"Session ID: `{session_id}`\n" | |
| if summary.get("total_messages", 0) > 0: | |
| display_text += f"Messages: {summary['total_messages']}\n" | |
| if summary.get("user_requirements"): | |
| reqs = summary["user_requirements"] | |
| if reqs.get("budget"): | |
| display_text += f"Budget: {reqs['budget']}\n" | |
| if reqs.get("location"): | |
| display_text += f"Location: {reqs['location']}\n" | |
| if reqs.get("family_size"): | |
| display_text += f"Family: {reqs['family_size']} people\n" | |
| if summary.get("floorplan_status"): | |
| floor_status = summary["floorplan_status"] | |
| if floor_status.get("size"): | |
| display_text += f"House Size: {floor_status['size']}\n" | |
| if floor_status.get("rooms"): | |
| display_text += f"Rooms: {floor_status['rooms']}\n" | |
| if summary.get("project_progress"): | |
| progress = summary["project_progress"] | |
| completed = progress.get("completed_phases", []) | |
| percentage = progress.get("completion_percentage", 0) | |
| display_text += f"Progress: {percentage}% - {', '.join(completed)}\n" | |
| display_text += "\n---\n\n" | |
| return display_text | |
| except Exception as e: | |
| return f"β Error retrieving history: {str(e)}" | |
| def get_all_users_summary(self): | |
| """Get summary of all users""" | |
| try: | |
| users = self.state_manager.get_all_users() | |
| if not users: | |
| return "No users found in the system." | |
| display_text = f"π₯ **ALL USERS SUMMARY** ({len(users)} users)\n\n" | |
| for i, user in enumerate(users, 1): | |
| user_id = user["user_id"] | |
| total_conversations = user["total_conversations"] | |
| last_activity = user["last_activity"] | |
| latest_summary = user.get("latest_summary", {}) | |
| display_text += f"**#{i} User: {user_id}**\n" | |
| display_text += f"Conversations: {total_conversations}\n" | |
| display_text += f"Last Activity: {last_activity}\n" | |
| if latest_summary.get("project_progress"): | |
| progress = latest_summary["project_progress"] | |
| percentage = progress.get("completion_percentage", 0) | |
| display_text += f"Latest Progress: {percentage}%\n" | |
| display_text += "\n---\n\n" | |
| return display_text | |
| except Exception as e: | |
| return f"β Error retrieving users summary: {str(e)}" | |
| def create_gradio_interface(): | |
| """Create and return the final enhanced Gradio interface""" | |
| app = GradioArchitectureApp() | |
| # Custom CSS for better styling | |
| css = """ | |
| .gradio-container { | |
| max-width: 1400px !important; | |
| } | |
| .chat-message { | |
| border-radius: 10px !important; | |
| } | |
| .user-id-input { | |
| background-color: #e3f2fd !important; | |
| font-weight: bold !important; | |
| } | |
| """ | |
| with gr.Blocks( | |
| title="π Architecture Assistant - Multi-User", | |
| theme=gr.themes.Soft(), | |
| css=css | |
| ) as interface: | |
| gr.HTML(""" | |
| <div style="text-align: center; margin-bottom: 20px;"> | |
| <h1>π Residential Architecture Assistant</h1> | |
| <p>Multi-user system with automatic conversation loading and precise floorplan specifications</p> | |
| </div> | |
| """) | |
| # API Key input for Hugging Face Spaces | |
| api_key_input = gr.Textbox( | |
| label="π OpenAI API Key", | |
| placeholder="Enter your OpenAI API key (sk-...)", | |
| type="password", | |
| info="Required for AI functionality. Not stored or logged." | |
| ) | |
| # User ID input | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| user_id_input = gr.Textbox( | |
| placeholder="Enter your User ID (e.g., 'john_smith_house') - automatically loads your conversation history", | |
| label="π€ User ID", | |
| elem_classes=["user-id-input"], | |
| interactive=True | |
| ) | |
| with gr.Column(scale=1): | |
| load_user_btn = gr.Button("Load User", variant="primary") | |
| status_display = gr.Markdown( | |
| label="π Status", | |
| value="Enter a User ID above to start or resume your architectural consultation." | |
| ) | |
| # Main interface | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| chatbot = gr.Chatbot( | |
| label="π¬ Chat with your Architecture Assistant", | |
| height=500, | |
| show_copy_button=True, | |
| bubble_full_width=False | |
| ) | |
| with gr.Row(): | |
| msg = gr.Textbox( | |
| placeholder="Ask about home design, budget, or floorplan planning...", | |
| container=False, | |
| scale=4, | |
| label="Your message" | |
| ) | |
| send_btn = gr.Button("Send", variant="primary", scale=1) | |
| with gr.Row(): | |
| clear_btn = gr.Button("π Reset Conversation", variant="secondary") | |
| gr.HTML(""" | |
| <div style="margin-top: 15px; padding: 10px; background-color: #f0f0f0; border-radius: 5px;"> | |
| <h4>π‘ How to use:</h4> | |
| <ul> | |
| <li><strong>New User:</strong> Enter any User ID above and start chatting</li> | |
| <li><strong>Returning User:</strong> Enter your previous User ID - conversation automatically loads</li> | |
| <li><strong>Multiple Users:</strong> Each User ID has completely separate conversations</li> | |
| <li><strong>Floorplans:</strong> Get precise room dimensions for drawing actual plans</li> | |
| </ul> | |
| </div> | |
| """) | |
| with gr.Column(scale=1): | |
| summary_display = gr.Markdown( | |
| label="π Current Project Summary", | |
| value="Enter a User ID above to start or resume a conversation!" | |
| ) | |
| gr.HTML(""" | |
| <div style="margin-top: 20px; padding: 10px; background-color: #e8f4f8; border-radius: 5px;"> | |
| <h4>π€ Available Core Specialists:</h4> | |
| <p><strong>π§ Router:</strong> Intelligent conversation routing</p> | |
| <p><strong>ποΈ General Design:</strong> Architecture principles & design guidance</p> | |
| <p><strong>π° Budget Analysis:</strong> Montreal market costs & feasibility</p> | |
| <p><strong>π Floorplan:</strong> Layout planning with exact dimensions</p> | |
| <p><strong>π Regulation:</strong> Montreal building codes & permit requirements</p> | |
| <br> | |
| </div> | |
| """) | |
| # Example prompts | |
| with gr.Row(): | |
| gr.HTML("<h3>π Try these examples:</h3>") | |
| with gr.Row(): | |
| example1 = gr.Button("What are key home design principles?", size="sm") | |
| example2 = gr.Button("I have $800k budget for Montreal - realistic?", size="sm") | |
| example3 = gr.Button("Need 3BR/2BA house, 2500 sqft - help floorplan design", size="sm") | |
| with gr.Row(): | |
| example4 = gr.Button("Show me a detailed budget breakdown", size="sm") | |
| example5 = gr.Button("What permits do I need for my house design in Montreal?", size="sm") | |
| example6 = gr.Button("Help me with Montreal building codes", size="sm") | |
| # History tab | |
| with gr.Accordion("π User History & Management", open=False): | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.HTML("<h4>π View User History</h4>") | |
| with gr.Row(): | |
| history_user_id = gr.Textbox( | |
| placeholder="Enter User ID to view history", | |
| label="User ID for History", | |
| scale=3 | |
| ) | |
| view_history_btn = gr.Button("View History", variant="primary", scale=1) | |
| user_history_display = gr.Markdown( | |
| label="User History", | |
| value="Enter a User ID to view their conversation history." | |
| ) | |
| with gr.Column(): | |
| gr.HTML("<h4>π₯ All Users Overview</h4>") | |
| view_all_btn = gr.Button("View All Users", variant="secondary") | |
| all_users_display = gr.Markdown( | |
| label="All Users Summary", | |
| value="Click 'View All Users' to see system overview." | |
| ) | |
| # Event handlers | |
| def send_message(message, history, user_id, api_key): | |
| return app.chat_with_assistant(message, history, user_id, api_key) | |
| def change_user(user_id, api_key): | |
| return app.change_user_id(user_id, api_key) | |
| def view_user_history(user_id): | |
| return app.get_user_history_display(user_id) | |
| def view_all_users(): | |
| return app.get_all_users_summary() | |
| # Wire up the events | |
| load_user_btn.click(change_user, [user_id_input, api_key_input], [chatbot, status_display, user_id_input, summary_display]) | |
| msg.submit(send_message, [msg, chatbot, user_id_input, api_key_input], [chatbot, msg, user_id_input, summary_display]) | |
| send_btn.click(send_message, [msg, chatbot, user_id_input, api_key_input], [chatbot, msg, user_id_input, summary_display]) | |
| clear_btn.click(lambda: app.assistant.reset_conversation() if app.assistant else None, outputs=[chatbot, summary_display]) | |
| # History management events | |
| view_history_btn.click(view_user_history, [history_user_id], [user_history_display]) | |
| view_all_btn.click(view_all_users, outputs=[all_users_display]) | |
| # Example button handlers | |
| example1.click(lambda: "What are key home design principles?", outputs=msg) | |
| example2.click(lambda: "I have $800k budget for Montreal - is that realistic?", outputs=msg) | |
| example3.click(lambda: "I need a 3 bedroom, 2 bathroom house with about 2500 square feet. Can you help me plan the layout?", outputs=msg) | |
| example4.click(lambda: "Can you show me a detailed budget breakdown for my house design?", outputs=msg) | |
| example5.click(lambda: "What permits do I need for my house design in Montreal?", outputs=msg) | |
| example6.click(lambda: "Help me understand Montreal building codes and regulations", outputs=msg) | |
| return interface | |
| def main(): | |
| """Launch the enhanced Gradio interface for Hugging Face Spaces""" | |
| try: | |
| interface = create_gradio_interface() | |
| print("π Starting Architecture Assistant for Hugging Face Spaces...") | |
| print("π₯ Features: Multi-user support, auto-loading conversations, precise floorplans") | |
| print("π Users provide their own OpenAI API key via the interface") | |
| print("π Ready for Hugging Face Spaces deployment!") | |
| # Simple launch for HF Spaces | |
| interface.launch() | |
| except Exception as e: | |
| print(f"β Error starting the application: {e}") | |
| if __name__ == "__main__": | |
| main() |