Merge pull request #302 from Madhuvod/ai_real_estate_team_

feat: updated AI real estate agent team
This commit is contained in:
Shubham Saboo 2025-08-05 17:40:02 -05:00 committed by GitHub
commit 2463d17dfd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1807 additions and 901 deletions

View file

@ -1,20 +1,19 @@
# 🏠 AI Real Estate Agent Team
The **AI Real Estate Agent Team** is a sophisticated property search and analysis platform powered by specialized AI agents with firecrawl's extract endpoint. This application provides comprehensive real estate insights, market analysis, and property recommendations using advanced web scraping and AI-powered search capabilities.
The **AI Real Estate Agent Team** is a sophisticated property search and analysis platform powered by specialized AI agents with Firecrawl's extract endpoint. This application provides comprehensive real estate insights, market analysis, and property recommendations using advanced web scraping and AI-powered search capabilities.
## Features
- **Multi-Agent Analysis System**
- **Property Search Agent**: Finds properties using Firecrawl extract + Perplexity fallback
- **Market Analysis Agent**: Provides elaborate market trends and neighborhood insights
- **Property Valuation Agent**: Gives comprehensive property valuations and investment analysis
- **Property Search Agent**: Finds properties using direct Firecrawl integration
- **Market Analysis Agent**: Provides concise market trends and neighborhood insights
- **Property Valuation Agent**: Gives brief property valuations and investment analysis
- **Multi-Platform Property Search**:
- **Zillow**: Largest real estate marketplace with comprehensive listings
- **Realtor.com**: Official site of the National Association of Realtors
- **Trulia**: Neighborhood-focused real estate search
- **Homes.com**: Comprehensive property search platform
- **Perplexity AI**: AI-powered search across multiple sources as fallback
- **Advanced Property Analysis**:
- Detailed property information extraction (address, price, bedrooms, bathrooms, sqft)
@ -24,16 +23,16 @@ The **AI Real Estate Agent Team** is a sophisticated property search and analysi
- **Comprehensive Market Insights**:
- Current market conditions (buyer's/seller's market)
- Price trends over 6-12 months
- Neighborhood analysis with school districts and safety ratings
- Investment potential assessment with ROI projections
- Comparative market analysis
- Price trends and market direction
- Neighborhood analysis with key insights
- Investment potential assessment
- Strategic recommendations
- **Smart Fallback System**:
- Primary: Firecrawl extract endpoint for structured data
- Fallback: Google Search when extract returns no results
- Seamless transition between data sources
- Google Search indicator when using web search
- **Sequential Manual Execution**:
- Optimized for speed and reliability
- Direct data flow between agents
- Manual coordination for better control
- Reduced overhead and improved performance
- **Interactive UI Features**:
- Real-time agent progression tracking
@ -41,156 +40,172 @@ The **AI Real Estate Agent Team** is a sophisticated property search and analysi
- Downloadable analysis reports
- Timing information for performance monitoring
## Requirements
The application requires the following Python libraries:
- `agno`
- `streamlit`
- `firecrawl-py`
- `python-dotenv`
- `pydantic`
You'll also need API keys for:
- **Cloud Version**: Google AI (Gemini) + Firecrawl
- **Local Version**: Firecrawl only (uses Ollama locally)
## How to Run
Follow the steps below to set up and run the application:
Follow these steps to set up and run the application:
### 1. **Get API Keys**:
- **OpenAI API Key**: Get from [OpenAI Platform](https://platform.openai.com/api-keys)
- **Firecrawl API Key**: Get from [Firecrawl](https://firecrawl.dev)
- **Google Search**: No API key required - uses Agno's GoogleSearchTools
### **API Version (Gemini 2.5 Flash)**
### 2. **Clone the Repository**:
1. **Clone the Repository**:
```bash
git clone https://github.com/your-username/awesome-llm-apps.git
cd awesome-llm-apps/advanced_ai_agents/multi_agent_apps/ai_real_estate_agent_team
git clone https://github.com/Shubhamsaboo/awesome-llm-apps.git
cd advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team
```
### 3. **Set Up Environment Variables**:
Create a `.env` file in the project root and add your API keys:
```
OPENAI_API_KEY=your_openai_api_key_here
FIRECRAWL_API_KEY=your_firecrawl_api_key_here
```
**Google Search is included automatically** - no API key required for fallback search functionality.
2. **Install the dependencies**:
```bash
pip install -r requirements.txt
```
### 4. **Install Dependencies**:
3. **Set up your API keys**:
- Get a Google AI API key from: https://aistudio.google.com/app/apikey
- Get a Firecrawl API key from: [Firecrawl website](https://firecrawl.dev)
4. **Run the Streamlit app**:
```bash
streamlit run real_estate_agent_team.py
```
### **Local Version (Ollama)**
1. **Install Ollama**:
```bash
pip install -r requirements.txt
#Pull the model: make sure to have a device that has more than 16GB RAM to run this model locally!
ollama pull gpt-oss:20b
```
### 5. **Run the Streamlit App**:
```bash
streamlit run real_estate_agent_team.py
```
2. **Install the dependencies**:
```bash
pip install -r requirements.txt
```
## Usage Guide
3. **Set up your API key**:
- Get a Firecrawl API key from: [Firecrawl website](https://firecrawl.dev)
### 1. **Configuration (Sidebar)**:
- Enter your API keys (or use environment variables)
- Select real estate websites to search
- View the 3-agent workflow explanation
4. **Run the local Streamlit app**:
```bash
streamlit run local_ai_real_estate_agent_team.py
```
### 2. **Property Requirements**:
- **Location**: City and state/province
- **Budget**: Minimum and maximum price range
- **Property Details**: Type, bedrooms, bathrooms, minimum square feet
- **Special Features**: Parking, yard, view, proximity to amenities
- **Timeline & Urgency**: How soon you need to move
## Usage
### 3. **Analysis Process**:
- **Search Phase**: Extracts property data from selected websites
- **Agent Analysis**: Three specialized agents provide insights
- **Results**: Comprehensive report with clickable property links
### **Cloud Version**
### 4. **Understanding Results**:
- **Property Search Agent**: Lists found properties with details
- **Market Analysis Agent**: Provides market trends and neighborhood insights
- **Property Valuation Agent**: Gives investment analysis and valuations
- **Property Links**: Clickable URLs to original listings
1. Enter your API keys in the sidebar:
- Google AI API Key
- Firecrawl API Key
2. Select real estate websites to search from:
- Zillow
- Realtor.com
- Trulia
- Homes.com
3. Configure your property requirements:
- Location (city, state)
- Budget range
- Property details (type, bedrooms, bathrooms, sqft)
- Special features and timeline
4. Click "Start Property Analysis" to generate:
- Property listings with details
- Market analysis and trends
- Property valuations and recommendations
### **Local Version**
1. Enter your Firecrawl API key in the sidebar
2. Ensure Ollama is running with `gpt-oss:20b` model
3. Follow the same property configuration steps as cloud version
4. Get the same comprehensive analysis with local AI processing
## Agent Workflow
### **Property Search Agent**
- Uses Firecrawl extract tools to search real estate websites
- Uses direct Firecrawl integration to search real estate websites
- Focuses on properties matching user criteria
- Falls back to Perplexity search if no properties found
- Extracts structured property data with all details
- Organizes results with clickable listing URLs
### **Market Analysis Agent**
- **Market Trends**: Current conditions, price trends, inventory levels
- **Neighborhood Analysis**: Schools, safety, amenities, transportation
- **Investment Insights**: Potential assessment, rental data, development plans
- **Comparative Analysis**: Market comparisons and unique advantages
- **Market Condition**: Buyer's/seller's market, price trends
- **Key Neighborhoods**: Brief overview of areas where properties are located
- **Investment Outlook**: 2-3 key points about investment potential
- **Format**: Concise bullet points under 100 words per section
### **Property Valuation Agent**
- **Property Valuation**: Fair market value with detailed reasoning
- **Pricing Assessment**: Over/under-priced analysis with strategies
- **Investment Analysis**: ROI projections and risk assessment
- **Features Evaluation**: Detailed property analysis and improvements
- **Market Positioning**: Competitive analysis and target profiles
- **Value Assessment**: Fair price, over/under priced analysis
- **Investment Potential**: High/Medium/Low with brief reasoning
- **Key Recommendation**: One actionable insight per property
- **Format**: Brief assessments under 50 words per property
## Technical Architecture
### **Data Sources**:
- **Firecrawl Extract API**: Structured property data extraction
- **Perplexity AI**: AI-powered search across multiple sources
- **Pydantic Schemas**: Structured data validation and formatting
### **AI Framework**:
- **Agno Framework**: Multi-agent coordination and communication
- **OpenAI GPT-4**: Advanced language model for analysis
- **Cloud Version**: Agno Framework with Google Gemini 2.5 Flash
- **Local Version**: Agno Framework with Ollama gpt-oss:20b
- **Streamlit**: Interactive web application interface
### **Performance Features**:
- **Rate Limiting**: Prevents API overload with intelligent delays
- **Sequential Execution**: Manual coordination for optimal performance
- **Progress Tracking**: Real-time updates on analysis progress
- **Timeout Handling**: Prevents hanging with 3-minute agent timeout
- **Error Recovery**: Graceful fallback when primary methods fail
- **Error Recovery**: Graceful handling of extraction failures
- **Direct Integration**: Bypasses tool wrappers for faster execution
## File Structure
```
ai_real_estate_agent_team/
├── real_estate_agent_team.py # Main application file
├── requirements.txt # Python dependencies
├── README.md # This documentation
└── .env # Environment variables (create this)
├── real_estate_agent_team.py # API version (Google Gemini)
├── local_ai_real_estate_agent_team.py # Local version (Ollama)
├── requirements.txt # Python dependencies
├── README.md # This documentation
└── .env # Environment variables (create this)
```
## API Requirements
### **OpenAI API**
- **Model**: GPT-4o
- **Usage**: Multi-agent analysis and property insights
- **Rate Limits**: Standard OpenAI rate limits apply
### **Cloud Version**
### **Firecrawl API**
#### **Google AI API**
- **Model**: Gemini 2.5 Flash
- **Usage**: Multi-agent analysis and property insights
- **Rate Limits**: Standard Google AI rate limits apply
#### **Firecrawl API**
- **Endpoint**: Extract API for structured data
- **Usage**: Property listing extraction from real estate websites
- **Rate Limits**: Firecrawl standard rate limits
### **Google Search**
- **Tool**: Agno GoogleSearchTools
- **Usage**: Web search for property listings fallback
- **Rate Limits**: Google Search standard rate limits
### **Local Version**
## Troubleshooting
#### **Firecrawl API**
- **Endpoint**: Extract API for structured data
- **Usage**: Property listing extraction from real estate websites
- **Rate Limits**: Firecrawl standard rate limits
### **Common Issues**:
#### **Ollama (Local)**
- **Model**: gpt-oss:20b
- **Usage**: All AI processing locally
- **Requirements**: ~16GB RAM recommended
- **No API costs**: Completely local processing
1. **"No properties found"**:
- This is normal for specific criteria
- Perplexity fallback will provide market insights
- Try broadening your search criteria
2. **API Key Errors**:
- Ensure all API keys are valid and have sufficient credits
- Check environment variables are properly set
- Verify API key permissions
3. **Slow Performance**:
- Reduce number of selected websites
- Simplify property criteria
- Check internet connection
4. **Agent Timeout**:
- Simplify search criteria
- Reduce number of websites
- Try again with different parameters
### **Performance Tips**:
- Start with 1-2 websites for testing
- Use specific but not overly restrictive criteria
- Monitor timing information for optimization

View file

@ -0,0 +1,836 @@
import os
import streamlit as st
import json
import time
import re
from agno.agent import Agent
from agno.models.google import Gemini
from dotenv import load_dotenv
from firecrawl import FirecrawlApp
from pydantic import BaseModel, Field
from typing import List, Optional
# Load environment variables
load_dotenv()
# API keys - must be set in environment variables
DEFAULT_GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
DEFAULT_FIRECRAWL_API_KEY = os.getenv("FIRECRAWL_API_KEY")
# Pydantic schemas
class PropertyDetails(BaseModel):
address: str = Field(description="Full property address")
price: Optional[str] = Field(description="Property price")
bedrooms: Optional[str] = Field(description="Number of bedrooms")
bathrooms: Optional[str] = Field(description="Number of bathrooms")
square_feet: Optional[str] = Field(description="Square footage")
property_type: Optional[str] = Field(description="Type of property")
description: Optional[str] = Field(description="Property description")
features: Optional[List[str]] = Field(description="Property features")
images: Optional[List[str]] = Field(description="Property image URLs")
agent_contact: Optional[str] = Field(description="Agent contact information")
listing_url: Optional[str] = Field(description="Original listing URL")
class PropertyListing(BaseModel):
properties: List[PropertyDetails] = Field(description="List of properties found")
total_count: int = Field(description="Total number of properties found")
source_website: str = Field(description="Website where properties were found")
class DirectFirecrawlAgent:
"""Agent with direct Firecrawl integration for property search"""
def __init__(self, firecrawl_api_key: str, google_api_key: str, model_id: str = "gemini-2.5-flash"):
self.agent = Agent(
model=Gemini(id=model_id, api_key=google_api_key),
markdown=True,
description="I am a real estate expert who helps find and analyze properties based on user preferences."
)
self.firecrawl = FirecrawlApp(api_key=firecrawl_api_key)
def find_properties_direct(self, city: str, state: str, user_criteria: dict, selected_websites: list) -> dict:
"""Direct Firecrawl integration for property search"""
city_formatted = city.replace(' ', '-').lower()
state_upper = state.upper() if state else ''
# Create URLs for selected websites
state_lower = state.lower() if state else ''
city_trulia = city.replace(' ', '_') # Trulia uses underscores for spaces
search_urls = {
"Zillow": f"https://www.zillow.com/homes/for_sale/{city_formatted}-{state_upper}/",
"Realtor.com": f"https://www.realtor.com/realestateandhomes-search/{city_formatted}_{state_upper}/pg-1",
"Trulia": f"https://www.trulia.com/{state_upper}/{city_trulia}/",
"Homes.com": f"https://www.homes.com/homes-for-sale/{city_formatted}-{state_lower}/"
}
# Filter URLs based on selected websites
urls_to_search = [url for site, url in search_urls.items() if site in selected_websites]
print(f"Selected websites: {selected_websites}")
print(f"URLs to search: {urls_to_search}")
if not urls_to_search:
return {"error": "No websites selected"}
# Create comprehensive prompt with specific schema guidance
prompt = f"""You are extracting property listings from real estate websites. Extract EVERY property listing you can find on the page.
USER SEARCH CRITERIA:
- Budget: {user_criteria.get('budget_range', 'Any')}
- Property Type: {user_criteria.get('property_type', 'Any')}
- Bedrooms: {user_criteria.get('bedrooms', 'Any')}
- Bathrooms: {user_criteria.get('bathrooms', 'Any')}
- Min Square Feet: {user_criteria.get('min_sqft', 'Any')}
- Special Features: {user_criteria.get('special_features', 'Any')}
EXTRACTION INSTRUCTIONS:
1. Find ALL property listings on the page (usually 20-40 per page)
2. For EACH property, extract these fields:
- address: Full street address (required)
- price: Listed price with $ symbol (required)
- bedrooms: Number of bedrooms (required)
- bathrooms: Number of bathrooms (required)
- square_feet: Square footage if available
- property_type: House/Condo/Townhouse/Apartment etc.
- description: Brief property description if available
- listing_url: Direct link to property details if available
- agent_contact: Agent name/phone if visible
3. CRITICAL REQUIREMENTS:
- Extract AT LEAST 10 properties if they exist on the page
- Do NOT skip properties even if some fields are missing
- Use "Not specified" for missing optional fields
- Ensure address and price are always filled
- Look for property cards, listings, search results
4. RETURN FORMAT:
- Return JSON with "properties" array containing all extracted properties
- Each property should be a complete object with all available fields
- Set "total_count" to the number of properties extracted
- Set "source_website" to the main website name (Zillow/Realtor/Trulia/Homes)
EXTRACT EVERY VISIBLE PROPERTY LISTING - DO NOT LIMIT TO JUST A FEW!
"""
try:
# Direct Firecrawl call - using correct API format
print(f"Calling Firecrawl with {len(urls_to_search)} URLs")
raw_response = self.firecrawl.extract(
urls_to_search,
prompt=prompt,
schema=PropertyListing.model_json_schema()
)
print("Raw Firecrawl Response:", raw_response)
if hasattr(raw_response, 'success') and raw_response.success:
# Handle Firecrawl response object
properties = raw_response.data.get('properties', []) if hasattr(raw_response, 'data') else []
total_count = raw_response.data.get('total_count', 0) if hasattr(raw_response, 'data') else 0
print(f"Response data keys: {list(raw_response.data.keys()) if hasattr(raw_response, 'data') else 'No data'}")
elif isinstance(raw_response, dict) and raw_response.get('success'):
# Handle dictionary response
properties = raw_response['data'].get('properties', [])
total_count = raw_response['data'].get('total_count', 0)
print(f"Response data keys: {list(raw_response['data'].keys())}")
else:
properties = []
total_count = 0
print(f"Response failed or unexpected format: {type(raw_response)}")
print(f"Extracted {len(properties)} properties from {total_count} total found")
# Debug: Print first property if available
if properties:
print(f"First property sample: {properties[0]}")
return {
'success': True,
'properties': properties,
'total_count': len(properties),
'source_websites': selected_websites
}
else:
# Enhanced error message with debugging info
error_msg = f"""No properties extracted despite finding {total_count} listings.
POSSIBLE CAUSES:
1. Website structure changed - extraction schema doesn't match
2. Website blocking or requiring interaction (captcha, login)
3. Properties don't match specified criteria too strictly
4. Extraction prompt needs refinement for this website
SUGGESTIONS:
- Try different websites (Zillow, Realtor.com, Trulia, Homes.com)
- Broaden search criteria (Any bedrooms, Any type, etc.)
- Check if website requires specific user interaction
Debug Info: Found {total_count} listings but extraction returned empty array."""
return {"error": error_msg}
except Exception as e:
return {"error": f"Firecrawl extraction failed: {str(e)}"}
def create_sequential_agents(llm, user_criteria):
"""Create agents for sequential manual execution"""
property_search_agent = Agent(
name="Property Search Agent",
model=llm,
instructions="""
You are a property search expert. Your role is to find and extract property listings.
WORKFLOW:
1. SEARCH FOR PROPERTIES:
- Use the provided Firecrawl data to extract property listings
- Focus on properties matching user criteria
- Extract detailed property information
2. EXTRACT PROPERTY DATA:
- Address, price, bedrooms, bathrooms, square footage
- Property type, features, listing URLs
- Agent contact information
3. PROVIDE STRUCTURED OUTPUT:
- List properties with complete details
- Include all listing URLs
- Rank by match quality to user criteria
IMPORTANT:
- Focus ONLY on finding and extracting property data
- Do NOT provide market analysis or valuations
- Your output will be used by other agents for analysis
""",
)
market_analysis_agent = Agent(
name="Market Analysis Agent",
model=llm,
instructions="""
You are a market analysis expert. Provide CONCISE market insights.
REQUIREMENTS:
- Keep analysis brief and to the point
- Focus on key market trends only
- Provide 2-3 bullet points per area
- Avoid repetition and lengthy explanations
COVER:
1. Market Condition: Buyer's/seller's market, price trends
2. Key Neighborhoods: Brief overview of areas where properties are located
3. Investment Outlook: 2-3 key points about investment potential
FORMAT: Use bullet points and keep each section under 100 words.
""",
)
property_valuation_agent = Agent(
name="Property Valuation Agent",
model=llm,
instructions="""
You are a property valuation expert. Provide CONCISE property assessments.
REQUIREMENTS:
- Keep each property assessment brief (2-3 sentences max)
- Focus on key points only: value, investment potential, recommendation
- Avoid lengthy analysis and repetition
- Use bullet points for clarity
FOR EACH PROPERTY, PROVIDE:
1. Value Assessment: Fair price, over/under priced
2. Investment Potential: High/Medium/Low with brief reason
3. Key Recommendation: One actionable insight
FORMAT:
- Use bullet points
- Keep each property under 50 words
- Focus on actionable insights only
""",
)
return property_search_agent, market_analysis_agent, property_valuation_agent
def run_sequential_analysis(city, state, user_criteria, selected_websites, firecrawl_api_key, google_api_key, update_callback):
"""Run agents sequentially with manual coordination"""
# Initialize agents
llm = Gemini(id="gemini-2.5-flash", api_key=google_api_key)
property_search_agent, market_analysis_agent, property_valuation_agent = create_sequential_agents(llm, user_criteria)
# Step 1: Property Search with Direct Firecrawl Integration
update_callback(0.2, "Searching properties...", "🔍 Property Search Agent: Finding properties...")
direct_agent = DirectFirecrawlAgent(
firecrawl_api_key=firecrawl_api_key,
google_api_key=google_api_key,
model_id="gemini-2.5-flash"
)
properties_data = direct_agent.find_properties_direct(
city=city,
state=state,
user_criteria=user_criteria,
selected_websites=selected_websites
)
if "error" in properties_data:
return f"Error in property search: {properties_data['error']}"
properties = properties_data.get('properties', [])
if not properties:
return "No properties found matching your criteria."
update_callback(0.4, "Properties found", f"✅ Found {len(properties)} properties")
# Step 2: Market Analysis
update_callback(0.5, "Analyzing market...", "📊 Market Analysis Agent: Analyzing market trends...")
market_analysis_prompt = f"""
Provide CONCISE market analysis for these properties:
PROPERTIES: {len(properties)} properties in {city}, {state}
BUDGET: {user_criteria.get('budget_range', 'Any')}
Give BRIEF insights on:
Market condition (buyer's/seller's market)
Key neighborhoods where properties are located
Investment outlook (2-3 bullet points max)
Keep each section under 100 words. Use bullet points.
"""
market_result = market_analysis_agent.run(market_analysis_prompt)
market_analysis = market_result.content
update_callback(0.7, "Market analysis complete", "✅ Market analysis completed")
# Step 3: Property Valuation
update_callback(0.8, "Evaluating properties...", "💰 Property Valuation Agent: Evaluating properties...")
# Create detailed property list for valuation
properties_for_valuation = []
for i, prop in enumerate(properties, 1):
if isinstance(prop, dict):
prop_data = {
'number': i,
'address': prop.get('address', 'Address not available'),
'price': prop.get('price', 'Price not available'),
'property_type': prop.get('property_type', 'Type not available'),
'bedrooms': prop.get('bedrooms', 'Not specified'),
'bathrooms': prop.get('bathrooms', 'Not specified'),
'square_feet': prop.get('square_feet', 'Not specified')
}
else:
prop_data = {
'number': i,
'address': getattr(prop, 'address', 'Address not available'),
'price': getattr(prop, 'price', 'Price not available'),
'property_type': getattr(prop, 'property_type', 'Type not available'),
'bedrooms': getattr(prop, 'bedrooms', 'Not specified'),
'bathrooms': getattr(prop, 'bathrooms', 'Not specified'),
'square_feet': getattr(prop, 'square_feet', 'Not specified')
}
properties_for_valuation.append(prop_data)
valuation_prompt = f"""
Provide CONCISE property assessments for each property. Use the EXACT format shown below:
USER BUDGET: {user_criteria.get('budget_range', 'Any')}
PROPERTIES TO EVALUATE:
{json.dumps(properties_for_valuation, indent=2)}
For EACH property, provide assessment in this EXACT format:
**Property [NUMBER]: [ADDRESS]**
Value: [Fair price/Over priced/Under priced] - [brief reason]
Investment Potential: [High/Medium/Low] - [brief reason]
Recommendation: [One actionable insight]
REQUIREMENTS:
- Start each assessment with "**Property [NUMBER]:**"
- Keep each property assessment under 50 words
- Analyze ALL {len(properties)} properties individually
- Use bullet points as shown
"""
valuation_result = property_valuation_agent.run(valuation_prompt)
property_valuations = valuation_result.content
update_callback(0.9, "Valuation complete", "✅ Property valuations completed")
# Step 4: Final Synthesis
update_callback(0.95, "Synthesizing results...", "🤖 Synthesizing final recommendations...")
# Debug: Check properties structure
print(f"Properties type: {type(properties)}")
print(f"Properties length: {len(properties)}")
if properties:
print(f"First property type: {type(properties[0])}")
print(f"First property: {properties[0]}")
# Format properties for better display
properties_display = ""
for i, prop in enumerate(properties, 1):
# Handle both dict and object access
if isinstance(prop, dict):
address = prop.get('address', 'Address not available')
price = prop.get('price', 'Price not available')
prop_type = prop.get('property_type', 'Type not available')
bedrooms = prop.get('bedrooms', 'Not specified')
bathrooms = prop.get('bathrooms', 'Not specified')
square_feet = prop.get('square_feet', 'Not specified')
agent_contact = prop.get('agent_contact', 'Contact not available')
description = prop.get('description', 'No description available')
listing_url = prop.get('listing_url', '#')
else:
# Handle object access
address = getattr(prop, 'address', 'Address not available')
price = getattr(prop, 'price', 'Price not available')
prop_type = getattr(prop, 'property_type', 'Type not available')
bedrooms = getattr(prop, 'bedrooms', 'Not specified')
bathrooms = getattr(prop, 'bathrooms', 'Not specified')
square_feet = getattr(prop, 'square_feet', 'Not specified')
agent_contact = getattr(prop, 'agent_contact', 'Contact not available')
description = getattr(prop, 'description', 'No description available')
listing_url = getattr(prop, 'listing_url', '#')
properties_display += f"""
### Property {i}: {address}
**Price:** {price}
**Type:** {prop_type}
**Bedrooms:** {bedrooms} | **Bathrooms:** {bathrooms}
**Square Feet:** {square_feet}
**Agent Contact:** {agent_contact}
**Description:** {description}
**Listing URL:** [View Property]({listing_url})
---
"""
final_synthesis = f"""
# 🏠 Property Listings Found
**Total Properties:** {len(properties)} properties matching your criteria
{properties_display}
---
# 📊 Market Analysis & Investment Insights
{market_analysis}
---
# 💰 Property Valuations & Recommendations
{property_valuations}
---
# 🔗 All Property Links
"""
# Extract and add property links
all_text = f"{json.dumps(properties, indent=2)} {market_analysis} {property_valuations}"
urls = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', all_text)
if urls:
final_synthesis += "\n### Available Property Links:\n"
for i, url in enumerate(set(urls), 1):
final_synthesis += f"{i}. {url}\n"
update_callback(1.0, "Analysis complete", "🎉 Complete analysis ready!")
# Return structured data for better UI display
return {
'properties': properties,
'market_analysis': market_analysis,
'property_valuations': property_valuations,
'markdown_synthesis': final_synthesis,
'total_properties': len(properties)
}
def extract_property_valuation(property_valuations, property_number, property_address):
"""Extract valuation for a specific property from the full analysis"""
if not property_valuations:
return None
# Split by property sections - look for the formatted property headers
sections = property_valuations.split('**Property')
# Look for the specific property number
for section in sections:
if section.strip().startswith(f"{property_number}:"):
# Add back the "**Property" prefix and clean up
clean_section = f"**Property{section}".strip()
# Remove any extra asterisks at the end
clean_section = clean_section.replace('**', '**').replace('***', '**')
return clean_section
# Fallback: look for property number mentions in any format
all_sections = property_valuations.split('\n\n')
for section in all_sections:
if (f"Property {property_number}" in section or
f"#{property_number}" in section):
return section
# Last resort: try to match by address
for section in all_sections:
if any(word in section.lower() for word in property_address.lower().split()[:3] if len(word) > 2):
return section
# If no specific match found, return indication that analysis is not available
return f"**Property {property_number} Analysis**\n• Analysis: Individual assessment not available\n• Recommendation: Review general market analysis in the Market Analysis tab"
def display_properties_professionally(properties, market_analysis, property_valuations, total_properties):
"""Display properties in a clean, professional UI using Streamlit components"""
# Header with key metrics
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Properties Found", total_properties)
with col2:
# Calculate average price
prices = []
for p in properties:
price_str = p.get('price', '') if isinstance(p, dict) else getattr(p, 'price', '')
if price_str and price_str != 'Price not available':
try:
price_num = ''.join(filter(str.isdigit, str(price_str)))
if price_num:
prices.append(int(price_num))
except:
pass
avg_price = f"${sum(prices) // len(prices):,}" if prices else "N/A"
st.metric("Average Price", avg_price)
with col3:
types = {}
for p in properties:
t = p.get('property_type', 'Unknown') if isinstance(p, dict) else getattr(p, 'property_type', 'Unknown')
types[t] = types.get(t, 0) + 1
most_common = max(types.items(), key=lambda x: x[1])[0] if types else "N/A"
st.metric("Most Common Type", most_common)
# Create tabs for different views
tab1, tab2, tab3 = st.tabs(["🏠 Properties", "📊 Market Analysis", "💰 Valuations"])
with tab1:
for i, prop in enumerate(properties, 1):
# Extract property data
data = {k: prop.get(k, '') if isinstance(prop, dict) else getattr(prop, k, '')
for k in ['address', 'price', 'property_type', 'bedrooms', 'bathrooms', 'square_feet', 'description', 'listing_url']}
with st.container():
# Property header with number and price
col1, col2 = st.columns([3, 1])
with col1:
st.subheader(f"#{i} 🏠 {data['address']}")
with col2:
st.metric("Price", data['price'])
# Property details with right-aligned button
col1, col2, col3 = st.columns([2, 2, 1])
with col1:
st.markdown(f"**Type:** {data['property_type']}")
st.markdown(f"**Beds/Baths:** {data['bedrooms']}/{data['bathrooms']}")
st.markdown(f"**Area:** {data['square_feet']}")
with col2:
with st.expander("💰 Investment Analysis"):
# Extract property-specific valuation from the full analysis
property_valuation = extract_property_valuation(property_valuations, i, data['address'])
if property_valuation:
st.markdown(property_valuation)
else:
st.info("Investment analysis not available for this property")
with col3:
if data['listing_url'] and data['listing_url'] != '#':
st.markdown(
f"""
<div style="height: 100%; display: flex; align-items: center; justify-content: flex-end;">
<a href="{data['listing_url']}" target="_blank"
style="text-decoration: none; padding: 0.5rem 1rem;
background-color: #0066cc; color: white;
border-radius: 6px; font-size: 0.9em; font-weight: 500;">
Property Link
</a>
</div>
""",
unsafe_allow_html=True
)
st.divider()
with tab2:
st.subheader("📊 Market Analysis")
if market_analysis:
for section in market_analysis.split('\n\n'):
if section.strip():
st.markdown(section)
else:
st.info("No market analysis available")
with tab3:
st.subheader("💰 Investment Analysis")
if property_valuations:
for section in property_valuations.split('\n\n'):
if section.strip():
st.markdown(section)
else:
st.info("No valuation data available")
def main():
st.set_page_config(
page_title="AI Real Estate Agent Team",
page_icon="🏠",
layout="wide",
initial_sidebar_state="expanded"
)
# Clean header
st.title("🏠 AI Real Estate Agent Team")
st.caption("Find Your Dream Home with Specialized AI Agents")
# Sidebar configuration
with st.sidebar:
st.header("⚙️ Configuration")
# API Key inputs with validation
with st.expander("🔑 API Keys", expanded=True):
google_key = st.text_input(
"Google AI API Key",
value=DEFAULT_GOOGLE_API_KEY,
type="password",
help="Get your API key from https://aistudio.google.com/app/apikey",
placeholder="AIza..."
)
firecrawl_key = st.text_input(
"Firecrawl API Key",
value=DEFAULT_FIRECRAWL_API_KEY,
type="password",
help="Get your API key from https://firecrawl.dev",
placeholder="fc_..."
)
# Update environment variables
if google_key: os.environ["GOOGLE_API_KEY"] = google_key
if firecrawl_key: os.environ["FIRECRAWL_API_KEY"] = firecrawl_key
# Website selection
with st.expander("🌐 Search Sources", expanded=True):
st.markdown("**Select real estate websites to search:**")
available_websites = ["Zillow", "Realtor.com", "Trulia", "Homes.com"]
selected_websites = [site for site in available_websites if st.checkbox(site, value=site in ["Zillow", "Realtor.com"])]
if selected_websites:
st.markdown(f'{len(selected_websites)} sources selected</div>', unsafe_allow_html=True)
else:
st.markdown('<div class="status-error">⚠️ Please select at least one website</div>', unsafe_allow_html=True)
# How it works
with st.expander("🤖 How It Works", expanded=False):
st.markdown("**🔍 Property Search Agent**")
st.markdown("Uses direct Firecrawl integration to find properties")
st.markdown("**📊 Market Analysis Agent**")
st.markdown("Analyzes market trends and neighborhood insights")
st.markdown("**💰 Property Valuation Agent**")
st.markdown("Evaluates properties and provides investment analysis")
# Main form
st.header("Your Property Requirements")
st.info("Please provide the location, budget, and property details to help us find your ideal home.")
with st.form("property_preferences"):
# Location and Budget Section
st.markdown("### 📍 Location & Budget")
col1, col2 = st.columns(2)
with col1:
city = st.text_input(
"🏙️ City",
placeholder="e.g., San Francisco",
help="Enter the city where you want to buy property"
)
state = st.text_input(
"🗺️ State/Province (optional)",
placeholder="e.g., CA",
help="Enter the state or province (optional)"
)
with col2:
min_price = st.number_input(
"💰 Minimum Price ($)",
min_value=0,
value=500000,
step=50000,
help="Your minimum budget for the property"
)
max_price = st.number_input(
"💰 Maximum Price ($)",
min_value=0,
value=1500000,
step=50000,
help="Your maximum budget for the property"
)
# Property Details Section
st.markdown("### 🏡 Property Details")
col1, col2, col3 = st.columns(3)
with col1:
property_type = st.selectbox(
"🏠 Property Type",
["Any", "House", "Condo", "Townhouse", "Apartment"],
help="Type of property you're looking for"
)
bedrooms = st.selectbox(
"🛏️ Bedrooms",
["Any", "1", "2", "3", "4", "5+"],
help="Number of bedrooms required"
)
with col2:
bathrooms = st.selectbox(
"🚿 Bathrooms",
["Any", "1", "1.5", "2", "2.5", "3", "3.5", "4+"],
help="Number of bathrooms required"
)
min_sqft = st.number_input(
"📏 Minimum Square Feet",
min_value=0,
value=1000,
step=100,
help="Minimum square footage required"
)
with col3:
timeline = st.selectbox(
"⏰ Timeline",
["Flexible", "1-3 months", "3-6 months", "6+ months"],
help="When do you plan to buy?"
)
urgency = st.selectbox(
"🚨 Urgency",
["Not urgent", "Somewhat urgent", "Very urgent"],
help="How urgent is your purchase?"
)
# Special Features
st.markdown("### ✨ Special Features")
special_features = st.text_area(
"🎯 Special Features & Requirements",
placeholder="e.g., Parking, Yard, View, Near public transport, Good schools, Walkable neighborhood, etc.",
help="Any specific features or requirements you're looking for"
)
# Submit button with custom styling
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
submitted = st.form_submit_button(
"🚀 Start Property Analysis",
type="primary",
use_container_width=True
)
# Process form submission
if submitted:
# Validate all required inputs
missing_items = []
if not google_key:
missing_items.append("Google AI API Key")
if not firecrawl_key:
missing_items.append("Firecrawl API Key")
if not city:
missing_items.append("City")
if not selected_websites:
missing_items.append("At least one website selection")
if missing_items:
st.markdown(f"""
<div class="status-error" style="text-align: center; margin: 2rem 0;">
Please provide: {', '.join(missing_items)}
</div>
""", unsafe_allow_html=True)
return
try:
user_criteria = {
'budget_range': f"${min_price:,} - ${max_price:,}",
'property_type': property_type,
'bedrooms': bedrooms,
'bathrooms': bathrooms,
'min_sqft': min_sqft,
'special_features': special_features if special_features else 'None specified'
}
except Exception as e:
st.markdown(f"""
<div class="status-error" style="text-align: center; margin: 2rem 0;">
Error initializing: {str(e)}
</div>
""", unsafe_allow_html=True)
return
# Display progress
st.markdown("#### Property Analysis in Progress")
st.info("AI Agents are searching for your perfect home...")
status_container = st.container()
with status_container:
st.markdown("### 📊 Current Activity")
progress_bar = st.progress(0)
current_activity = st.empty()
def update_progress(progress, status, activity=None):
if activity:
progress_bar.progress(progress)
current_activity.text(activity)
try:
start_time = time.time()
update_progress(0.1, "Initializing...", "Starting sequential property analysis")
# Run sequential analysis with manual coordination
final_result = run_sequential_analysis(
city=city,
state=state,
user_criteria=user_criteria,
selected_websites=selected_websites,
firecrawl_api_key=firecrawl_key,
google_api_key=google_key,
update_callback=update_progress
)
total_time = time.time() - start_time
# Display results
if isinstance(final_result, dict):
# Use the new professional display
display_properties_professionally(
final_result['properties'],
final_result['market_analysis'],
final_result['property_valuations'],
final_result['total_properties']
)
else:
# Fallback to markdown display
st.markdown("### 🏠 Comprehensive Real Estate Analysis")
st.markdown(final_result)
# Timing info in a subtle way
st.caption(f"Analysis completed in {total_time:.1f}s")
except Exception as e:
st.markdown(f"""
<div class="status-error" style="text-align: center; margin: 2rem 0;">
An error occurred: {str(e)}
</div>
""", unsafe_allow_html=True)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,828 @@
import os
import streamlit as st
import json
import time
import re
from agno.agent import Agent
from agno.models.ollama import Ollama
from dotenv import load_dotenv
from firecrawl import FirecrawlApp
from pydantic import BaseModel, Field
from typing import List, Optional
# Load environment variables
load_dotenv()
# API keys - must be set in environment variables
DEFAULT_FIRECRAWL_API_KEY = os.getenv("FIRECRAWL_API_KEY", "")
# Pydantic schemas
class PropertyDetails(BaseModel):
address: str = Field(description="Full property address")
price: Optional[str] = Field(description="Property price")
bedrooms: Optional[str] = Field(description="Number of bedrooms")
bathrooms: Optional[str] = Field(description="Number of bathrooms")
square_feet: Optional[str] = Field(description="Square footage")
property_type: Optional[str] = Field(description="Type of property")
description: Optional[str] = Field(description="Property description")
features: Optional[List[str]] = Field(description="Property features")
images: Optional[List[str]] = Field(description="Property image URLs")
agent_contact: Optional[str] = Field(description="Agent contact information")
listing_url: Optional[str] = Field(description="Original listing URL")
class PropertyListing(BaseModel):
properties: List[PropertyDetails] = Field(description="List of properties found")
total_count: int = Field(description="Total number of properties found")
source_website: str = Field(description="Website where properties were found")
class DirectFirecrawlAgent:
"""Agent with direct Firecrawl integration for property search"""
def __init__(self, firecrawl_api_key: str, model_id: str = "gpt-oss:20b"):
self.agent = Agent(
model=Ollama(id=model_id),
markdown=True,
description="I am a real estate expert who helps find and analyze properties based on user preferences."
)
self.firecrawl = FirecrawlApp(api_key=firecrawl_api_key)
def find_properties_direct(self, city: str, state: str, user_criteria: dict, selected_websites: list) -> dict:
"""Direct Firecrawl integration for property search"""
city_formatted = city.replace(' ', '-').lower()
state_upper = state.upper() if state else ''
# Create URLs for selected websites
state_lower = state.lower() if state else ''
city_trulia = city.replace(' ', '_') # Trulia uses underscores for spaces
search_urls = {
"Zillow": f"https://www.zillow.com/homes/for_sale/{city_formatted}-{state_upper}/",
"Realtor.com": f"https://www.realtor.com/realestateandhomes-search/{city_formatted}_{state_upper}/pg-1",
"Trulia": f"https://www.trulia.com/{state_upper}/{city_trulia}/",
"Homes.com": f"https://www.homes.com/homes-for-sale/{city_formatted}-{state_lower}/"
}
# Filter URLs based on selected websites
urls_to_search = [url for site, url in search_urls.items() if site in selected_websites]
print(f"Selected websites: {selected_websites}")
print(f"URLs to search: {urls_to_search}")
if not urls_to_search:
return {"error": "No websites selected"}
# Create comprehensive prompt with specific schema guidance
prompt = f"""You are extracting property listings from real estate websites. Extract EVERY property listing you can find on the page.
USER SEARCH CRITERIA:
- Budget: {user_criteria.get('budget_range', 'Any')}
- Property Type: {user_criteria.get('property_type', 'Any')}
- Bedrooms: {user_criteria.get('bedrooms', 'Any')}
- Bathrooms: {user_criteria.get('bathrooms', 'Any')}
- Min Square Feet: {user_criteria.get('min_sqft', 'Any')}
- Special Features: {user_criteria.get('special_features', 'Any')}
EXTRACTION INSTRUCTIONS:
1. Find ALL property listings on the page (usually 20-40 per page)
2. For EACH property, extract these fields:
- address: Full street address (required)
- price: Listed price with $ symbol (required)
- bedrooms: Number of bedrooms (required)
- bathrooms: Number of bathrooms (required)
- square_feet: Square footage if available
- property_type: House/Condo/Townhouse/Apartment etc.
- description: Brief property description if available
- listing_url: Direct link to property details if available
- agent_contact: Agent name/phone if visible
3. CRITICAL REQUIREMENTS:
- Extract AT LEAST 10 properties if they exist on the page
- Do NOT skip properties even if some fields are missing
- Use "Not specified" for missing optional fields
- Ensure address and price are always filled
- Look for property cards, listings, search results
4. RETURN FORMAT:
- Return JSON with "properties" array containing all extracted properties
- Each property should be a complete object with all available fields
- Set "total_count" to the number of properties extracted
- Set "source_website" to the main website name (Zillow/Realtor/Trulia/Homes)
EXTRACT EVERY VISIBLE PROPERTY LISTING - DO NOT LIMIT TO JUST A FEW!
"""
try:
# Direct Firecrawl call - using correct API format
print(f"Calling Firecrawl with {len(urls_to_search)} URLs")
raw_response = self.firecrawl.extract(
urls_to_search,
prompt=prompt,
schema=PropertyListing.model_json_schema()
)
print("Raw Firecrawl Response:", raw_response)
if hasattr(raw_response, 'success') and raw_response.success:
# Handle Firecrawl response object
properties = raw_response.data.get('properties', []) if hasattr(raw_response, 'data') else []
total_count = raw_response.data.get('total_count', 0) if hasattr(raw_response, 'data') else 0
print(f"Response data keys: {list(raw_response.data.keys()) if hasattr(raw_response, 'data') else 'No data'}")
elif isinstance(raw_response, dict) and raw_response.get('success'):
# Handle dictionary response
properties = raw_response['data'].get('properties', [])
total_count = raw_response['data'].get('total_count', 0)
print(f"Response data keys: {list(raw_response['data'].keys())}")
else:
properties = []
total_count = 0
print(f"Response failed or unexpected format: {type(raw_response)}")
print(f"Extracted {len(properties)} properties from {total_count} total found")
# Debug: Print first property if available
if properties:
print(f"First property sample: {properties[0]}")
return {
'success': True,
'properties': properties,
'total_count': len(properties),
'source_websites': selected_websites
}
else:
# Enhanced error message with debugging info
error_msg = f"""No properties extracted despite finding {total_count} listings.
POSSIBLE CAUSES:
1. Website structure changed - extraction schema doesn't match
2. Website blocking or requiring interaction (captcha, login)
3. Properties don't match specified criteria too strictly
4. Extraction prompt needs refinement for this website
SUGGESTIONS:
- Try different websites (Zillow, Realtor.com, Trulia, Homes.com)
- Broaden search criteria (Any bedrooms, Any type, etc.)
- Check if website requires specific user interaction
Debug Info: Found {total_count} listings but extraction returned empty array."""
return {"error": error_msg}
except Exception as e:
return {"error": f"Firecrawl extraction failed: {str(e)}"}
def create_sequential_agents(llm, user_criteria):
"""Create agents for sequential manual execution"""
property_search_agent = Agent(
name="Property Search Agent",
model=llm,
instructions="""
You are a property search expert. Your role is to find and extract property listings.
WORKFLOW:
1. SEARCH FOR PROPERTIES:
- Use the provided Firecrawl data to extract property listings
- Focus on properties matching user criteria
- Extract detailed property information
2. EXTRACT PROPERTY DATA:
- Address, price, bedrooms, bathrooms, square footage
- Property type, features, listing URLs
- Agent contact information
3. PROVIDE STRUCTURED OUTPUT:
- List properties with complete details
- Include all listing URLs
- Rank by match quality to user criteria
IMPORTANT:
- Focus ONLY on finding and extracting property data
- Do NOT provide market analysis or valuations
- Your output will be used by other agents for analysis
""",
)
market_analysis_agent = Agent(
name="Market Analysis Agent",
model=llm,
instructions="""
You are a market analysis expert. Provide CONCISE market insights.
REQUIREMENTS:
- Keep analysis brief and to the point
- Focus on key market trends only
- Provide 2-3 bullet points per area
- Avoid repetition and lengthy explanations
COVER:
1. Market Condition: Buyer's/seller's market, price trends
2. Key Neighborhoods: Brief overview of areas where properties are located
3. Investment Outlook: 2-3 key points about investment potential
FORMAT: Use bullet points and keep each section under 100 words.
""",
)
property_valuation_agent = Agent(
name="Property Valuation Agent",
model=llm,
instructions="""
You are a property valuation expert. Provide CONCISE property assessments.
REQUIREMENTS:
- Keep each property assessment brief (2-3 sentences max)
- Focus on key points only: value, investment potential, recommendation
- Avoid lengthy analysis and repetition
- Use bullet points for clarity
FOR EACH PROPERTY, PROVIDE:
1. Value Assessment: Fair price, over/under priced
2. Investment Potential: High/Medium/Low with brief reason
3. Key Recommendation: One actionable insight
FORMAT:
- Use bullet points
- Keep each property under 50 words
- Focus on actionable insights only
""",
)
return property_search_agent, market_analysis_agent, property_valuation_agent
def run_sequential_analysis(city, state, user_criteria, selected_websites, firecrawl_api_key, update_callback):
"""Run agents sequentially with manual coordination"""
# Initialize agents
llm = Ollama(id="gpt-oss:20b")
property_search_agent, market_analysis_agent, property_valuation_agent = create_sequential_agents(llm, user_criteria)
# Step 1: Property Search with Direct Firecrawl Integration
update_callback(0.2, "Searching properties...", "🔍 Property Search Agent: Finding properties...")
direct_agent = DirectFirecrawlAgent(
firecrawl_api_key=firecrawl_api_key,
model_id="gpt-oss:20b"
)
properties_data = direct_agent.find_properties_direct(
city=city,
state=state,
user_criteria=user_criteria,
selected_websites=selected_websites
)
if "error" in properties_data:
return f"Error in property search: {properties_data['error']}"
properties = properties_data.get('properties', [])
if not properties:
return "No properties found matching your criteria."
update_callback(0.4, "Properties found", f"✅ Found {len(properties)} properties")
# Step 2: Market Analysis
update_callback(0.5, "Analyzing market...", "📊 Market Analysis Agent: Analyzing market trends...")
market_analysis_prompt = f"""
Provide CONCISE market analysis for these properties:
PROPERTIES: {len(properties)} properties in {city}, {state}
BUDGET: {user_criteria.get('budget_range', 'Any')}
Give BRIEF insights on:
Market condition (buyer's/seller's market)
Key neighborhoods where properties are located
Investment outlook (2-3 bullet points max)
Keep each section under 100 words. Use bullet points.
"""
market_result = market_analysis_agent.run(market_analysis_prompt)
market_analysis = market_result.content
update_callback(0.7, "Market analysis complete", "✅ Market analysis completed")
# Step 3: Property Valuation
update_callback(0.8, "Evaluating properties...", "💰 Property Valuation Agent: Evaluating properties...")
# Create detailed property list for valuation
properties_for_valuation = []
for i, prop in enumerate(properties, 1):
if isinstance(prop, dict):
prop_data = {
'number': i,
'address': prop.get('address', 'Address not available'),
'price': prop.get('price', 'Price not available'),
'property_type': prop.get('property_type', 'Type not available'),
'bedrooms': prop.get('bedrooms', 'Not specified'),
'bathrooms': prop.get('bathrooms', 'Not specified'),
'square_feet': prop.get('square_feet', 'Not specified')
}
else:
prop_data = {
'number': i,
'address': getattr(prop, 'address', 'Address not available'),
'price': getattr(prop, 'price', 'Price not available'),
'property_type': getattr(prop, 'property_type', 'Type not available'),
'bedrooms': getattr(prop, 'bedrooms', 'Not specified'),
'bathrooms': getattr(prop, 'bathrooms', 'Not specified'),
'square_feet': getattr(prop, 'square_feet', 'Not specified')
}
properties_for_valuation.append(prop_data)
valuation_prompt = f"""
Provide CONCISE property assessments for each property. Use the EXACT format shown below:
USER BUDGET: {user_criteria.get('budget_range', 'Any')}
PROPERTIES TO EVALUATE:
{json.dumps(properties_for_valuation, indent=2)}
For EACH property, provide assessment in this EXACT format:
**Property [NUMBER]: [ADDRESS]**
Value: [Fair price/Over priced/Under priced] - [brief reason]
Investment Potential: [High/Medium/Low] - [brief reason]
Recommendation: [One actionable insight]
REQUIREMENTS:
- Start each assessment with "**Property [NUMBER]:**"
- Keep each property assessment under 50 words
- Analyze ALL {len(properties)} properties individually
- Use bullet points as shown
"""
valuation_result = property_valuation_agent.run(valuation_prompt)
property_valuations = valuation_result.content
update_callback(0.9, "Valuation complete", "✅ Property valuations completed")
# Step 4: Final Synthesis
update_callback(0.95, "Synthesizing results...", "🤖 Synthesizing final recommendations...")
# Debug: Check properties structure
print(f"Properties type: {type(properties)}")
print(f"Properties length: {len(properties)}")
if properties:
print(f"First property type: {type(properties[0])}")
print(f"First property: {properties[0]}")
# Format properties for better display
properties_display = ""
for i, prop in enumerate(properties, 1):
# Handle both dict and object access
if isinstance(prop, dict):
address = prop.get('address', 'Address not available')
price = prop.get('price', 'Price not available')
prop_type = prop.get('property_type', 'Type not available')
bedrooms = prop.get('bedrooms', 'Not specified')
bathrooms = prop.get('bathrooms', 'Not specified')
square_feet = prop.get('square_feet', 'Not specified')
agent_contact = prop.get('agent_contact', 'Contact not available')
description = prop.get('description', 'No description available')
listing_url = prop.get('listing_url', '#')
else:
# Handle object access
address = getattr(prop, 'address', 'Address not available')
price = getattr(prop, 'price', 'Price not available')
prop_type = getattr(prop, 'property_type', 'Type not available')
bedrooms = getattr(prop, 'bedrooms', 'Not specified')
bathrooms = getattr(prop, 'bathrooms', 'Not specified')
square_feet = getattr(prop, 'square_feet', 'Not specified')
agent_contact = getattr(prop, 'agent_contact', 'Contact not available')
description = getattr(prop, 'description', 'No description available')
listing_url = getattr(prop, 'listing_url', '#')
properties_display += f"""
### Property {i}: {address}
**Price:** {price}
**Type:** {prop_type}
**Bedrooms:** {bedrooms} | **Bathrooms:** {bathrooms}
**Square Feet:** {square_feet}
**Agent Contact:** {agent_contact}
**Description:** {description}
**Listing URL:** [View Property]({listing_url})
---
"""
final_synthesis = f"""
# 🏠 Property Listings Found
**Total Properties:** {len(properties)} properties matching your criteria
{properties_display}
---
# 📊 Market Analysis & Investment Insights
{market_analysis}
---
# 💰 Property Valuations & Recommendations
{property_valuations}
---
# 🔗 All Property Links
"""
# Extract and add property links
all_text = f"{json.dumps(properties, indent=2)} {market_analysis} {property_valuations}"
urls = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', all_text)
if urls:
final_synthesis += "\n### Available Property Links:\n"
for i, url in enumerate(set(urls), 1):
final_synthesis += f"{i}. {url}\n"
update_callback(1.0, "Analysis complete", "🎉 Complete analysis ready!")
# Return structured data for better UI display
return {
'properties': properties,
'market_analysis': market_analysis,
'property_valuations': property_valuations,
'markdown_synthesis': final_synthesis,
'total_properties': len(properties)
}
def extract_property_valuation(property_valuations, property_number, property_address):
"""Extract valuation for a specific property from the full analysis"""
if not property_valuations:
return None
# Split by property sections - look for the formatted property headers
sections = property_valuations.split('**Property')
# Look for the specific property number
for section in sections:
if section.strip().startswith(f"{property_number}:"):
# Add back the "**Property" prefix and clean up
clean_section = f"**Property{section}".strip()
# Remove any extra asterisks at the end
clean_section = clean_section.replace('**', '**').replace('***', '**')
return clean_section
# Fallback: look for property number mentions in any format
all_sections = property_valuations.split('\n\n')
for section in all_sections:
if (f"Property {property_number}" in section or
f"#{property_number}" in section):
return section
# Last resort: try to match by address
for section in all_sections:
if any(word in section.lower() for word in property_address.lower().split()[:3] if len(word) > 2):
return section
# If no specific match found, return indication that analysis is not available
return f"**Property {property_number} Analysis**\n• Analysis: Individual assessment not available\n• Recommendation: Review general market analysis in the Market Analysis tab"
def display_properties_professionally(properties, market_analysis, property_valuations, total_properties):
"""Display properties in a clean, professional UI using Streamlit components"""
# Header with key metrics
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Properties Found", total_properties)
with col2:
# Calculate average price
prices = []
for p in properties:
price_str = p.get('price', '') if isinstance(p, dict) else getattr(p, 'price', '')
if price_str and price_str != 'Price not available':
try:
price_num = ''.join(filter(str.isdigit, str(price_str)))
if price_num:
prices.append(int(price_num))
except:
pass
avg_price = f"${sum(prices) // len(prices):,}" if prices else "N/A"
st.metric("Average Price", avg_price)
with col3:
types = {}
for p in properties:
t = p.get('property_type', 'Unknown') if isinstance(p, dict) else getattr(p, 'property_type', 'Unknown')
types[t] = types.get(t, 0) + 1
most_common = max(types.items(), key=lambda x: x[1])[0] if types else "N/A"
st.metric("Most Common Type", most_common)
# Create tabs for different views
tab1, tab2, tab3 = st.tabs(["🏠 Properties", "📊 Market Analysis", "💰 Valuations"])
with tab1:
for i, prop in enumerate(properties, 1):
# Extract property data
data = {k: prop.get(k, '') if isinstance(prop, dict) else getattr(prop, k, '')
for k in ['address', 'price', 'property_type', 'bedrooms', 'bathrooms', 'square_feet', 'description', 'listing_url']}
with st.container():
# Property header with number and price
col1, col2 = st.columns([3, 1])
with col1:
st.subheader(f"#{i} 🏠 {data['address']}")
with col2:
st.metric("Price", data['price'])
# Property details with right-aligned button
col1, col2, col3 = st.columns([2, 2, 1])
with col1:
st.markdown(f"**Type:** {data['property_type']}")
st.markdown(f"**Beds/Baths:** {data['bedrooms']}/{data['bathrooms']}")
st.markdown(f"**Area:** {data['square_feet']}")
with col2:
with st.expander("💰 Investment Analysis"):
# Extract property-specific valuation from the full analysis
property_valuation = extract_property_valuation(property_valuations, i, data['address'])
if property_valuation:
st.markdown(property_valuation)
else:
st.info("Investment analysis not available for this property")
with col3:
if data['listing_url'] and data['listing_url'] != '#':
st.markdown(
f"""
<div style="height: 100%; display: flex; align-items: center; justify-content: flex-end;">
<a href="{data['listing_url']}" target="_blank"
style="text-decoration: none; padding: 0.5rem 1rem;
background-color: #0066cc; color: white;
border-radius: 6px; font-size: 0.9em; font-weight: 500;">
Property Link
</a>
</div>
""",
unsafe_allow_html=True
)
st.divider()
with tab2:
st.subheader("📊 Market Analysis")
if market_analysis:
for section in market_analysis.split('\n\n'):
if section.strip():
st.markdown(section)
else:
st.info("No market analysis available")
with tab3:
st.subheader("💰 Investment Analysis")
if property_valuations:
for section in property_valuations.split('\n\n'):
if section.strip():
st.markdown(section)
else:
st.info("No valuation data available")
def main():
st.set_page_config(
page_title="Local AI Real Estate Agent Team",
page_icon="🏠",
layout="wide",
initial_sidebar_state="expanded"
)
# Clean header
st.title("🏠 Local AI Real Estate Agent Team")
st.caption("Find Your Dream Home with Local Ollama AI Agents")
# Sidebar configuration
with st.sidebar:
st.header("⚙️ Configuration")
# API Key inputs with validation
with st.expander("🔑 API Keys", expanded=True):
firecrawl_key = st.text_input(
"Firecrawl API Key",
value=DEFAULT_FIRECRAWL_API_KEY,
type="password",
help="Get your API key from https://firecrawl.dev",
placeholder="fc_..."
)
# Update environment variables
if firecrawl_key: os.environ["FIRECRAWL_API_KEY"] = firecrawl_key
# Ollama model info
st.info("🤖 Using Ollama model: gpt-oss:20b (local)")
st.markdown("Make sure Ollama is running with: `ollama run gpt-oss:20b`")
# Website selection
with st.expander("🌐 Search Sources", expanded=True):
st.markdown("**Select real estate websites to search:**")
available_websites = ["Zillow", "Realtor.com", "Trulia", "Homes.com"]
selected_websites = [site for site in available_websites if st.checkbox(site, value=site in ["Zillow", "Realtor.com"])]
if selected_websites:
st.markdown(f'{len(selected_websites)} sources selected')
else:
st.markdown('⚠️ Please select at least one website')
# How it works
with st.expander("🤖 How It Works", expanded=False):
st.markdown("**🔍 Property Search Agent**")
st.markdown("Uses direct Firecrawl integration to find properties")
st.markdown("**📊 Market Analysis Agent**")
st.markdown("Analyzes market trends and neighborhood insights")
st.markdown("**💰 Property Valuation Agent**")
st.markdown("Evaluates properties and provides investment analysis")
# Main form
st.header("Your Property Requirements")
st.info("Please provide the location, budget, and property details to help us find your ideal home.")
with st.form("property_preferences"):
# Location and Budget Section
st.markdown("### 📍 Location & Budget")
col1, col2 = st.columns(2)
with col1:
city = st.text_input(
"🏙️ City",
placeholder="e.g., San Francisco",
help="Enter the city where you want to buy property"
)
state = st.text_input(
"🗺️ State/Province (optional)",
placeholder="e.g., CA",
help="Enter the state or province (optional)"
)
with col2:
min_price = st.number_input(
"💰 Minimum Price ($)",
min_value=0,
value=500000,
step=50000,
help="Your minimum budget for the property"
)
max_price = st.number_input(
"💰 Maximum Price ($)",
min_value=0,
value=1500000,
step=50000,
help="Your maximum budget for the property"
)
# Property Details Section
st.markdown("### 🏡 Property Details")
col1, col2, col3 = st.columns(3)
with col1:
property_type = st.selectbox(
"🏠 Property Type",
["Any", "House", "Condo", "Townhouse", "Apartment"],
help="Type of property you're looking for"
)
bedrooms = st.selectbox(
"🛏️ Bedrooms",
["Any", "1", "2", "3", "4", "5+"],
help="Number of bedrooms required"
)
with col2:
bathrooms = st.selectbox(
"🚿 Bathrooms",
["Any", "1", "1.5", "2", "2.5", "3", "3.5", "4+"],
help="Number of bathrooms required"
)
min_sqft = st.number_input(
"📏 Minimum Square Feet",
min_value=0,
value=1000,
step=100,
help="Minimum square footage required"
)
with col3:
timeline = st.selectbox(
"⏰ Timeline",
["Flexible", "1-3 months", "3-6 months", "6+ months"],
help="When do you plan to buy?"
)
urgency = st.selectbox(
"🚨 Urgency",
["Not urgent", "Somewhat urgent", "Very urgent"],
help="How urgent is your purchase?"
)
# Special Features
st.markdown("### ✨ Special Features")
special_features = st.text_area(
"🎯 Special Features & Requirements",
placeholder="e.g., Parking, Yard, View, Near public transport, Good schools, Walkable neighborhood, etc.",
help="Any specific features or requirements you're looking for"
)
# Submit button with custom styling
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
submitted = st.form_submit_button(
"🚀 Start Property Analysis",
type="primary",
use_container_width=True
)
# Process form submission
if submitted:
# Validate all required inputs
missing_items = []
if not firecrawl_key:
missing_items.append("Firecrawl API Key")
if not city:
missing_items.append("City")
if not selected_websites:
missing_items.append("At least one website selection")
if missing_items:
st.error(f"⚠️ Please provide: {', '.join(missing_items)}")
return
try:
user_criteria = {
'budget_range': f"${min_price:,} - ${max_price:,}",
'property_type': property_type,
'bedrooms': bedrooms,
'bathrooms': bathrooms,
'min_sqft': min_sqft,
'special_features': special_features if special_features else 'None specified'
}
except Exception as e:
st.error(f"❌ Error initializing: {str(e)}")
return
# Display progress
st.markdown("#### Property Analysis in Progress")
st.info("AI Agents are searching for your perfect home...")
status_container = st.container()
with status_container:
st.markdown("### 📊 Current Activity")
progress_bar = st.progress(0)
current_activity = st.empty()
def update_progress(progress, status, activity=None):
if activity:
progress_bar.progress(progress)
current_activity.text(activity)
try:
start_time = time.time()
update_progress(0.1, "Initializing...", "Starting sequential property analysis")
# Run sequential analysis with manual coordination
final_result = run_sequential_analysis(
city=city,
state=state,
user_criteria=user_criteria,
selected_websites=selected_websites,
firecrawl_api_key=firecrawl_key,
update_callback=update_progress
)
total_time = time.time() - start_time
# Display results
if isinstance(final_result, dict):
# Use the new professional display
display_properties_professionally(
final_result['properties'],
final_result['market_analysis'],
final_result['property_valuations'],
final_result['total_properties']
)
else:
# Fallback to markdown display
st.markdown("### 🏠 Comprehensive Real Estate Analysis")
st.markdown(final_result)
# Download button with better styling
download_content = final_result['markdown_synthesis'] if isinstance(final_result, dict) else final_result
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
st.download_button(
label="📄 Download Full Report",
data=download_content,
file_name="property_analysis_report.md",
mime="text/markdown",
use_container_width=True
)
# Timing info in a subtle way
st.caption(f"Analysis completed in {total_time:.1f}s")
except Exception as e:
st.error(f"❌ An error occurred: {str(e)}")
if __name__ == "__main__":
main()

View file

@ -1,777 +0,0 @@
import os
import streamlit as st
import json
import time
import requests
from agno.agent import Agent
from agno.team import Team
from agno.models.openai import OpenAIChat
from agno.tools.googlesearch import GoogleSearchTools
from dotenv import load_dotenv
from firecrawl import FirecrawlApp
from pydantic import BaseModel, Field
from typing import List, Optional
# Load environment variables
load_dotenv()
# API keys - must be set in environment variables
DEFAULT_OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
DEFAULT_FIRECRAWL_API_KEY = os.getenv("FIRECRAWL_API_KEY", "")
# Pydantic schemas
class PropertyDetails(BaseModel):
address: str = Field(description="Full property address")
price: Optional[str] = Field(description="Property price")
bedrooms: Optional[str] = Field(description="Number of bedrooms")
bathrooms: Optional[str] = Field(description="Number of bathrooms")
square_feet: Optional[str] = Field(description="Square footage")
property_type: Optional[str] = Field(description="Type of property")
description: Optional[str] = Field(description="Property description")
features: Optional[List[str]] = Field(description="Property features")
images: Optional[List[str]] = Field(description="Property image URLs")
agent_contact: Optional[str] = Field(description="Agent contact information")
listing_url: Optional[str] = Field(description="Original listing URL")
class PropertyListing(BaseModel):
properties: List[PropertyDetails] = Field(description="List of properties found")
total_count: int = Field(description="Total number of properties found")
source_website: str = Field(description="Website where properties were found")
def get_firecrawl_app():
"""Get FirecrawlApp instance"""
api_key = os.getenv("FIRECRAWL_API_KEY", DEFAULT_FIRECRAWL_API_KEY)
return FirecrawlApp(api_key=api_key)
def extract_property_listings(url, user_criteria=None):
"""Extract property listings from search pages"""
try:
app = get_firecrawl_app()
base_prompt = "Extract property listings from this real estate search page."
if user_criteria:
criteria_prompt = f"""
Focus on properties matching:
- Budget: {user_criteria.get('budget_range', 'Any')}
- Type: {user_criteria.get('property_type', 'Any')}
- Bedrooms: {user_criteria.get('bedrooms', 'Any')}
- Bathrooms: {user_criteria.get('bathrooms', 'Any')}
- Min Sqft: {user_criteria.get('min_sqft', 'Any')}
- Features: {user_criteria.get('special_features', 'Any')}
Extract: address, price, bedrooms, bathrooms, sqft, type, listing URLs.
"""
full_prompt = base_prompt + criteria_prompt
else:
full_prompt = base_prompt + " Extract property information including address, price, bedrooms, bathrooms, square footage, property type, and listing URLs."
data = app.extract([url], prompt=full_prompt, schema=PropertyListing.model_json_schema())
if hasattr(data, 'data'):
return data.data
elif hasattr(data, 'model_dump'):
return data.model_dump()
else:
return {"error": "Unexpected response format"}
except Exception as e:
return {"error": f"Failed to extract listings: {str(e)}"}
def search_google_properties(city, state, user_criteria):
"""Search for properties using Google Search"""
try:
# Create Google Search query
search_query = f"""
Find real estate properties for sale {city} {state}
budget {user_criteria.get('budget_range', '')}
{user_criteria.get('property_type', '')}
{user_criteria.get('bedrooms', '')} bedrooms
{user_criteria.get('bathrooms', '')} bathrooms
{user_criteria.get('min_sqft', '')} sqft
{user_criteria.get('special_features', '')}
site:zillow.com OR site:realtor.com OR site:trulia.com OR site:homes.com OR site:redfin.com
"""
# Use GoogleSearchTools to perform the search
google_search = GoogleSearchTools()
search_results = google_search.google_search(
query=search_query,
max_results=10,
language="en"
)
return {"success": True, "content": search_results, "source": "Google Search"}
except Exception as e:
return {"error": f"Google search failed: {str(e)}"}
def search_real_estate_websites(city, state, user_criteria, selected_websites, update_callback):
"""Search real estate websites"""
results = {}
def create_search_urls(city, state):
city_formatted = city.replace(' ', '-').lower()
state_upper = state.upper() if state else ''
return {
"Zillow": f"https://www.zillow.com/homes/for_sale/{city_formatted}-{state_upper}/",
"Realtor.com": f"https://www.realtor.com/realestateandhomes-search/{city_formatted}_{state_upper}/pg-1",
"Trulia": f"https://www.trulia.com/for_sale/{city_formatted}-{state_upper}/",
"Homes.com": f"https://www.homes.com/homes-for-sale/{city_formatted}-{state_upper}/"
}
search_urls = {site: url for site, url in create_search_urls(city, state).items() if site in selected_websites}
for i, (site_name, search_url) in enumerate(search_urls.items()):
try:
progress = 0.2 + (i * 0.6 / len(search_urls))
update_callback(progress, f"Searching {site_name}...", f"🔍 Analyzing {site_name}...")
if i > 0:
time.sleep(1.5) # Reduced delay for better UX
result = extract_property_listings(search_url, user_criteria)
if "error" not in result and len(result.get('properties', [])) > 0:
results[site_name] = result
property_count = len(result.get('properties', []))
update_callback(progress + 0.3, f"Found {property_count} properties on {site_name}", f"✅ Successfully analyzed {site_name} ({property_count} properties)")
else:
results[site_name] = {"error": f"No data from {site_name}"}
update_callback(progress + 0.3, f"Analyzing {site_name}", f"⚠️ No properties found on {site_name}")
except Exception as e:
results[site_name] = {"error": f"Error: {str(e)}"}
update_callback(progress + 0.3, f"Analyzing {site_name}", f"❌ Error processing {site_name}")
return results
def create_firecrawl_tools(user_criteria):
"""Create tools for agents"""
def extract_listings_tool(url: str) -> str:
result = extract_property_listings(url, user_criteria)
return json.dumps(result, indent=2) if "error" not in result else f"Error: {result['error']}"
def google_search_tool(city: str, state: str) -> str:
result = search_google_properties(city, state, user_criteria)
return json.dumps(result, indent=2) if "error" not in result else f"Error: {result['error']}"
# Include both Firecrawl extract and Google Search tools
tools = [extract_listings_tool, google_search_tool]
return tools
def create_real_estate_agents(llm, firecrawl_tools, user_criteria):
"""Create specialized agents"""
property_search_agent = Agent(
name="Property Search Agent",
model=llm,
tools=firecrawl_tools,
instructions="""
You are a property search expert. Your role:
1. SEARCH FOR PROPERTIES:
- Use Firecrawl extract tools to search real estate websites
- Focus on properties matching user criteria
- Use Google Search tool if extract methods don't find properties
2. GATHER PROPERTY DATA:
- Extract detailed property information
- Collect listing URLs and agent contacts
- Organize results clearly
3. PROVIDE STRUCTURED OUTPUT:
- List properties with full details
- Include clickable listing URLs
- Rank by match quality
IMPORTANT: Use google_search_tool if extract methods don't find properties.
Google Search will find relevant real estate listings from Zillow, Realtor.com, Trulia, Homes.com, and Redfin.
Focus on finding properties that match user's exact criteria.
""",
)
market_analysis_agent = Agent(
name="Market Analysis Agent",
model=llm,
instructions="""
You are a market analysis expert. Provide ELABORATE market insights:
1. MARKET TRENDS:
- Current market condition (buyer's/seller's market)
- Price trends over 6-12 months
- Market direction and key factors
- Inventory levels and supply/demand
2. NEIGHBORHOOD ANALYSIS:
- Top neighborhood features and amenities
- School district ratings and performance
- Safety ratings and crime statistics
- Local amenities (parks, shopping, restaurants)
- Transportation and commute options
- Employment opportunities
3. INVESTMENT INSIGHTS:
- Investment potential assessment
- Price per square foot trends
- Rental market data
- Future development plans
- Economic factors affecting the area
4. COMPARATIVE ANALYSIS:
- Compare with similar markets
- Highlight unique advantages
- Identify potential risks or opportunities
PROVIDE DETAILED, ACTIONABLE INSIGHTS with specific data points.
Include relevant links and sources when possible.
""",
)
property_valuation_agent = Agent(
name="Property Valuation Agent",
model=llm,
instructions="""
You are a property valuation expert. Provide ELABORATE property assessments:
1. PROPERTY VALUATION:
- Fair market value estimates with reasoning
- Price per square foot analysis
- Comparable property analysis
- Value appreciation potential
2. PRICING ASSESSMENT:
- Overpriced/Underpriced/Fair price analysis
- Key pricing factors and market positioning
- Negotiation potential and strategies
- Price history and trends
3. INVESTMENT ANALYSIS:
- Investment potential (high/medium/low) with detailed reasoning
- ROI projections and cash flow analysis
- Key investment factors and considerations
- Risk assessment and mitigation strategies
4. PROPERTY FEATURES EVALUATION:
- Detailed analysis of property features
- Unique selling points and advantages
- Potential improvements and their value impact
- Maintenance considerations and costs
5. MARKET POSITIONING:
- How the property compares to market
- Competitive advantages and disadvantages
- Target buyer/renter profile
- Marketing recommendations
PROVIDE COMPREHENSIVE, DETAILED ANALYSIS with specific recommendations.
Include relevant market data and comparative analysis.
""",
)
return property_search_agent, market_analysis_agent, property_valuation_agent
def create_real_estate_team(llm, firecrawl_tools, user_criteria):
"""Create the real estate team"""
property_search_agent, market_analysis_agent, property_valuation_agent = create_real_estate_agents(llm, firecrawl_tools, user_criteria)
return Team(
name="🏠 AI Real Estate Agent Team",
mode="coordinate",
model=llm,
members=[property_search_agent, market_analysis_agent, property_valuation_agent],
instructions=[
"You are a professional AI Real Estate Agent Team.",
"1. Property Search Agent: Find properties using extract + Google Search fallback",
"2. Market Analysis Agent: Provide ELABORATE market trends and neighborhood insights",
"3. Property Valuation Agent: Give ELABORATE property valuations and investment analysis",
"IMPORTANT: Provide detailed, actionable insights with specific data points.",
"Include relevant links and sources when possible.",
"Work together to provide comprehensive recommendations."
],
show_members_responses=True,
markdown=True,
)
def display_property_results(result):
"""Display property results with clickable links"""
st.markdown("""
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 1.5rem; border-radius: 10px; margin-bottom: 2rem;">
<h3 style="color: white; text-align: center; margin: 0;">🤖 AI-Powered Real Estate Recommendations</h3>
</div>
""", unsafe_allow_html=True)
if hasattr(result, 'content'):
result_text = result.content
else:
result_text = str(result)
# Display the full text result with markdown support for links
st.markdown("### 📋 Analysis Report")
st.markdown(result_text)
# Extract and display clickable links
import re
urls = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', result_text)
if urls:
st.markdown("""
<div style="background: white; padding: 1.5rem; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); margin: 2rem 0;">
<h3 style="color: #667eea; margin-bottom: 1rem;">🔗 Property Links</h3>
""", unsafe_allow_html=True)
for i, url in enumerate(set(urls), 1):
st.markdown(f"""
<div style="margin: 0.5rem 0; padding: 0.5rem; background: #f8f9fa; border-radius: 5px;">
<a href="{url}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 600;">
{i}. {url}
</a>
</div>
""", unsafe_allow_html=True)
st.markdown("</div>", unsafe_allow_html=True)
def main():
st.set_page_config(
page_title="AI Real Estate Agent Team",
page_icon="🏠",
layout="wide",
initial_sidebar_state="expanded"
)
# Custom CSS for better styling
st.markdown("""
<style>
.main-header {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
border-radius: 15px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
}
.main-header h1 {
color: white;
font-size: 3rem;
font-weight: 700;
text-align: center;
margin: 0;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.main-header p {
color: rgba(255,255,255,0.9);
text-align: center;
font-size: 1.2rem;
margin: 0.5rem 0 0 0;
}
.feature-card {
background: white;
padding: 1.5rem;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
margin: 1rem 0;
border-left: 4px solid #667eea;
}
.status-info {
background: linear-gradient(90deg, #2196F3, #1976D2);
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 600;
margin: 0.5rem 0;
text-align: center;
}
.status-success {
background: linear-gradient(90deg, #4CAF50, #45a049);
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 600;
margin: 0.5rem 0;
text-align: center;
}
.status-error {
background: linear-gradient(90deg, #f44336, #da190b);
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 600;
margin: 0.5rem 0;
text-align: center;
}
.form-container {
background: white;
padding: 2rem;
border-radius: 15px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
margin: 2rem 0;
}
.progress-container {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
border-radius: 15px;
margin: 2rem 0;
color: white;
}
.result-container {
background: white;
padding: 2rem;
border-radius: 15px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
margin: 2rem 0;
}
</style>
""", unsafe_allow_html=True)
# Beautiful header
st.markdown("""
<div class="main-header">
<h1>🏠 AI Real Estate Agent Team</h1>
<p>Find Your Dream Home with Specialized AI Agents</p>
</div>
""", unsafe_allow_html=True)
# Sidebar configuration
with st.sidebar:
st.markdown("""
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 1rem; border-radius: 10px; margin-bottom: 1rem;">
<h3 style="color: white; margin: 0; text-align: center;"> Configuration</h3>
</div>
""", unsafe_allow_html=True)
# API Key inputs with validation
with st.expander("🔑 API Keys", expanded=True):
openai_key = st.text_input(
"OpenAI API Key",
value=DEFAULT_OPENAI_API_KEY,
type="password",
help="Get your API key from https://platform.openai.com/api-keys",
placeholder="sk-..."
)
firecrawl_key = st.text_input(
"Firecrawl API Key",
value=DEFAULT_FIRECRAWL_API_KEY,
type="password",
help="Get your API key from https://firecrawl.dev",
placeholder="fc_..."
)
# Update environment variables
if openai_key: os.environ["OPENAI_API_KEY"] = openai_key
if firecrawl_key: os.environ["FIRECRAWL_API_KEY"] = firecrawl_key
# Website selection
with st.expander("🌐 Search Sources", expanded=True):
st.markdown("**Select real estate websites to search:**")
available_websites = ["Zillow", "Realtor.com", "Trulia", "Homes.com"]
selected_websites = [site for site in available_websites if st.checkbox(site, value=site in ["Zillow", "Realtor.com"])]
if selected_websites:
st.markdown(f'<div class="status-success">✅ {len(selected_websites)} sources selected</div>', unsafe_allow_html=True)
else:
st.markdown('<div class="status-error">⚠️ Please select at least one website</div>', unsafe_allow_html=True)
# How it works
with st.expander("🤖 How It Works", expanded=False):
st.markdown("""
<div class="feature-card">
<h4>🔍 Property Search Agent</h4>
<p>Uses extract + Google Search fallback</p>
</div>
<div class="feature-card">
<h4>📊 Market Analysis Agent</h4>
<p>Analyzes trends and neighborhood insights</p>
</div>
<div class="feature-card">
<h4>💰 Property Valuation Agent</h4>
<p>Evaluates values and investment potential</p>
</div>
""", unsafe_allow_html=True)
# Main form
st.markdown("""
<div class="form-container">
<h2 style="color: #667eea; margin-bottom: 1.5rem; text-align: center;">🏠 Your Property Requirements</h2>
""", unsafe_allow_html=True)
with st.form("property_preferences"):
# Location and Budget Section
st.markdown("### 📍 Location & Budget")
col1, col2 = st.columns(2)
with col1:
city = st.text_input(
"🏙️ City",
placeholder="e.g., San Francisco",
help="Enter the city where you want to buy property"
)
state = st.text_input(
"🗺️ State/Province (optional)",
placeholder="e.g., CA",
help="Enter the state or province (optional)"
)
with col2:
min_price = st.number_input(
"💰 Minimum Price ($)",
min_value=0,
value=500000,
step=50000,
help="Your minimum budget for the property"
)
max_price = st.number_input(
"💰 Maximum Price ($)",
min_value=0,
value=1500000,
step=50000,
help="Your maximum budget for the property"
)
# Property Details Section
st.markdown("### 🏡 Property Details")
col1, col2, col3 = st.columns(3)
with col1:
property_type = st.selectbox(
"🏠 Property Type",
["Any", "House", "Condo", "Townhouse", "Apartment"],
help="Type of property you're looking for"
)
bedrooms = st.selectbox(
"🛏️ Bedrooms",
["Any", "1", "2", "3", "4", "5+"],
help="Number of bedrooms required"
)
with col2:
bathrooms = st.selectbox(
"🚿 Bathrooms",
["Any", "1", "1.5", "2", "2.5", "3", "3.5", "4+"],
help="Number of bathrooms required"
)
min_sqft = st.number_input(
"📏 Minimum Square Feet",
min_value=0,
value=1000,
step=100,
help="Minimum square footage required"
)
with col3:
timeline = st.selectbox(
"⏰ Timeline",
["Flexible", "1-3 months", "3-6 months", "6+ months"],
help="When do you plan to buy?"
)
urgency = st.selectbox(
"🚨 Urgency",
["Not urgent", "Somewhat urgent", "Very urgent"],
help="How urgent is your purchase?"
)
# Special Features
st.markdown("### ✨ Special Features")
special_features = st.text_area(
"🎯 Special Features & Requirements",
placeholder="e.g., Parking, Yard, View, Near public transport, Good schools, Walkable neighborhood, etc.",
help="Any specific features or requirements you're looking for"
)
# Submit button with custom styling
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
submitted = st.form_submit_button(
"🚀 Start Property Analysis",
type="primary",
use_container_width=True
)
st.markdown("</div>", unsafe_allow_html=True)
# Process form submission
if submitted:
# Validate all required inputs
missing_items = []
if not openai_key:
missing_items.append("OpenAI API Key")
if not firecrawl_key:
missing_items.append("Firecrawl API Key")
if not city:
missing_items.append("City")
if not selected_websites:
missing_items.append("At least one website selection")
if missing_items:
st.markdown(f"""
<div class="status-error" style="text-align: center; margin: 2rem 0;">
Please provide: {', '.join(missing_items)}
</div>
""", unsafe_allow_html=True)
return
try:
# Initialize components
llm = OpenAIChat(id="gpt-4o", api_key=openai_key)
user_criteria = {
'budget_range': f"${min_price:,} - ${max_price:,}",
'property_type': property_type,
'bedrooms': bedrooms,
'bathrooms': bathrooms,
'min_sqft': min_sqft,
'special_features': special_features if special_features else 'None specified'
}
firecrawl_tools = create_firecrawl_tools(user_criteria)
real_estate_team = create_real_estate_team(llm, firecrawl_tools, user_criteria)
except Exception as e:
st.markdown(f"""
<div class="status-error" style="text-align: center; margin: 2rem 0;">
Error initializing: {str(e)}
</div>
""", unsafe_allow_html=True)
return
# Display progress
st.markdown("""
<div class="progress-container">
<h2 style="color: white; text-align: center; margin-bottom: 1rem;">🚀 Property Analysis in Progress</h2>
<div style="text-align: center; margin-bottom: 1rem;">
<div style="font-size: 2rem; margin-bottom: 0.5rem;">🔍</div>
<div style="font-size: 1.1rem; opacity: 0.9;">AI Agents are working together to find your perfect home</div>
</div>
</div>
""", unsafe_allow_html=True)
status_container = st.container()
with status_container:
st.markdown("### 📊 Current Activity")
progress_bar = st.progress(0)
current_activity = st.empty()
def update_progress(progress, status, activity=None):
if activity:
progress_bar.progress(progress)
current_activity.text(activity)
try:
start_time = time.time()
update_progress(0.1, "Initializing...", "Starting comprehensive property search")
# Search websites
search_start = time.time()
search_results = search_real_estate_websites(city, state, user_criteria, selected_websites, update_progress)
search_duration = time.time() - search_start
# Process results
successful_searches = sum(1 for result in search_results.values() if "error" not in result)
total_properties = sum(len(result.get('properties', [])) for result in search_results.values() if "error" not in result)
use_google_fallback = total_properties == 0
if use_google_fallback:
update_progress(0.85, "Running analysis...", "🔍 Searching Google for real estate listings...")
else:
update_progress(0.85, "Running analysis...", "Property Search Agent: Analyzing search results")
# Run agents
agent_start = time.time()
prompt = f"""
Analyze real estate properties using our specialized agent team:
USER REQUIREMENTS:
LOCATION: {city}, {state if state else 'Any state'}
BUDGET: {user_criteria['budget_range']}
TYPE: {property_type}
BEDROOMS: {bedrooms}
BATHROOMS: {bathrooms}
MIN SQFT: {min_sqft:,}
FEATURES: {user_criteria['special_features']}
TIMELINE: {timeline}
URGENCY: {urgency}
SEARCH RESULTS:
- Websites: {', '.join(selected_websites)}
- Successful: {successful_searches}/{len(selected_websites)}
- Properties found: {total_properties}
AGENT WORKFLOW:
1. Property Search Agent: Find and analyze properties
2. Market Analysis Agent: Provide ELABORATE market insights
3. Property Valuation Agent: Give ELABORATE valuations
IMPORTANT: Provide detailed, actionable insights with specific data points.
Include relevant links and sources when possible.
"""
# Show agent progression with better messaging
agent_messages = [
"🔍 Property Search Agent: Processing property data and listings",
"📊 Market Analysis Agent: Analyzing market trends and neighborhood insights",
"💰 Property Valuation Agent: Evaluating property values and investment potential"
]
for i, message in enumerate(agent_messages):
progress = 0.87 + (i * 0.03)
update_progress(progress, "Analysis in progress...", message)
time.sleep(1.5) # Slightly longer for better UX
# Execute agents with better UX
if use_google_fallback:
with st.spinner("🔍 Searching Google for real estate listings..."):
result = real_estate_team.run(prompt)
else:
with st.spinner("🤖 AI Agents are analyzing your property requirements..."):
result = real_estate_team.run(prompt)
agent_duration = time.time() - agent_start
total_time = time.time() - start_time
# Display results
st.markdown("""
<div class="result-container">
<h2 style="color: #667eea; text-align: center; margin-bottom: 2rem;">🎉 Analysis Complete!</h2>
""", unsafe_allow_html=True)
display_property_results(result)
st.markdown("</div>", unsafe_allow_html=True)
# Download button with better styling
if hasattr(result, 'content'):
download_content = result.content
else:
download_content = str(result)
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
st.download_button(
label="📄 Download Full Report",
data=download_content,
file_name="property_analysis_report.md",
mime="text/markdown",
use_container_width=True
)
# Timing info with better styling
st.markdown(f"""
<div style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); padding: 1rem; border-radius: 10px; text-align: center; margin: 2rem 0;">
<h4 style="color: #667eea; margin-bottom: 0.5rem;"> Performance Metrics</h4>
<p style="margin: 0; color: #6c757d;">
Total: <strong>{total_time:.2f}s</strong> |
Search: <strong>{search_duration:.2f}s</strong> |
AI Analysis: <strong>{agent_duration:.2f}s</strong>
</p>
</div>
""", unsafe_allow_html=True)
except Exception as e:
st.markdown(f"""
<div class="status-error" style="text-align: center; margin: 2rem 0;">
An error occurred: {str(e)}
</div>
""", unsafe_allow_html=True)
if __name__ == "__main__":
main()

View file

@ -102,7 +102,7 @@ class FinanceAdvisorSystem:
self.budget_analysis_agent = LlmAgent(
name="BudgetAnalysisAgent",
model="gemini-2.0-flash-exp",
model="gemini-2.5-flash",
description="Analyzes financial data to categorize spending patterns and recommend budget improvements",
instruction="""You are a Budget Analysis Agent specialized in reviewing financial transactions and expenses.
You are the first agent in a sequence of three financial advisor agents.
@ -136,7 +136,7 @@ IMPORTANT: Store your analysis in state['budget_analysis'] for use by subsequent
self.savings_strategy_agent = LlmAgent(
name="SavingsStrategyAgent",
model="gemini-2.0-flash-exp",
model="gemini-2.5-flash",
description="Recommends optimal savings strategies based on income, expenses, and financial goals",
instruction="""You are a Savings Strategy Agent specialized in creating personalized savings plans.
You are the second agent in the sequence. READ the budget analysis from state['budget_analysis'] first.
@ -162,7 +162,7 @@ IMPORTANT: Store your strategy in state['savings_strategy'] for use by the Debt
self.debt_reduction_agent = LlmAgent(
name="DebtReductionAgent",
model="gemini-2.0-flash-exp",
model="gemini-2.5-flash",
description="Creates optimized debt payoff plans to minimize interest paid and time to debt freedom",
instruction="""You are a Debt Reduction Agent specialized in creating debt payoff strategies.
You are the final agent in the sequence. READ both state['budget_analysis'] and state['savings_strategy'] first.
@ -282,7 +282,7 @@ IMPORTANT: Store your final plan in state['debt_reduction'] and ensure it aligns
def _preprocess_manual_expenses(self, session):
manual_expenses = session.state.get("manual_expenses", {})
if not manual_expenses:
if not manual_expenses or manual_expenses is None:
return
session.state.update({
@ -294,6 +294,10 @@ IMPORTANT: Store your final plan in state['debt_reduction'] and ensure it aligns
monthly_income = financial_data.get("monthly_income", 0)
expenses = financial_data.get("manual_expenses", {})
# Ensure expenses is not None
if expenses is None:
expenses = {}
if not expenses and financial_data.get("transactions"):
expenses = {}
for transaction in financial_data["transactions"]:
@ -774,7 +778,7 @@ def main():
help=f"Enter your monthly {cat.lower()} expenses"
)
if any(manual_expenses.values()):
if manual_expenses and any(manual_expenses.values()):
st.markdown("#### 📊 Summary of Entered Expenses")
manual_df_disp = pd.DataFrame({
'Category': list(manual_expenses.keys()),
@ -883,7 +887,7 @@ def main():
if expense_option == "Upload CSV Transactions" and transactions_df is None:
st.error("Please upload a valid transaction CSV file or choose manual entry.")
return
if use_manual_expenses and not any(manual_expenses.values()):
if use_manual_expenses and (not manual_expenses or not any(manual_expenses.values())):
st.warning("No manual expenses entered. Analysis might be limited.")
st.header("Financial Analysis Results")

View file

@ -1,8 +1,8 @@
google-adk==0.1.0
streamlit
pandas==2.1.1
matplotlib==3.8.0
numpy==1.26.0
python-dotenv==1.0.0
plotly==5.18.0
asyncio==3.4.3
streamlit>=1.28.0
pandas>=2.0.0
matplotlib>=3.7.0
numpy==1.26.4
python-dotenv>=1.0.0
plotly>=5.15.0
asyncio>=3.4.3