test2 / src /app.py
Sylviasy's picture
Update src/app.py
8d5ade7 verified
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",
"google/gemma-2-27b-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",
"google/gemma-2-27b-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()