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 @st.cache_resource 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 @st.cache_data(ttl=300) # 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()