Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import requests | |
| import pandas as pd | |
| from together import Together | |
| import os | |
| # ============================================================================= | |
| # CONFIGURATION - Using Secrets Management | |
| # ============================================================================= | |
| NOCODB_URL = "https://app.nocodb.com" # Base URL | |
| # Get sensitive data from Streamlit secrets or environment variables | |
| def get_api_credentials(): | |
| """Get API credentials from secrets or environment""" | |
| try: | |
| # Try Streamlit secrets first (for Hugging Face Spaces) | |
| api_token = st.secrets.get("NOCODB_API_TOKEN", os.environ.get("NOCODB_API_TOKEN", "")) | |
| together_key = st.secrets.get("TOGETHER_API_KEY", os.environ.get("TOGETHER_API_KEY", "")) | |
| endpoint_path = st.secrets.get("NOCODB_ENDPOINT_PATH", os.environ.get("NOCODB_ENDPOINT_PATH", "")) | |
| return api_token, together_key, endpoint_path | |
| except: | |
| # Fallback to environment variables | |
| api_token = os.environ.get("NOCODB_API_TOKEN", "") | |
| together_key = os.environ.get("TOGETHER_API_KEY", "") | |
| endpoint_path = os.environ.get("NOCODB_ENDPOINT_PATH", "") | |
| return api_token, together_key, endpoint_path | |
| # Initialize Together AI client | |
| def get_ai_client(): | |
| """Initialize Together AI client""" | |
| _, together_key, _ = get_api_credentials() | |
| if not together_key: | |
| st.error("Together AI API key not found. Please configure it in the secrets.") | |
| return None | |
| return Together(api_key=together_key) | |
| # ============================================================================= | |
| # HELPER FUNCTIONS | |
| # ============================================================================= | |
| def safe_int(value, default=0): | |
| """Safely convert value to integer""" | |
| try: | |
| return int(float(value)) if value else default | |
| except (ValueError, TypeError): | |
| return default | |
| def safe_float(value, default=0.0): | |
| """Safely convert value to float""" | |
| try: | |
| return float(value) if value else default | |
| except (ValueError, TypeError): | |
| return default | |
| # Cache for 5 minutes | |
| def get_properties(): | |
| """Fetch properties from NocoDB""" | |
| api_token, _, endpoint_path = get_api_credentials() | |
| if not api_token or not endpoint_path: | |
| st.error("NocoDB credentials not configured. Please set up your secrets.") | |
| return [] | |
| headers = {"xc-token": api_token} | |
| try: | |
| response = requests.get( | |
| f"{NOCODB_URL}{endpoint_path}?limit=1000", # Get more records | |
| headers=headers | |
| ) | |
| if response.status_code == 200: | |
| data = response.json() | |
| return data.get('list', []) | |
| else: | |
| st.error(f"Failed to fetch data: {response.status_code}") | |
| return [] | |
| except Exception as e: | |
| st.error(f"Error connecting to database: {e}") | |
| return [] | |
| def filter_properties(properties, filters): | |
| """Apply filters to properties list""" | |
| filtered = [] | |
| for prop in properties: | |
| # Price filter | |
| price = safe_int(prop.get('cash_price')) | |
| if price > filters['max_price']: | |
| continue | |
| # Rooms filter | |
| rooms = safe_int(prop.get('rooms')) | |
| if rooms < filters['min_rooms']: | |
| continue | |
| # Energy rating filter | |
| if filters['energy_ratings'] and prop.get('energy_rating') not in filters['energy_ratings']: | |
| continue | |
| # City filter | |
| if filters['cities'] and prop.get('city') not in filters['cities']: | |
| continue | |
| filtered.append(prop) | |
| return filtered | |
| def create_property_context(properties): | |
| """Create context string about current properties for AI""" | |
| if not properties: | |
| return "No properties match the current filters." | |
| total = len(properties) | |
| prices = [safe_int(p.get('cash_price')) for p in properties if safe_int(p.get('cash_price')) > 0] | |
| if prices: | |
| avg_price = sum(prices) / len(prices) | |
| min_price = min(prices) | |
| max_price = max(prices) | |
| context = f"""Currently showing {total} Danish villas. | |
| Price range: {min_price:,} - {max_price:,} DKK. | |
| Average price: {avg_price:,.0f} DKK. """ | |
| else: | |
| context = f"Currently showing {total} Danish villas. " | |
| # Add some location info | |
| cities = list(set([p.get('city', 'Unknown') for p in properties[:10]])) | |
| if cities: | |
| context += f"Cities include: {', '.join(cities[:5])}. " | |
| return context | |
| def get_ai_response(client, question, context, model_name): | |
| """Get response from Together AI""" | |
| try: | |
| # Create a comprehensive prompt | |
| prompt = f"""You are a helpful Danish real estate assistant. Based on the current property data, please answer the user's question accurately and helpfully. | |
| Current Property Data Context: | |
| {context} | |
| User Question: {question} | |
| Please provide a helpful, accurate response based on the data provided. Keep your answer concise but informative.""" | |
| response = client.chat.completions.create( | |
| model=model_name, | |
| messages=[ | |
| {"role": "system", "content": "You are a helpful Danish real estate assistant with expertise in property analysis and market insights."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| max_tokens=300, | |
| temperature=0.7, | |
| ) | |
| return response.choices[0].message.content | |
| except Exception as e: | |
| raise Exception(f"Together AI Error: {str(e)}") | |
| def test_together_models(): | |
| """Test different Together AI models""" | |
| # Include both Gemma and other reliable serverless models | |
| models_to_test = [ | |
| # Gemma models (Google's lightweight models) | |
| "google/gemma-2b-it", | |
| # Other reliable models | |
| "mistralai/Mistral-7B-Instruct-v0.1", | |
| "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO", | |
| "mistralai/Mixtral-8x7B-Instruct-v0.1" | |
| ] | |
| results = {} | |
| client = get_ai_client() | |
| if not client: | |
| return {"error": "Could not initialize AI client"} | |
| for model_name in models_to_test: | |
| try: | |
| test_response = client.chat.completions.create( | |
| model=model_name, | |
| messages=[ | |
| {"role": "system", "content": "You are a helpful assistant."}, | |
| {"role": "user", "content": "Hello, can you help me analyze real estate data?"} | |
| ], | |
| max_tokens=50, | |
| temperature=0.7, | |
| ) | |
| results[model_name] = { | |
| "status": "✅ Success", | |
| "response": test_response.choices[0].message.content[:100] | |
| } | |
| except Exception as e: | |
| results[model_name] = {"status": "❌ Error", "response": str(e)[:100]} | |
| return results | |
| # ============================================================================= | |
| # MAIN APP | |
| # ============================================================================= | |
| def main(): | |
| # Page config | |
| st.set_page_config( | |
| page_title="Danish Villa Assistant", | |
| page_icon="🏡", | |
| layout="wide" | |
| ) | |
| # Header | |
| st.title("🏡 Danish Villa Assistant") | |
| st.write("Explore Danish villas with AI-powered insights using Together AI!") | |
| # Check API credentials | |
| api_token, together_key, endpoint_path = get_api_credentials() | |
| if not together_key: | |
| st.error("⚠️ Together AI API key not configured!") | |
| st.info("Please set your TOGETHER_API_KEY in the Hugging Face Spaces secrets.") | |
| st.stop() | |
| if not api_token or not endpoint_path: | |
| st.error("⚠️ NocoDB credentials not configured!") | |
| st.info("Please set NOCODB_API_TOKEN and NOCODB_ENDPOINT_PATH in the Hugging Face Spaces secrets.") | |
| st.stop() | |
| # Add model testing section | |
| with st.expander("🧪 Test Together AI Models (for debugging)"): | |
| if st.button("Test Different Models"): | |
| with st.spinner("Testing models..."): | |
| test_results = test_together_models() | |
| for model, result in test_results.items(): | |
| st.write(f"**{model}:** {result['status']}") | |
| if result['status'] == "✅ Success": | |
| st.success(f"Response preview: {result['response']}") | |
| else: | |
| st.error(f"Error: {result['response']}") | |
| # Initialize AI client | |
| try: | |
| client = get_ai_client() | |
| if not client: | |
| st.stop() | |
| except Exception as e: | |
| st.error(f"Failed to initialize Together AI client: {e}") | |
| st.stop() | |
| # Sidebar filters | |
| st.sidebar.header("🔍 Filter Properties") | |
| # Get all properties first to populate filter options | |
| with st.spinner("Loading properties..."): | |
| all_properties = get_properties() | |
| if not all_properties: | |
| st.error("Could not load properties. Please check your NocoDB connection.") | |
| st.stop() | |
| # Extract unique values for filters | |
| all_cities = sorted(list(set([p.get('city', 'Unknown') for p in all_properties if p.get('city')]))) | |
| all_energy_ratings = sorted(list(set([p.get('energy_rating') for p in all_properties if p.get('energy_rating')]))) | |
| # Sidebar filter controls | |
| max_price = st.sidebar.slider( | |
| "Maximum Price (DKK)", | |
| min_value=0, | |
| max_value=20000000, | |
| value=10000000, | |
| step=500000, | |
| format="%d" | |
| ) | |
| min_rooms = st.sidebar.slider( | |
| "Minimum Rooms", | |
| min_value=1, | |
| max_value=15, | |
| value=3 | |
| ) | |
| selected_cities = st.sidebar.multiselect( | |
| "Cities", | |
| options=all_cities, | |
| default=[] | |
| ) | |
| selected_energy_ratings = st.sidebar.multiselect( | |
| "Energy Ratings", | |
| options=all_energy_ratings, | |
| default=[] | |
| ) | |
| # Create filter dictionary | |
| filters = { | |
| 'max_price': max_price, | |
| 'min_rooms': min_rooms, | |
| 'cities': selected_cities, | |
| 'energy_ratings': selected_energy_ratings | |
| } | |
| # Apply filters | |
| filtered_properties = filter_properties(all_properties, filters) | |
| # Main content area | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| # Property listings | |
| st.subheader(f"📋 Found {len(filtered_properties)} Properties") | |
| if filtered_properties: | |
| # Show first 10 properties | |
| for i, prop in enumerate(filtered_properties[:10]): | |
| with st.expander( | |
| f"{prop.get('address', 'N/A')} - {safe_int(prop.get('cash_price')):,} DKK" | |
| ): | |
| # Property details in columns | |
| detail_col1, detail_col2, detail_col3 = st.columns(3) | |
| with detail_col1: | |
| st.write(f"**🏙️ City:** {prop.get('city', 'N/A')}") | |
| st.write(f"**🚪 Rooms:** {prop.get('rooms', 'N/A')}") | |
| st.write(f"**📐 Living Area:** {prop.get('living_area', 'N/A')} m²") | |
| with detail_col2: | |
| st.write(f"**⚡ Energy Rating:** {prop.get('energy_rating', 'N/A')}") | |
| st.write(f"**📅 Year Built:** {prop.get('year_built', 'N/A')}") | |
| st.write(f"**🏛️ Municipality:** {prop.get('municipal', 'N/A')}") | |
| with detail_col3: | |
| price_per_sqm = safe_int(prop.get('square_meter_price')) | |
| st.write(f"**💰 Price/m²:** {price_per_sqm:,} DKK" if price_per_sqm else "**💰 Price/m²:** N/A") | |
| plot_area = safe_int(prop.get('area')) | |
| st.write(f"**🌿 Plot Area:** {plot_area:,} m²" if plot_area else "**🌿 Plot Area:** N/A") | |
| st.write(f"**🏠 Type:** {prop.get('legal_type', 'N/A')}") | |
| if len(filtered_properties) > 10: | |
| st.info(f"Showing first 10 of {len(filtered_properties)} properties. Adjust filters to narrow results.") | |
| else: | |
| st.info("No properties match your current filters. Try adjusting the criteria.") | |
| with col2: | |
| # AI Chat Section | |
| st.subheader("🤖 Ask AI Assistant") | |
| st.write("Ask questions about the Danish villa market!") | |
| # Model selection for Together AI | |
| model_choice = st.selectbox( | |
| "Select AI Model:", | |
| [ | |
| # Gemma models (Google's efficient models) | |
| "google/gemma-2b-it", | |
| # Other reliable models | |
| "mistralai/Mistral-7B-Instruct-v0.1", | |
| "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO", | |
| "mistralai/Mixtral-8x7B-Instruct-v0.1" | |
| ], | |
| help="Gemma models are Google's efficient, lightweight models." | |
| ) | |
| # Example questions | |
| with st.expander("💡 Example Questions"): | |
| st.write("• What's the average price range?") | |
| st.write("• Tell me about energy ratings in the data") | |
| st.write("• Which areas have the most expensive properties?") | |
| st.write("• How many properties are available in each city?") | |
| st.write("• What's the price per square meter trend?") | |
| user_question = st.text_area( | |
| "Your Question:", | |
| placeholder="Ask about prices, locations, energy ratings, market trends...", | |
| height=100 | |
| ) | |
| if st.button("🔍 Ask AI", type="primary"): | |
| if user_question: | |
| with st.spinner("AI is analyzing the data..."): | |
| # Create context from current filtered data | |
| context = create_property_context(filtered_properties) | |
| try: | |
| # Get AI response | |
| ai_response = get_ai_response(client, user_question, context, model_choice) | |
| st.success("**AI Assistant Response:**") | |
| st.write(ai_response) | |
| # Show debug info | |
| with st.expander("Debug Info"): | |
| st.write(f"Model used: {model_choice}") | |
| st.write(f"Properties analyzed: {len(filtered_properties)}") | |
| st.write(f"Context: {context[:150]}...") | |
| except Exception as e: | |
| st.error(f"AI Error: {str(e)}") | |
| # Fallback response with data analysis | |
| st.info("**Fallback Analysis:**") | |
| if filtered_properties: | |
| avg_price = sum(safe_int(p.get('cash_price')) for p in filtered_properties) / len(filtered_properties) | |
| st.write(f"• Found {len(filtered_properties)} properties") | |
| st.write(f"• Average price: {avg_price:,.0f} DKK") | |
| cities = list(set(p.get('city') for p in filtered_properties if p.get('city'))) | |
| if cities: | |
| st.write(f"• Cities: {', '.join(cities[:3])}") | |
| energy_ratings = list(set(p.get('energy_rating') for p in filtered_properties if p.get('energy_rating'))) | |
| if energy_ratings: | |
| st.write(f"• Energy ratings: {', '.join(energy_ratings[:3])}") | |
| else: | |
| st.warning("Please enter a question first!") | |
| # Footer stats | |
| st.markdown("---") | |
| if all_properties: | |
| total_props = len(all_properties) | |
| filtered_props = len(filtered_properties) | |
| stat_col1, stat_col2, stat_col3, stat_col4 = st.columns(4) | |
| with stat_col1: | |
| st.metric("Total Properties", total_props) | |
| with stat_col2: | |
| st.metric("Filtered Results", filtered_props) | |
| with stat_col3: | |
| if filtered_properties: | |
| avg_price = sum(safe_int(p.get('cash_price')) for p in filtered_properties) / len(filtered_properties) | |
| st.metric("Avg Price", f"{avg_price:,.0f} DKK") | |
| with stat_col4: | |
| unique_cities = len(set(p.get('city') for p in filtered_properties if p.get('city'))) | |
| st.metric("Cities", unique_cities) | |
| if __name__ == "__main__": | |
| main() |