From f32c9451569538a191c32e221678c6c0609d4b03 Mon Sep 17 00:00:00 2001 From: Madhu Date: Wed, 6 Aug 2025 03:59:39 +0530 Subject: [PATCH 1/2] NEW Multi Agent Demos: AI Realtor Agent Team changes --- .../ai_real_estate_agent_team/README.md | 237 ++--- .../ai_real_estate_agent_team.cpython-311.pyc | Bin 0 -> 39814 bytes .../ai_real_estate_agent_team.py | 836 ++++++++++++++++++ .../local_ai_real_estate_agent_team.py | 828 +++++++++++++++++ .../real_estate_agent_team.py | 777 ---------------- .../ai_financial_coach_agent.py | 16 +- .../ai_financial_coach_agent/requirements.txt | 14 +- 7 files changed, 1807 insertions(+), 901 deletions(-) create mode 100644 advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/__pycache__/ai_real_estate_agent_team.cpython-311.pyc create mode 100644 advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/ai_real_estate_agent_team.py create mode 100644 advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/local_ai_real_estate_agent_team.py delete mode 100644 advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/real_estate_agent_team.py diff --git a/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/README.md b/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/README.md index 06bd8e8..64075e0 100644 --- a/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/README.md +++ b/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/README.md @@ -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 diff --git a/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/__pycache__/ai_real_estate_agent_team.cpython-311.pyc b/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/__pycache__/ai_real_estate_agent_team.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6ea5bc905d2234e7fc4d96e255247b7001e59cb GIT binary patch literal 39814 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

- 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%0Ax+lfWk)oPN(Nv^zCyZ#6 z=RoDD9LI(MtRMe8hl9T0DTda@(2hka>w#89XjOz(m9u^-R>0+z%dWu>n(lSp?IYFP zioKECO4)VlL-)PQcVCqo4=UA%VmlMDYiW4KQu0|+s*+Qf?q<3$sex!{uy;?{mCIT` z@W}4NiuR`{aDcxeB#QvbwVy3R9r)Bpe;vW*_Y+G25Wxa!Z({k0WNL|h#*+M5Qu33tlI~^&lNyMI_Ia^hFwGBxvip$Y zKBT%2(I;v-1oeX1r?~d3uKluWzy4xeaZRYM3E4IAiB@!J7{?XY3DtE%cAd~M4k)gJ zs_UTaI;e*+p}0<~uG6yXH03cKqZTja@eEY!Qr{=1E!nN-(Hw}5FlI^g1g$?na~8eg zv!ou9+*{J!EcDbsG_=p52!}2?Cg(IM{2rCxBlCNxOX48Dgx#%A9ND)@-{ez6DCKf+{9O(u4uG^Mb0c{Tk@AX2ab={aJW_N3)S!&0K^aklvYhoX5Dm~4 z1FdQsv{-&TR2DQDH9`occZrA-E|$=AEa-=#22jokoxUd(Rd@R~z3&{ldq}aDQbBJF znv1%!`tHd{UVS8QKxTKO2q!1laC*rSkBK}IJ zY%fJSMg!^FoTopfqFt>(AJ*FutN@a}D5?hl)uWcSMDkiAMb(k|hP0ZAS>u=t9RofU zx#?XZu!M^h`4(8$G7`i^BQehyq4_L{=p+-MbTXBVN804S#v5-DC8a_9R6<34mYLHzG5gJS_jsc7D zQLgP*T*p+`G1+zOX*O+^aTKyqB!=dl_q3t;$BsX9JZw=Kj;akuV>Y~FA2eX+Q0xxg z-G~ZwH=+Vb|CoT2a~G};$M`%);Zxxl49+nvd%%ZK%qN9=+>zQ=Oll%ky9n+ZmFr)T z>$~Y5eM_$U|u$WeJc-<0p;H_s{&G_(Aatrfz*$`$Mh#78>Y| zLi{2ojhyXM?E6*we%ZdCW_Ve0-Z8~KpxOsy`@kpEe&0EK_b}ROgtm8B9Z=y1&hsj0pD+B>1UA=y=lT?V=z<`G*A98K6^ko+zYY{JFb&lXrt zs{@PD=K|+$N8^*Ss=Hl1ed`&H`snzNjSf3;K!RC zzicL)U$$Au-A>{BvZrgJ+xDvl!ueG*5A1-1WO0pmv(diakum3I_rPz z*!^;~^+~l2?x=lchE|j_GmV0jh5EyiH*Q#g@hPNrTA}9Ri}Ve+qE7FXrR7%0;$iTZ zrxyk61v^sRugj>J)#PZVNT;@Ew+R zAt>COKMQO!Z1S>LBAjE5{YAK9IetgD>^1h6Ww zOA92z)yiAlFq3C-t!4ic_Z#jtHy+CC?iAcDlj~q-_7$aUMlG8`>aotgbL{Sz47;!g zzN%Du)k?3-mBli%Ehn%d+Zt}nKVx4$Oa5MNwM@as){uPA7f|yAmh+OuLdQ{W|3d>m zP(CxGgfmWAER*nP`gtW|KEnd0MG`!3E3{x-HbB#i#e$tHN${BUHLC^0C<(%R1n*i8 fJ+hX`*0Kn1UmN@l;LGuvHI)AkH*_IT literal 0 HcmV?d00001 diff --git a/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/ai_real_estate_agent_team.py b/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/ai_real_estate_agent_team.py new file mode 100644 index 0000000..37439fd --- /dev/null +++ b/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/ai_real_estate_agent_team.py @@ -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""" +

+ """, + 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', unsafe_allow_html=True) + else: + st.markdown('
⚠️ Please select at least one website
', 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""" +
+ ⚠️ Please provide: {', '.join(missing_items)} +
+ """, 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""" +
+ ❌ Error initializing: {str(e)} +
+ """, 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""" +
+ ❌ An error occurred: {str(e)} +
+ """, unsafe_allow_html=True) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/local_ai_real_estate_agent_team.py b/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/local_ai_real_estate_agent_team.py new file mode 100644 index 0000000..aaff516 --- /dev/null +++ b/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/local_ai_real_estate_agent_team.py @@ -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""" + + """, + 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() \ No newline at end of file diff --git a/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/real_estate_agent_team.py b/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/real_estate_agent_team.py deleted file mode 100644 index cda7d44..0000000 --- a/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/real_estate_agent_team.py +++ /dev/null @@ -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(""" -
-

πŸ€– AI-Powered Real Estate Recommendations

-
- """, 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(""" -
-

πŸ”— Property Links

- """, unsafe_allow_html=True) - - for i, url in enumerate(set(urls), 1): - st.markdown(f""" - - """, unsafe_allow_html=True) - - st.markdown("
", 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(""" - - """, unsafe_allow_html=True) - - # Beautiful header - st.markdown(""" -
-

🏠 AI Real Estate Agent Team

-

Find Your Dream Home with Specialized AI Agents

-
- """, unsafe_allow_html=True) - - # Sidebar configuration - with st.sidebar: - st.markdown(""" -
-

βš™οΈ Configuration

-
- """, 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'
βœ… {len(selected_websites)} sources selected
', unsafe_allow_html=True) - else: - st.markdown('
⚠️ Please select at least one website
', unsafe_allow_html=True) - - # How it works - with st.expander("πŸ€– How It Works", expanded=False): - st.markdown(""" -
-

πŸ” Property Search Agent

-

Uses extract + Google Search fallback

-
-
-

πŸ“Š Market Analysis Agent

-

Analyzes trends and neighborhood insights

-
-
-

πŸ’° Property Valuation Agent

-

Evaluates values and investment potential

-
- """, unsafe_allow_html=True) - - # Main form - st.markdown(""" -
-

🏠 Your Property Requirements

- """, 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("
", 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""" -
- ⚠️ Please provide: {', '.join(missing_items)} -
- """, 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""" -
- ❌ Error initializing: {str(e)} -
- """, unsafe_allow_html=True) - return - - # Display progress - st.markdown(""" -
-

πŸš€ Property Analysis in Progress

-
-
πŸ”
-
AI Agents are working together to find your perfect home
-
-
- """, 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(""" -
-

πŸŽ‰ Analysis Complete!

- """, unsafe_allow_html=True) - display_property_results(result) - st.markdown("
", 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""" -
-

⏱️ Performance Metrics

-

- Total: {total_time:.2f}s | - Search: {search_duration:.2f}s | - AI Analysis: {agent_duration:.2f}s -

-
- """, unsafe_allow_html=True) - - except Exception as e: - st.markdown(f""" -
- ❌ An error occurred: {str(e)} -
- """, unsafe_allow_html=True) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/advanced_ai_agents/multi_agent_apps/ai_financial_coach_agent/ai_financial_coach_agent.py b/advanced_ai_agents/multi_agent_apps/ai_financial_coach_agent/ai_financial_coach_agent.py index 2e7c361..f76d5e9 100644 --- a/advanced_ai_agents/multi_agent_apps/ai_financial_coach_agent/ai_financial_coach_agent.py +++ b/advanced_ai_agents/multi_agent_apps/ai_financial_coach_agent/ai_financial_coach_agent.py @@ -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") diff --git a/advanced_ai_agents/multi_agent_apps/ai_financial_coach_agent/requirements.txt b/advanced_ai_agents/multi_agent_apps/ai_financial_coach_agent/requirements.txt index 2f6d591..5d3f815 100644 --- a/advanced_ai_agents/multi_agent_apps/ai_financial_coach_agent/requirements.txt +++ b/advanced_ai_agents/multi_agent_apps/ai_financial_coach_agent/requirements.txt @@ -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 From cbc9edfc66a28560fb8c6c50e18837a1f15c66ea Mon Sep 17 00:00:00 2001 From: Madhu Date: Wed, 6 Aug 2025 04:05:21 +0530 Subject: [PATCH 2/2] Removed pycache --- .../ai_real_estate_agent_team.cpython-311.pyc | Bin 39814 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/__pycache__/ai_real_estate_agent_team.cpython-311.pyc diff --git a/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/__pycache__/ai_real_estate_agent_team.cpython-311.pyc b/advanced_ai_agents/multi_agent_apps/agent_teams/ai_real_estate_agent_team/__pycache__/ai_real_estate_agent_team.cpython-311.pyc deleted file mode 100644 index c6ea5bc905d2234e7fc4d96e255247b7001e59cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39814 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

- 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%0Ax+lfWk)oPN(Nv^zCyZ#6 z=RoDD9LI(MtRMe8hl9T0DTda@(2hka>w#89XjOz(m9u^-R>0+z%dWu>n(lSp?IYFP zioKECO4)VlL-)PQcVCqo4=UA%VmlMDYiW4KQu0|+s*+Qf?q<3$sex!{uy;?{mCIT` z@W}4NiuR`{aDcxeB#QvbwVy3R9r)Bpe;vW*_Y+G25Wxa!Z({k0WNL|h#*+M5Qu33tlI~^&lNyMI_Ia^hFwGBxvip$Y zKBT%2(I;v-1oeX1r?~d3uKluWzy4xeaZRYM3E4IAiB@!J7{?XY3DtE%cAd~M4k)gJ zs_UTaI;e*+p}0<~uG6yXH03cKqZTja@eEY!Qr{=1E!nN-(Hw}5FlI^g1g$?na~8eg zv!ou9+*{J!EcDbsG_=p52!}2?Cg(IM{2rCxBlCNxOX48Dgx#%A9ND)@-{ez6DCKf+{9O(u4uG^Mb0c{Tk@AX2ab={aJW_N3)S!&0K^aklvYhoX5Dm~4 z1FdQsv{-&TR2DQDH9`occZrA-E|$=AEa-=#22jokoxUd(Rd@R~z3&{ldq}aDQbBJF znv1%!`tHd{UVS8QKxTKO2q!1laC*rSkBK}IJ zY%fJSMg!^FoTopfqFt>(AJ*FutN@a}D5?hl)uWcSMDkiAMb(k|hP0ZAS>u=t9RofU zx#?XZu!M^h`4(8$G7`i^BQehyq4_L{=p+-MbTXBVN804S#v5-DC8a_9R6<34mYLHzG5gJS_jsc7D zQLgP*T*p+`G1+zOX*O+^aTKyqB!=dl_q3t;$BsX9JZw=Kj;akuV>Y~FA2eX+Q0xxg z-G~ZwH=+Vb|CoT2a~G};$M`%);Zxxl49+nvd%%ZK%qN9=+>zQ=Oll%ky9n+ZmFr)T z>$~Y5eM_$U|u$WeJc-<0p;H_s{&G_(Aatrfz*$`$Mh#78>Y| zLi{2ojhyXM?E6*we%ZdCW_Ve0-Z8~KpxOsy`@kpEe&0EK_b}ROgtm8B9Z=y1&hsj0pD+B>1UA=y=lT?V=z<`G*A98K6^ko+zYY{JFb&lXrt zs{@PD=K|+$N8^*Ss=Hl1ed`&H`snzNjSf3;K!RC zzicL)U$$Au-A>{BvZrgJ+xDvl!ueG*5A1-1WO0pmv(diakum3I_rPz z*!^;~^+~l2?x=lchE|j_GmV0jh5EyiH*Q#g@hPNrTA}9Ri}Ve+qE7FXrR7%0;$iTZ zrxyk61v^sRugj>J)#PZVNT;@Ew+R zAt>COKMQO!Z1S>LBAjE5{YAK9IetgD>^1h6Ww zOA92z)yiAlFq3C-t!4ic_Z#jtHy+CC?iAcDlj~q-_7$aUMlG8`>aotgbL{Sz47;!g zzN%Du)k?3-mBli%Ehn%d+Zt}nKVx4$Oa5MNwM@as){uPA7f|yAmh+OuLdQ{W|3d>m zP(CxGgfmWAER*nP`gtW|KEnd0MG`!3E3{x-HbB#i#e$tHN${BUHLC^0C<(%R1n*i8 fJ+hX`*0Kn1UmN@l;LGuvHI)AkH*_IT