| import streamlit as st |
| import requests |
| import pandas as pd |
| from together import Together |
| import os |
|
|
| |
| |
| |
| NOCODB_URL = "https://app.nocodb.com" |
|
|
| |
| def get_api_credentials(): |
| """Get API credentials from secrets or environment""" |
| try: |
| |
| 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: |
| |
| 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 |
|
|
| |
| @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) |
|
|
| |
| |
| |
| 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) |
| 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", |
| 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 = safe_int(prop.get('cash_price')) |
| if price > filters['max_price']: |
| continue |
| |
| |
| rooms = safe_int(prop.get('rooms')) |
| if rooms < filters['min_rooms']: |
| continue |
| |
| |
| if filters['energy_ratings'] and prop.get('energy_rating') not in filters['energy_ratings']: |
| continue |
| |
| |
| 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. " |
| |
| |
| 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: |
| |
| 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""" |
| |
| models_to_test = [ |
| |
| "google/gemma-2b-it", |
| "google/gemma-2-27b-it", |
| |
| "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 |
|
|
| |
| |
| |
| def main(): |
| |
| st.set_page_config( |
| page_title="Danish Villa Assistant", |
| page_icon="π‘", |
| layout="wide" |
| ) |
| |
| |
| st.title("π‘ Danish Villa Assistant") |
| st.write("Explore Danish villas with AI-powered insights using Together AI!") |
| |
| |
| 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() |
| |
| |
| 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']}") |
| |
| |
| 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() |
| |
| |
| st.sidebar.header("π Filter Properties") |
| |
| |
| 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() |
| |
| |
| 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')]))) |
| |
| |
| 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=[] |
| ) |
| |
| |
| filters = { |
| 'max_price': max_price, |
| 'min_rooms': min_rooms, |
| 'cities': selected_cities, |
| 'energy_ratings': selected_energy_ratings |
| } |
| |
| |
| filtered_properties = filter_properties(all_properties, filters) |
| |
| |
| col1, col2 = st.columns([2, 1]) |
| |
| with col1: |
| |
| st.subheader(f"π Found {len(filtered_properties)} Properties") |
| |
| if filtered_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" |
| ): |
| |
| 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: |
| |
| st.subheader("π€ Ask AI Assistant") |
| st.write("Ask questions about the Danish villa market!") |
| |
| |
| model_choice = st.selectbox( |
| "Select AI Model:", |
| [ |
| |
| "google/gemma-2b-it", |
| "google/gemma-2-27b-it", |
| |
| "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." |
| ) |
| |
| |
| 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..."): |
| |
| context = create_property_context(filtered_properties) |
| |
| try: |
| |
| ai_response = get_ai_response(client, user_question, context, model_choice) |
| |
| st.success("**AI Assistant Response:**") |
| st.write(ai_response) |
| |
| |
| 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)}") |
| |
| |
| 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!") |
| |
| |
| 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() |