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/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
diff --git a/advanced_llm_apps/gpt_oss_critique_improvement_loop/README.md b/advanced_llm_apps/gpt_oss_critique_improvement_loop/README.md
new file mode 100644
index 0000000..a9c5fcb
--- /dev/null
+++ b/advanced_llm_apps/gpt_oss_critique_improvement_loop/README.md
@@ -0,0 +1,83 @@
+# π GPT-OSS Advanced Critique & Improvement Loop
+
+A Streamlit app demonstrating the "Automatic Critique + Improvement Loop" pattern using GPT-OSS via Groq.
+
+## π― What It Does
+
+This demo implements an iterative quality improvement process:
+
+1. **Generate Initial Answer** - Uses Pro Mode (parallel candidates + synthesis)
+2. **Critique Phase** - AI critic identifies flaws, missing information, unclear explanations
+3. **Revision Phase** - AI revises the answer addressing all critiques
+4. **Repeat** - Continue for 1-3 iterations for maximum quality
+
+## π Key Features
+
+- **Iterative Improvement** - Each round makes the answer better
+- **Transparent Process** - See critiques and revisions at each step
+- **Configurable Iterations** - Choose 1-3 improvement rounds
+- **Paper Trail** - Track why decisions were made
+- **Cost Effective** - Uses GPT-OSS instead of expensive models
+
+## π οΈ Installation & Usage
+
+```bash
+cd critique_improvement_streamlit_demo
+pip install -r requirements.txt
+export GROQ_API_KEY=your_key_here
+streamlit run streamlit_app.py
+```
+
+## π How It Works
+
+### Step 1: Initial Answer Generation
+- Generates 3 parallel candidates with high temperature (0.9)
+- Synthesizes them into one coherent answer with low temperature (0.2)
+
+### Step 2: Critique Phase
+- AI critic analyzes the answer for:
+ - Missing information
+ - Unclear explanations
+ - Logical flaws
+ - Areas needing improvement
+
+### Step 3: Revision Phase
+- AI revises the answer addressing every critique point
+- Maintains good parts while fixing issues
+
+### Step 4: Repeat
+- Continues for specified number of iterations
+- Each round typically improves quality significantly
+
+## π― Use Cases
+
+- **Technical Documentation** - Ensure completeness and clarity
+- **Educational Content** - Catch gaps in explanations
+- **Business Proposals** - Identify missing elements
+- **Code Reviews** - Find potential issues and improvements
+- **Research Papers** - Ensure thoroughness and accuracy
+
+## π‘ Benefits
+
+- **Higher Quality** - Often beats single-shot generation
+- **Error Detection** - Catches issues humans might miss
+- **Completeness** - Ensures all aspects are covered
+- **Transparency** - See the improvement process
+- **Cost Effective** - Better results than expensive models
+
+## π§ Technical Details
+
+- **Model**: GPT-OSS 120B via Groq
+- **Token Limit**: 1024 per completion (optimized for Groq limits)
+- **Parallel Processing**: 3 candidates for initial generation
+- **Temperature Control**: High for diversity, low for synthesis/improvement
+
+## π Expected Results
+
+Typically see:
+- **20-40% improvement** in answer quality
+- **Better completeness** and accuracy
+- **Clearer explanations** and structure
+- **Fewer logical gaps** or missing information
+
+The improvement is most noticeable on complex topics where initial answers might miss important details or have unclear explanations.
\ No newline at end of file
diff --git a/advanced_llm_apps/gpt_oss_critique_improvement_loop/requirements.txt b/advanced_llm_apps/gpt_oss_critique_improvement_loop/requirements.txt
new file mode 100644
index 0000000..9aefa54
--- /dev/null
+++ b/advanced_llm_apps/gpt_oss_critique_improvement_loop/requirements.txt
@@ -0,0 +1,2 @@
+streamlit>=1.32.0
+groq>=0.5.0
\ No newline at end of file
diff --git a/advanced_llm_apps/gpt_oss_critique_improvement_loop/streamlit_app.py b/advanced_llm_apps/gpt_oss_critique_improvement_loop/streamlit_app.py
new file mode 100644
index 0000000..c376963
--- /dev/null
+++ b/advanced_llm_apps/gpt_oss_critique_improvement_loop/streamlit_app.py
@@ -0,0 +1,228 @@
+"""Streamlit Critique & Improvement Loop Demo using GPT-OSS via Groq
+
+This implements the "Automatic Critique + Improvement Loop" pattern:
+1. Generate initial answer (Pro Mode style)
+2. Have a critic model identify flaws/missing pieces
+3. Revise the answer addressing all critiques
+4. Repeat if needed
+
+Run with:
+ streamlit run streamlit_app.py
+"""
+
+import os
+import time
+import concurrent.futures as cf
+from typing import List, Dict, Any
+
+import streamlit as st
+from groq import Groq, GroqError
+
+MODEL = "openai/gpt-oss-120b"
+MAX_COMPLETION_TOKENS = 1024 # stay within Groq limits
+
+SAMPLE_PROMPTS = [
+ "Explain how to implement a binary search tree in Python.",
+ "What are the best practices for API design?",
+ "How would you optimize a slow database query?",
+ "Explain the concept of recursion with examples.",
+]
+
+# --- Helper functions --------------------------------------------------------
+
+def _one_completion(client: Groq, messages: List[Dict[str, str]], temperature: float) -> str:
+ """Single non-streaming completion with basic retries."""
+ delay = 0.5
+ for attempt in range(3):
+ try:
+ resp = client.chat.completions.create(
+ model=MODEL,
+ messages=messages,
+ temperature=temperature,
+ max_completion_tokens=MAX_COMPLETION_TOKENS,
+ top_p=1,
+ stream=False,
+ )
+ return resp.choices[0].message.content
+ except GroqError:
+ if attempt == 2:
+ raise
+ time.sleep(delay)
+ delay *= 2
+
+
+def generate_initial_answer(client: Groq, prompt: str) -> str:
+ """Generate initial answer using parallel candidates + synthesis (Pro Mode)."""
+ # Generate 3 candidates in parallel
+ candidates = []
+ with cf.ThreadPoolExecutor(max_workers=3) as ex:
+ futures = [
+ ex.submit(_one_completion, client,
+ [{"role": "user", "content": prompt}], 0.9)
+ for _ in range(3)
+ ]
+ for fut in cf.as_completed(futures):
+ candidates.append(fut.result())
+
+ # Synthesize candidates
+ candidate_texts = []
+ for i, c in enumerate(candidates):
+ candidate_texts.append(f"--- Candidate {i+1} ---\n{c}")
+
+ synthesis_prompt = (
+ f"You are given 3 candidate answers. Synthesize them into ONE best answer, "
+ f"eliminating repetition and ensuring coherence:\n\n"
+ f"{chr(10).join(candidate_texts)}\n\n"
+ f"Return the single best final answer."
+ )
+
+ return _one_completion(client, [{"role": "user", "content": synthesis_prompt}], 0.2)
+
+
+def critique_answer(client: Groq, prompt: str, answer: str) -> str:
+ """Have a critic model identify flaws and missing pieces."""
+ critique_prompt = (
+ f"Original question: {prompt}\n\n"
+ f"Answer to critique:\n{answer}\n\n"
+ f"Act as a critical reviewer. List specific flaws, missing information, "
+ f"unclear explanations, or areas that need improvement. Be constructive but thorough. "
+ f"Format as a bulleted list starting with 'β’'."
+ )
+
+ return _one_completion(client, [{"role": "user", "content": critique_prompt}], 0.3)
+
+
+def revise_answer(client: Groq, prompt: str, original_answer: str, critiques: str) -> str:
+ """Revise the original answer addressing all critiques."""
+ revision_prompt = (
+ f"Original question: {prompt}\n\n"
+ f"Original answer:\n{original_answer}\n\n"
+ f"Critiques to address:\n{critiques}\n\n"
+ f"Revise the original answer to address every critique point. "
+ f"Maintain the good parts, fix the issues, and add missing information. "
+ f"Return the improved answer."
+ )
+
+ return _one_completion(client, [{"role": "user", "content": revision_prompt}], 0.2)
+
+
+def critique_improvement_loop(prompt: str, max_iterations: int = 2, groq_api_key: str | None = None) -> Dict[str, Any]:
+ """Main function implementing the critique and improvement loop."""
+ client = Groq(api_key=groq_api_key) if groq_api_key else Groq()
+
+ results = {
+ "iterations": [],
+ "final_answer": "",
+ "total_iterations": 0
+ }
+
+ # Generate initial answer
+ with st.spinner("Generating initial answer..."):
+ initial_answer = generate_initial_answer(client, prompt)
+ results["iterations"].append({
+ "type": "initial",
+ "answer": initial_answer,
+ "critiques": None
+ })
+
+ current_answer = initial_answer
+
+ # Improvement loop
+ for iteration in range(max_iterations):
+ with st.spinner(f"Critiquing iteration {iteration + 1}..."):
+ critiques = critique_answer(client, prompt, current_answer)
+
+ with st.spinner(f"Revising iteration {iteration + 1}..."):
+ revised_answer = revise_answer(client, prompt, current_answer, critiques)
+
+ results["iterations"].append({
+ "type": "improvement",
+ "answer": revised_answer,
+ "critiques": critiques
+ })
+
+ current_answer = revised_answer
+
+ results["final_answer"] = current_answer
+ results["total_iterations"] = len(results["iterations"])
+
+ return results
+
+
+# --- Streamlit UI ------------------------------------------------------------
+
+st.set_page_config(page_title="Critique & Improvement Loop", page_icon="π", layout="wide")
+st.title("π Critique & Improvement Loop")
+
+st.markdown(
+ "Generate high-quality answers through iterative critique and improvement using GPT-OSS."
+)
+
+with st.sidebar:
+ st.header("Settings")
+ api_key = st.text_input("Groq API Key", value=os.getenv("GROQ_API_KEY", ""), type="password")
+ max_iterations = st.slider("Max Improvement Iterations", 1, 3, 2)
+ st.markdown("---")
+ st.caption("Each iteration adds critique + revision steps for higher quality.")
+
+# Initialize prompt in session state if not present
+if "prompt" not in st.session_state:
+ st.session_state["prompt"] = ""
+
+def random_prompt_callback():
+ import random
+ st.session_state["prompt"] = random.choice(SAMPLE_PROMPTS)
+
+prompt = st.text_area("Your prompt", height=150, placeholder="Ask me anythingβ¦", key="prompt")
+
+col1, col2 = st.columns([1, 1])
+with col1:
+ st.button("π Random Sample Prompt", on_click=random_prompt_callback)
+with col2:
+ generate_clicked = st.button("π Start Critique Loop")
+
+if generate_clicked:
+ if not prompt.strip():
+ st.error("Please enter a prompt.")
+ st.stop()
+
+ try:
+ results = critique_improvement_loop(prompt, max_iterations, groq_api_key=api_key or None)
+ except Exception as e:
+ st.exception(e)
+ st.stop()
+
+ # Display results
+ st.subheader("π― Final Answer")
+ st.write(results["final_answer"])
+
+ # Show improvement history
+ with st.expander(f"π Show Improvement History ({results['total_iterations']} iterations)"):
+ for i, iteration in enumerate(results["iterations"]):
+ if iteration["type"] == "initial":
+ st.markdown(f"### π Initial Answer")
+ st.write(iteration["answer"])
+ else:
+ st.markdown(f"### π Iteration {i}")
+
+ # Show critiques
+ if iteration["critiques"]:
+ st.markdown("**Critiques:**")
+ st.write(iteration["critiques"])
+
+ # Show improved answer
+ st.markdown("**Improved Answer:**")
+ st.write(iteration["answer"])
+
+ if i < len(results["iterations"]) - 1:
+ st.markdown("---")
+
+ # Summary metrics
+ st.markdown("---")
+ col1, col2, col3 = st.columns(3)
+ with col1:
+ st.metric("Total Iterations", results["total_iterations"])
+ with col2:
+ st.metric("Improvement Rounds", max_iterations)
+ with col3:
+ st.metric("Final Answer Length", len(results["final_answer"]))
\ No newline at end of file
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/6_callbacks/6_3_tool_execution_callbacks/__pycache__/agent.cpython-311.pyc b/ai_agent_framework_crash_course/google_adk_crash_course/6_callbacks/6_3_tool_execution_callbacks/__pycache__/agent.cpython-311.pyc
deleted file mode 100644
index 6f7cecb..0000000
Binary files a/ai_agent_framework_crash_course/google_adk_crash_course/6_callbacks/6_3_tool_execution_callbacks/__pycache__/agent.cpython-311.pyc and /dev/null differ
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/.env.example b/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/.env.example
new file mode 100644
index 0000000..f5cfcfb
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/.env.example
@@ -0,0 +1,3 @@
+# If using Gemini via Google AI Studio
+GOOGLE_GENAI_USE_VERTEXAI=False
+GOOGLE_API_KEY="your-api-key"
\ No newline at end of file
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/README.md b/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/README.md
new file mode 100644
index 0000000..1dd06f4
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/README.md
@@ -0,0 +1,200 @@
+# π Tutorial 7: Plugins
+
+## π― What You'll Learn
+- **Plugin Fundamentals**: Understanding what plugins are and how they work
+- **Global Callback Management**: Using plugins for cross-cutting concerns
+- **Practical Plugin Examples**: Logging, monitoring, and request modification
+
+## π‘ Core Concept: Plugins
+
+Plugins in Google ADK are custom code modules that can be executed at various stages of an agent workflow lifecycle using callback hooks. Unlike regular callbacks that are configured on individual agents or tools, plugins are registered once on the `Runner` and apply globally to every agent, tool, and LLM call managed by that runner.
+
+### **Plugin vs Callback Comparison**
+```
+βββββββββββββββββββ βββββββββββββββββββ
+β Regular β β Plugin β
+β Callbacks β β Callbacks β
+βββββββββββββββββββ€ βββββββββββββββββββ€
+β β’ Per agent β β β’ Global scope β
+β β’ Per tool β β β’ Runner level β
+β β’ Specific task β β β’ Cross-cutting β
+β β’ Local effect β β β’ Reusable β
+βββββββββββββββββββ βββββββββββββββββββ
+```
+
+### **Plugin Lifecycle Hooks**
+```
+βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
+β User Message βββββΆβ Runner Start βββββΆβ Agent Execute β
+β Callback β β Callback β β Callback β
+βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
+ β β β
+ βΌ βΌ βΌ
+βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
+β Model Callback β β Tool Callback β β Event Callback β
+β (before/after)β β (before/after)β β (modify event)β
+βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
+ β β β
+ βΌ βΌ βΌ
+βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
+β Error Handling β β Runner End β β Cleanup Tasks β
+β Callback β β Callback β β & Reporting β
+βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
+```
+
+### **Why Use Plugins?**
+- **Cross-cutting Concerns**: Implement functionality that applies across your entire application
+- **Reusability**: Package related callback functions together for reuse
+- **Global Monitoring**: Track all agent, tool, and model interactions
+- **Policy Enforcement**: Implement security guardrails and access controls
+- **Logging & Metrics**: Centralized logging and performance monitoring
+- **Request/Response Modification**: Dynamically modify inputs and outputs
+
+## π Tutorial Overview
+
+This tutorial demonstrates how to create and use plugins in Google ADK through a practical example that combines multiple use cases:
+
+### **Demo Plugin Features**
+1. **Request Logging**: Log all user messages with timestamps
+2. **Request Modification**: Add timestamp context to user messages
+3. **Agent Tracking**: Count and monitor agent executions
+4. **Tool Monitoring**: Track tool usage and handle errors
+5. **Final Reporting**: Generate summary of plugin activity
+
+## π Project Structure
+
+```
+7_plugins/
+βββ README.md # This file - concept explanation
+βββ agent.py # Agent implementation with plugin
+βββ app.py # Streamlit interface
+βββ requirements.txt # Dependencies
+βββ plugin_example.py # Standalone plugin demonstration
+```
+
+## π― Learning Objectives
+
+By the end of this tutorial, you'll understand:
+
+- β
**Plugin Architecture**: How plugins extend the BasePlugin class
+- β
**Global Scope**: How plugins apply across all agents and tools
+- β
**Callback Hooks**: Available plugin callback methods and their timing
+- β
**Practical Applications**: Real-world use cases for plugins
+- β
**Error Handling**: How to implement graceful error recovery
+- β
**Simple Implementation**: Easy-to-understand plugin structure
+
+## π Getting Started
+
+### **Prerequisites**
+- Python 3.11+
+- Google AI Studio API key
+- Basic understanding of Google ADK (Tutorials 1-6)
+
+### **Setup**
+1. **Get API Key**: Visit [Google AI Studio](https://aistudio.google.com/)
+2. **Create .env file**: Create a file named `.env` in this directory with:
+ ```
+ GOOGLE_API_KEY=your_google_ai_studio_api_key_here
+ ```
+3. **Install dependencies**: `pip install -r requirements.txt`
+
+**Important**:
+- Make sure your `.env` file is in the same directory as the tutorial files
+- Replace `your_google_ai_studio_api_key_here` with your actual API key
+- The `.env` file should not be committed to version control
+
+### **Run Tutorial**
+```bash
+cd 7_plugins
+streamlit run app.py
+```
+
+## π§ Key Concepts
+
+### **Plugin Class Structure**
+```python
+from google.adk.plugins.base_plugin import BasePlugin
+
+class MyPlugin(BasePlugin):
+ def __init__(self):
+ super().__init__(name="my_plugin")
+ # Initialize plugin state
+
+ async def before_agent_callback(self, *, agent, callback_context):
+ # Called before each agent execution
+ pass
+
+ async def after_model_callback(self, *, callback_context, llm_response):
+ # Called after each model call
+ pass
+```
+
+### **Plugin Registration**
+```python
+from google.adk.runners import InMemoryRunner
+
+runner = InMemoryRunner(
+ agent=my_agent,
+ app_name="my_app",
+ plugins=[MyPlugin()] # Register plugins here
+)
+```
+
+### **Available Callback Hooks**
+- `on_user_message_callback`: Modify user input
+- `before_run_callback`: Setup before execution
+- `before_agent_callback` / `after_agent_callback`: Agent lifecycle
+- `before_model_callback` / `after_model_callback`: Model interactions
+- `before_tool_callback` / `after_tool_callback`: Tool execution
+- `on_model_error_callback` / `on_tool_error_callback`: Error handling
+- `on_event_callback`: Modify events before streaming
+- `after_run_callback`: Cleanup after execution
+
+## π― Use Cases
+
+### **Common Plugin Applications**
+1. **Logging & Tracing**: Detailed logs for debugging and analysis
+2. **Policy Enforcement**: Security guardrails and access controls
+3. **Monitoring & Metrics**: Performance tracking and analytics
+4. **Response Caching**: Cache expensive operations
+5. **Request/Response Modification**: Add context or standardize outputs
+6. **Error Recovery**: Graceful handling of failures
+7. **Usage Analytics**: Track patterns and usage statistics
+
+## π¨ Important Notes
+
+- **Plugin Precedence**: Plugin callbacks run **before** agent-level callbacks
+- **Global Scope**: Plugins affect **all** agents, tools, and models in the runner
+- **Web Interface**: Plugins are **not supported** by the ADK web interface
+- **Error Handling**: Plugin error callbacks can suppress exceptions and provide fallbacks
+
+## π Next Steps
+
+After completing this tutorial, you can:
+- Create custom plugins for your specific use cases
+- Implement monitoring and analytics plugins
+- Build security and policy enforcement plugins
+- Explore advanced plugin patterns and combinations
+
+## π§ Troubleshooting
+
+### **Common Issues**
+
+**"Missing key inputs argument" Error**
+- Ensure you have created a `.env` file with your Google AI Studio API key
+- Verify the API key is valid and has the necessary permissions
+- Check that the `.env` file is in the same directory as the tutorial files
+
+**Import Errors**
+- Make sure you have installed all dependencies: `pip install -r requirements.txt`
+- Verify you're using Python 3.11 or higher
+
+**Plugin Not Working**
+- Remember that plugins are not supported by the ADK web interface
+- Ensure you're running the agent through the Streamlit app or Python script
+
+## π Additional Resources
+
+- [Google ADK Plugins Documentation](https://google.github.io/adk-docs/plugins/)
+- [Plugin Callback Hooks](https://google.github.io/adk-docs/plugins/#plugin-callback-hooks)
+- [BasePlugin API Reference](https://google.github.io/adk-docs/api/python/google.adk.plugins.base_plugin.BasePlugin)
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/agent.py b/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/agent.py
new file mode 100644
index 0000000..04d45d9
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/agent.py
@@ -0,0 +1,105 @@
+import asyncio
+from datetime import datetime
+from typing import Optional, Dict, Any
+from google.adk.agents import LlmAgent
+from google.adk.agents.base_agent import BaseAgent
+from google.adk.agents.callback_context import CallbackContext
+from google.adk.plugins.base_plugin import BasePlugin
+from google.adk.runners import InMemoryRunner
+from google.adk.tools.base_tool import BaseTool
+from google.adk.tools.tool_context import ToolContext
+from google.genai import types
+from dotenv import load_dotenv
+
+# Load environment variables (API key)
+load_dotenv()
+
+# ============================================================================
+# PLUGIN DEFINITION
+# ============================================================================
+# Plugins extend BasePlugin and provide global callbacks across all agents/tools
+class SimplePlugin(BasePlugin):
+ def __init__(self) -> None:
+ super().__init__(name="simple_plugin")
+ # Track usage statistics across all executions
+ self.agent_count = 0
+ self.tool_count = 0
+
+ # Called when user sends a message - can modify the input
+ async def on_user_message_callback(self, *, invocation_context, user_message: types.Content) -> Optional[types.Content]:
+ timestamp = datetime.now().strftime("%H:%M:%S")
+ print(f"π [Plugin] User message at {timestamp}")
+ # Add timestamp to each message part for context
+ modified_parts = [types.Part(text=f"[{timestamp}] {part.text}") for part in user_message.parts if hasattr(part, 'text')]
+ return types.Content(role='user', parts=modified_parts)
+
+ # Called before each agent execution - good for logging and setup
+ async def before_agent_callback(self, *, agent: BaseAgent, callback_context: CallbackContext) -> None:
+ self.agent_count += 1
+ print(f"π€ [Plugin] Agent {agent.name} starting (count: {self.agent_count})")
+
+ # Called before each tool execution - track tool usage
+ async def before_tool_callback(self, *, tool: BaseTool, tool_args: Dict[str, Any], tool_context: ToolContext) -> None:
+ self.tool_count += 1
+ print(f"π§ [Plugin] Tool {tool.name} starting (count: {self.tool_count})")
+
+ # Called after the entire run completes - generate final report
+ async def after_run_callback(self, *, invocation_context) -> None:
+ print(f"π [Plugin] Final Report: {self.agent_count} agents, {self.tool_count} tools")
+
+# ============================================================================
+# TOOL DEFINITION
+# ============================================================================
+# This tool can fail (division by zero) to demonstrate error handling
+async def calculator_tool(tool_context: ToolContext, operation: str, a: float, b: float) -> Dict[str, Any]:
+ print(f"π§ [Tool] Calculator: {operation}({a}, {b})")
+ if operation == "divide" and b == 0:
+ raise ValueError("Division by zero is not allowed")
+ # Dictionary of operations for cleaner code
+ ops = {"add": lambda x, y: x + y, "subtract": lambda x, y: x - y, "multiply": lambda x, y: x * y, "divide": lambda x, y: x / y}
+ if operation not in ops:
+ raise ValueError(f"Unknown operation: {operation}")
+ return {"operation": operation, "a": a, "b": b, "result": ops[operation](a, b)}
+
+# ============================================================================
+# AGENT AND RUNNER SETUP
+# ============================================================================
+# Create agent with the calculator tool
+agent = LlmAgent(name="plugin_demo_agent", model="gemini-2.0-flash",
+ instruction="You are a helpful assistant that can perform calculations. Use the calculator_tool when needed.",
+ tools=[calculator_tool])
+
+# Create runner and register the plugin - this makes the plugin global
+runner = InMemoryRunner(agent=agent, app_name="plugin_demo_app", plugins=[SimplePlugin()])
+
+# ============================================================================
+# AGENT EXECUTION FUNCTION
+# ============================================================================
+async def run_agent(message: str) -> str:
+ # Session management for conversation state
+ user_id, session_id = "demo_user", "demo_session"
+ session_service = runner.session_service
+
+ # Get or create session (required for ADK)
+ session = await session_service.get_session(app_name="plugin_demo_app", user_id=user_id, session_id=session_id)
+ if not session:
+ session = await session_service.create_session(app_name="plugin_demo_app", user_id=user_id, session_id=session_id)
+
+ # Create user message content
+ user_content = types.Content(role='user', parts=[types.Part(text=message)])
+
+ # Run agent and collect response - plugin callbacks will fire automatically
+ response_text = ""
+ async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=user_content):
+ if event.content and event.content.parts:
+ for part in event.content.parts:
+ if hasattr(part, 'text') and part.text:
+ response_text += part.text
+ return response_text if response_text else "No response received from agent."
+
+# ============================================================================
+# MAIN EXECUTION
+# ============================================================================
+if __name__ == "__main__":
+ # Test the plugin functionality
+ asyncio.run(run_agent("what is 2 + 2?"))
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/app.py b/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/app.py
new file mode 100644
index 0000000..defbe82
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/app.py
@@ -0,0 +1,64 @@
+import streamlit as st
+import asyncio
+import sys
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).parent))
+from agent import run_agent
+
+st.set_page_config(page_title="Google ADK Plugins Tutorial", page_icon="π")
+
+st.title("π Google ADK Plugins Tutorial")
+st.markdown("Demonstrates plugins for cross-cutting concerns like logging and monitoring.")
+
+test_scenarios = {
+ "Normal Conversation": "Hello! How are you?",
+ "Simple Calculation": "Calculate 15 + 27",
+ "Error Handling": "What is 10 divided by 0?"
+}
+
+selected_scenario = st.selectbox("Choose a test scenario:", list(test_scenarios.keys()))
+
+if st.button("π Run Test"):
+ with st.spinner("Running..."):
+ try:
+ response = asyncio.run(run_agent(test_scenarios[selected_scenario]))
+ st.success("**Agent Response:**")
+ st.write(response)
+ except Exception as e:
+ st.error(f"Error: {str(e)}")
+
+st.markdown("---")
+custom_message = st.text_area("Or enter your own message:", placeholder="Type here...")
+
+if st.button("π Run Custom Message"):
+ if custom_message.strip():
+ with st.spinner("Processing..."):
+ try:
+ response = asyncio.run(run_agent(custom_message))
+ st.success("**Agent Response:**")
+ st.write(response)
+ except Exception as e:
+ st.error(f"Error: {str(e)}")
+ else:
+ st.warning("Please enter a message.")
+
+with st.expander("π About Plugins"):
+ st.markdown("""
+ **Plugins** are custom code modules that execute at various stages of agent workflow lifecycle.
+
+ **Key Features:**
+ - π Request logging and modification
+ - π€ Agent execution tracking
+ - π§ Tool usage monitoring
+ - π Final reporting and analytics
+
+ **Plugin Callbacks:**
+ - `on_user_message_callback()` - Modify user input
+ - `before_agent_callback()` - Track agent starts
+ - `before_tool_callback()` - Track tool usage
+ - `after_run_callback()` - Generate reports
+ """)
+
+st.markdown("---")
+st.markdown("*Part of the Google ADK Crash Course*")
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/requirements.txt b/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/requirements.txt
new file mode 100644
index 0000000..4d30dae
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/7_plugins/requirements.txt
@@ -0,0 +1,4 @@
+google-genai>=1.28.0
+google-adk>=1.9.0
+streamlit>=1.47.1
+python-dotenv>=1.1.1
\ No newline at end of file
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/README.md b/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/README.md
new file mode 100644
index 0000000..0d98128
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/README.md
@@ -0,0 +1,128 @@
+# π― Tutorial 8: Simple MultiβAgent Researcher (Runs with ADK)
+
+## π― What You'll Learn
+- **Multiβagent orchestration** using a coordinator agent with specialized subβagents
+- **Sequential workflow** where agents build upon each other's outputs
+- **Web search integration** for real-time research capabilities
+- **Running with ADK Web** to interactively test the multiβagent system
+
+## π§ Core Concept: Multi-Agent Research Pipeline
+A coordinator `LlmAgent` orchestrates three specialized agents in a sequential workflow: Research β Summarize β Critique. Each agent contributes to building a comprehensive research report.
+
+```
+User Query β Coordinator Agent
+ β
+ ββββΆ Research Agent (web search + analysis)
+ β β
+ β ββββΆ Research Findings
+ β
+ ββββΆ Summarizer Agent (synthesis)
+ β β
+ β ββββΆ Key Insights
+ β
+ ββββΆ Critic Agent (quality analysis)
+ β
+ ββββΆ Final Report with Recommendations
+```
+
+## π Project Structure
+```
+8_simple_multi_agent/
+βββ README.md # This file
+βββ requirements.txt # Dependencies
+βββ multi_agent_researcher/ # Main implementation
+β βββ agent.py # Multi-agent system (exports root_agent)
+βββ .env # Environment variables (create this)
+```
+
+## π Getting Started
+
+### 1. Install Dependencies
+Navigate to the `8_simple_multi_agent` folder and install the required libraries:
+```bash
+cd 8_simple_multi_agent
+pip install -r requirements.txt
+```
+
+### 2. Set Up Environment
+Create a `.env` file in the `8_simple_multi_agent` folder:
+```bash
+# Create .env file
+echo "GOOGLE_API_KEY=your_ai_studio_key_here" > .env
+```
+
+**Important**: Replace `your_ai_studio_key_here` with your actual Google AI Studio API key from [https://aistudio.google.com/](https://aistudio.google.com/)
+
+### 3. Run with ADK Web (Recommended)
+From the `8_simple_multi_agent` folder:
+```bash
+adk web
+```
+
+**ADK Web Setup:**
+- Open the local URL printed in the terminal
+- In the import section, use this path:
+ ```
+ ai_agent_framework_crash_course.google_adk_crash_course.8_simple_multi_agent.multi_agent_researcher
+ ```
+- Select the `root_agent` object
+- Start chatting with your multi-agent researcher!
+
+## π§ͺ Sample Prompts to Try
+
+### **Comprehensive Research Query:**
+```
+Research the future of renewable energy integration in smart cities, including current technologies, implementation challenges, economic feasibility, and policy requirements. Provide a critique and suggestions.
+```
+
+### **Other Test Queries:**
+```
+"Research the current state of AI regulation in the European Union and its impact on business innovation"
+```
+
+```
+"Investigate the latest developments in CRISPR gene editing technology and its potential applications in medicine"
+```
+
+```
+"Research the effectiveness of personalized learning platforms in K-12 education, including current implementations and learning outcomes"
+```
+
+## π How It Works
+
+### **Research Agent:**
+- Conducts comprehensive web research using Google Search
+- Gathers current information, trends, and developments
+- Provides structured findings with sources and outlines
+
+### **Summarizer Agent:**
+- Synthesizes research into clear, actionable insights
+- Creates executive summaries and key bullet points
+- Identifies critical patterns and takeaways
+
+### **Critic Agent:**
+- Performs quality analysis and gap identification
+- Provides risk assessment and opportunity analysis
+- Gives actionable recommendations and next steps
+
+### **Coordinator:**
+- Orchestrates the entire research workflow
+- Ensures proper sequence: Research β Summarize β Critique
+- Integrates all outputs into a cohesive final report
+
+## π Tips for Best Results
+- **Be specific** in your research queries for better agent coordination
+- **Allow completion** of the full workflow for comprehensive results
+- The system automatically follows the research pipeline for thorough analysis
+- Each agent builds upon the previous agent's work for better insights
+
+## π Next Steps
+After mastering this tutorial, explore:
+- **Tutorial 9**: Workflow Agents (Sequential, Parallel, Branching)
+- **Advanced Patterns**: Custom tools and agent communication
+- **Integration**: Connect with external data sources and APIs
+
+## π¨ Troubleshooting
+- **API Key Issues**: Ensure your `.env` file is in the correct location and contains a valid `GOOGLE_API_KEY`
+- **Import Errors**: Make sure you're using the exact import path shown above
+- **Agent Not Found**: Verify that `root_agent` is properly exported from the module
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/multi_agent_researcher/.env.example b/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/multi_agent_researcher/.env.example
new file mode 100644
index 0000000..f5cfcfb
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/multi_agent_researcher/.env.example
@@ -0,0 +1,3 @@
+# If using Gemini via Google AI Studio
+GOOGLE_GENAI_USE_VERTEXAI=False
+GOOGLE_API_KEY="your-api-key"
\ No newline at end of file
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/multi_agent_researcher/__init__.py b/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/multi_agent_researcher/__init__.py
new file mode 100644
index 0000000..b437de5
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/multi_agent_researcher/__init__.py
@@ -0,0 +1 @@
+from .agent import root_agent
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/multi_agent_researcher/agent.py b/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/multi_agent_researcher/agent.py
new file mode 100644
index 0000000..8f05ba3
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/multi_agent_researcher/agent.py
@@ -0,0 +1,90 @@
+import os
+from dotenv import load_dotenv
+from google.adk.agents import LlmAgent
+from google.adk.tools.agent_tool import AgentTool
+from google.adk.tools import google_search
+
+# Load environment variables
+load_dotenv()
+
+# --- Sub-agents ---
+research_agent = LlmAgent(
+ name="research_agent",
+ model="gemini-2.0-flash",
+ description="Finds key information and outlines for a given topic.",
+ instruction=(
+ "You are a focused research specialist. Given a user topic or goal, "
+ "conduct thorough research and produce:\n"
+ "1. A comprehensive bullet list of key facts and findings\n"
+ "2. Relevant sources and references (when available)\n"
+ "3. A structured outline for approaching the topic\n"
+ "4. Current trends or recent developments\n\n"
+ "Keep your research factual, well-organized, and comprehensive. "
+ "Use the google_search tool to find current information when needed."
+ ),
+ tools=[google_search]
+)
+
+summarizer_agent = LlmAgent(
+ name="summarizer_agent",
+ model="gemini-2.5-flash",
+ description="Summarizes research findings clearly and concisely.",
+ instruction=(
+ "You are a skilled summarizer. Given research findings, create:\n"
+ "1. A concise executive summary (2-3 sentences)\n"
+ "2. 5-7 key bullet points highlighting the most important information\n"
+ "3. A clear takeaway message\n"
+ "4. Any critical insights or patterns you notice\n\n"
+ "Focus on clarity, relevance, and actionable insights. "
+ "Avoid repetition and maintain the logical flow of information."
+ ),
+)
+
+critic_agent = LlmAgent(
+ name="critic_agent",
+ model="gemini-2.5-flash",
+ description="Provides constructive critique and improvement suggestions.",
+ instruction=(
+ "You are a thoughtful analyst and critic. Given research and summaries, provide:\n"
+ "1. **Gap Analysis**: Identify missing information or areas that need more research\n"
+ "2. **Risk Assessment**: Highlight potential risks, limitations, or biases\n"
+ "3. **Opportunity Identification**: Suggest areas for further exploration or improvement\n"
+ "4. **Quality Score**: Rate the overall research quality (1-10) with justification\n"
+ "5. **Actionable Recommendations**: Provide specific next steps or improvements\n\n"
+ "Be constructive, thorough, and evidence-based in your analysis."
+ ),
+)
+
+# --- Coordinator (root) agent ---
+root_agent = LlmAgent(
+ name="multi_agent_researcher",
+ model="gemini-2.5-flash",
+ description="Advanced multi-agent research coordinator that orchestrates research, analysis, and critique.",
+ instruction=(
+ "You are an advanced research coordinator managing a team of specialized agents.\n\n"
+ "**Your Research Team:**\n"
+ "- **research_agent**: Conducts comprehensive research using web search and analysis\n"
+ "- **summarizer_agent**: Synthesizes findings into clear, actionable insights\n"
+ "- **critic_agent**: Provides quality analysis, gap identification, and recommendations\n\n"
+ "**Research Workflow:**\n"
+ "1. **Research Phase**: Delegate to research_agent to gather comprehensive information\n"
+ "2. **Synthesis Phase**: Use summarizer_agent to distill findings into key insights\n"
+ "3. **Analysis Phase**: Engage critic_agent to evaluate quality and identify opportunities\n"
+ "4. **Integration**: Combine all outputs into a cohesive research report\n\n"
+ "**For Each Research Request:**\n"
+ "- Always start with research_agent to gather information\n"
+ "- Then use summarizer_agent to create clear summaries\n"
+ "- Finally, engage critic_agent for quality analysis and recommendations\n"
+ "- Present the final integrated research report to the user\n\n"
+ "**Output Format:**\n"
+ "Provide a structured response that includes:\n"
+ "- Executive Summary\n"
+ "- Key Findings\n"
+ "- Critical Analysis\n"
+ "- Recommendations\n"
+ "- Next Steps\n\n"
+ "Coordinate your team effectively to deliver high-quality, comprehensive research."
+ ),
+ sub_agents=[summarizer_agent, critic_agent],
+ tools=[AgentTool(research_agent)]
+)
\ No newline at end of file
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/requirements.txt b/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/requirements.txt
new file mode 100644
index 0000000..86ec1b2
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/8_simple_multi_agent/requirements.txt
@@ -0,0 +1,2 @@
+google-adk>=1.9.0
+python-dotenv>=1.1.1
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/.env.example b/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/.env.example
new file mode 100644
index 0000000..f5cfcfb
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/.env.example
@@ -0,0 +1,3 @@
+# If using Gemini via Google AI Studio
+GOOGLE_GENAI_USE_VERTEXAI=False
+GOOGLE_API_KEY="your-api-key"
\ No newline at end of file
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/README.md b/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/README.md
new file mode 100644
index 0000000..8887fb1
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/README.md
@@ -0,0 +1,146 @@
+# π― Tutorial 9.1: Sequential Agents - Business Implementation Plan Generator
+
+## π― What You'll Learn
+
+- **Sequential Agent Composition**: How to orchestrate multiple specialized agents in sequence
+- **AgentTool Integration**: Wrapping agents as tools for enhanced capabilities
+- **Web Search Integration**: Real-time market intelligence through search agents
+- **Business Analysis Pipeline**: From market research to implementation planning
+- **Streamlit Web Interface**: User-friendly application for business planning
+
+## π§ Core Concept: Sequential Agent with Search Capabilities
+
+According to the [ADK workflow agents documentation](https://google.github.io/adk-docs/agents/workflow-agents/), **Sequential Agents** execute sub-agents one after another, in sequence. This tutorial demonstrates a **Business Implementation Plan Generator** that combines web search capabilities with sequential analysis:
+
+```
+Business Topic β SequentialAgent β 4 Sub-agents (Sequential Execution)
+ β
+ [Market Research + Web Search] β [SWOT Analysis] β [Strategy] β [Implementation]
+ β
+ Complete Business Implementation Plan
+```
+
+**Key Innovation**: The Market Research Agent uses a specialized Search Agent (wrapped as AgentTool) to access real-time web search capabilities for current market intelligence.
+
+## π Project Structure
+
+```
+9_1_sequential_agent/
+βββ agent.py # Business implementation plan generator with search capabilities
+βββ app.py # Streamlit web interface for business planning
+βββ requirements.txt # Python dependencies
+βββ README.md # This documentation
+```
+
+## π Getting Started
+
+### 1. Install Dependencies
+```bash
+cd 9_1_sequential_agent
+pip install -r requirements.txt
+```
+
+### 2. Set Up Environment
+Create a `.env` file with your Google API key:
+```bash
+echo "GOOGLE_API_KEY=your_ai_studio_key_here" > .env
+```
+
+**Important**: Get your API key from [Google AI Studio](https://aistudio.google.com/)
+
+### 3. Run the Streamlit App
+```bash
+streamlit run app.py
+```
+
+This will launch the **Business Implementation Plan Generator Agent** web interface!
+
+## π§ͺ How It Works
+
+### **Business Implementation Plan Generation Pipeline**
+
+The agent processes business opportunities through a sophisticated 4-step sequential workflow:
+
+1. **π Market Analysis** - Uses web search for current market information and competitive research
+2. **π SWOT Analysis** - Strategic assessment of strengths, weaknesses, opportunities, and threats
+3. **π― Strategy Development** - Strategic objectives and action plans
+4. **π Implementation Planning** - Detailed execution roadmap and resource requirements
+
+**Key Innovation**: The Market Analysis Agent has access to a specialized Search Agent (wrapped as AgentTool) that can perform real-time web searches using the `google_search` tool. This provides current market intelligence that feeds into the sequential analysis pipeline.
+
+The `SequentialAgent` ensures each step builds upon the previous step's output, creating a comprehensive business implementation plan ready for execution.
+
+**Result**: A complete business implementation plan with market research, strategic analysis, and execution roadmap.
+
+## π§ ADK Concepts Demonstrated
+
+### **1. SequentialAgent Pattern**
+The core workflow orchestrator that executes sub-agents in sequence, ensuring each step builds upon the previous step's output.
+
+### **2. AgentTool Integration**
+Advanced pattern where one agent (Search Agent) is wrapped as a tool and used by another agent (Market Researcher) to enhance capabilities.
+
+### **3. Web Search Capabilities**
+Real-time market intelligence through integrated search functionality, providing current data rather than relying on training data.
+
+### **4. Sub-agent Specialization**
+Each sub-agent specializes in a specific business analysis phase, creating a modular and maintainable system.
+
+### **5. Session Management**
+Maintains conversation state across the entire analysis pipeline, ensuring context flows between agents.
+
+### **6. Runner Execution**
+Processes the complete business implementation workflow with proper error handling and response management.
+
+## π§ͺ Sample Topics to Try
+
+- **Electric vehicle charging stations** in urban areas
+- **AI-powered healthcare diagnostics** and patient care
+- **Sustainable food delivery** services and packaging
+- **Remote work collaboration** tools and platforms
+- **Renewable energy storage** solutions
+
+## π Expected Output
+
+The sequential agent will provide:
+1. **Market Research**: Competitive analysis and market trends
+2. **SWOT Analysis**: Strategic assessment with actionable insights
+3. **Strategy Plan**: Clear objectives and implementation steps
+4. **Implementation Roadmap**: Practical execution guidance
+
+## π― Learning Objectives
+
+- β
Understand how `SequentialAgent` orchestrates sub-agents
+- β
Learn to execute sequential agents with Runner and Session management
+- β
See how sub-agents can build upon each other's output
+- β
Experience a working, executable sequential workflow
+- β
Understand AgentTool integration for enhanced capabilities
+
+## π Next Steps
+
+- Try different business topics to see the sequential workflow in action
+- Experiment with reordering the sub-agents
+- Add more specialized agents to the pipeline
+- Explore other ADK workflow patterns (Parallel, Branching)
+
+## π§ Troubleshooting
+
+**Common Issues:**
+- **API Key Error**: Ensure `GOOGLE_API_KEY` is set in `.env`
+- **Import Errors**: Make sure you're in the correct directory
+- **Search Tool Errors**: Verify your API key has access to search capabilities
+
+**Pro Tips:**
+- Start with simple topics to understand the flow
+- Use the Streamlit app for easy testing and visualization
+- The sequential pattern is great for predictable, step-by-step processes
+- Web search integration provides real-time market intelligence
+
+## π Key Takeaways
+
+- **SequentialAgent** is perfect for workflows that must happen in order
+- **AgentTool integration** allows agents to enhance each other's capabilities
+- **Web search capabilities** provide current market intelligence
+- **Sub-agents** can be simple `LlmAgent` instances or complex tool-enabled agents
+- **Clean, readable code** makes it easy to understand and modify
+- **Streamlit interface** provides user-friendly access to complex agent workflows
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/agent.py b/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/agent.py
new file mode 100644
index 0000000..25cfeec
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/agent.py
@@ -0,0 +1,146 @@
+import os
+import asyncio
+from dotenv import load_dotenv
+from google.adk.agents import LlmAgent, SequentialAgent
+from google.adk.tools import google_search
+from google.adk.tools.agent_tool import AgentTool
+from google.adk.sessions import InMemorySessionService
+from google.adk.runners import Runner
+from google.genai import types
+
+# Load environment variables
+load_dotenv()
+
+# --- Search Agent (Wrapped as AgentTool) ---
+search_agent = LlmAgent(
+ name="search_agent",
+ model="gemini-2.0-flash",
+ description="Conducts web search for current market information and competitive analysis",
+ instruction=(
+ "You are a web search specialist. When given a business topic:\n"
+ "1. Use web search to find current market information\n"
+ "2. Identify key competitors and their market position\n"
+ "3. Gather recent industry trends and market data\n"
+ "4. Find market size estimates and growth projections\n"
+ "5. Provide comprehensive, up-to-date market analysis\n\n"
+ "Always use web search to get the most current information available."
+ ),
+ tools=[google_search]
+)
+
+# --- Simple Sub-agents ---
+market_researcher = LlmAgent(
+ name="market_researcher",
+ model="gemini-2.5-flash",
+ description="Conducts market research and competitive analysis using search capabilities",
+ instruction=(
+ "You are a market research specialist. Given a business topic:\n"
+ "1. Use the search_agent to gather current market information\n"
+ "2. Identify key competitors and their market position\n"
+ "3. Analyze current market trends and opportunities\n"
+ "4. Provide industry insights and market size estimates\n"
+ "5. Synthesize search results into comprehensive market analysis\n\n"
+ "Provide a comprehensive analysis in clear, structured format based on current web research."
+ ),
+ tools=[AgentTool(search_agent)]
+)
+
+swot_analyzer = LlmAgent(
+ name="swot_analyzer",
+ model="gemini-2.5-flash",
+ description="Performs SWOT analysis based on market research",
+ instruction=(
+ "You are a strategic analyst. Given market research findings:\n"
+ "1. Identify internal strengths and competitive advantages\n"
+ "2. Assess internal weaknesses and limitations\n"
+ "3. Identify external opportunities in the market\n"
+ "4. Evaluate external threats and challenges\n\n"
+ "Provide a clear SWOT analysis with actionable insights."
+ )
+)
+
+strategy_formulator = LlmAgent(
+ name="strategy_formulator",
+ model="gemini-2.5-flash",
+ description="Develops strategic objectives and action plans",
+ instruction=(
+ "You are a strategic planner. Given SWOT analysis results:\n"
+ "1. Define 3-5 key strategic objectives\n"
+ "2. Create specific action items for each objective\n"
+ "3. Recommend realistic timeline for implementation\n"
+ "4. Define success metrics and KPIs to track\n\n"
+ "Provide a clear strategic plan with actionable steps."
+ )
+)
+
+implementation_planner = LlmAgent(
+ name="implementation_planner",
+ model="gemini-2.5-flash",
+ description="Creates detailed implementation roadmap",
+ instruction=(
+ "You are an implementation specialist. Given the strategy plan:\n"
+ "1. Identify required resources (human, financial, technical)\n"
+ "2. Define key milestones and checkpoints\n"
+ "3. Develop risk mitigation strategies\n"
+ "4. Provide final recommendations with confidence level\n\n"
+ "Create a practical implementation roadmap."
+ )
+)
+
+# --- Sequential Agent (Pure Sequential Pattern) ---
+business_intelligence_team = SequentialAgent(
+ name="business_intelligence_team",
+ description="Sequentially processes business intelligence through research, analysis, strategy, and planning",
+ sub_agents=[
+ market_researcher, # Step 1: Market research (with search capabilities)
+ swot_analyzer, # Step 2: SWOT analysis
+ strategy_formulator, # Step 3: Strategy development
+ implementation_planner # Step 4: Implementation planning
+ ]
+)
+
+# --- Runner Setup for Execution ---
+session_service = InMemorySessionService()
+runner = Runner(
+ agent=business_intelligence_team,
+ app_name="business_intelligence",
+ session_service=session_service
+)
+
+# --- Simple Execution Function ---
+async def analyze_business_intelligence(user_id: str, business_topic: str) -> str:
+ """Process business intelligence through the sequential pipeline"""
+ session_id = f"bi_session_{user_id}"
+
+ # Create or get session
+ session = await session_service.get_session(
+ app_name="business_intelligence",
+ user_id=user_id,
+ session_id=session_id
+ )
+ if not session:
+ session = await session_service.create_session(
+ app_name="business_intelligence",
+ user_id=user_id,
+ session_id=session_id,
+ state={"business_topic": business_topic, "conversation_history": []}
+ )
+
+ # Create user content
+ user_content = types.Content(
+ role='user',
+ parts=[types.Part(text=f"Please analyze this business topic: {business_topic}")]
+ )
+
+ # Run the sequential pipeline
+ response_text = ""
+ async for event in runner.run_async(
+ user_id=user_id,
+ session_id=session_id,
+ new_message=user_content
+ ):
+ if event.is_final_response():
+ if event.content and event.content.parts:
+ response_text = event.content.parts[0].text
+
+ return response_text
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/app.py b/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/app.py
new file mode 100644
index 0000000..a2f4430
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/app.py
@@ -0,0 +1,112 @@
+import streamlit as st
+import asyncio
+from agent import business_intelligence_team, analyze_business_intelligence
+
+# Page configuration
+st.set_page_config(
+ page_title="Sequential Agent Demo",
+ page_icon=":arrow_right:",
+ layout="wide"
+)
+
+# Title and description
+st.title("π Business Implementation Plan Generator Agent")
+st.markdown("""
+This **Business Implementation Plan Generator Agent** analyzes business opportunities through a comprehensive 4-step process:
+
+1. **π Market Analysis** - Researches market, competitors, and trends using web search
+2. **π SWOT Analysis** - Identifies strengths, weaknesses, opportunities, and threats
+3. **π― Strategy Development** - Creates strategic objectives and action plans
+4. **π Implementation Planning** - Generates detailed business implementation roadmap
+
+**Result**: A complete business implementation plan ready for execution.
+""")
+
+# This is a placeholder user_id for demo purposes.
+# In a real app, you might use authentication or session state to set this.
+user_id = "demo_user"
+
+# Sample business topics
+sample_topics = [
+ "Electric vehicle charging stations in urban areas",
+ "AI-powered healthcare diagnostics",
+ "Sustainable food delivery services",
+ "Remote work collaboration tools",
+ "Renewable energy storage solutions"
+]
+
+# Main content
+st.header("Generate Your Business Implementation Plan")
+
+# Topic input
+business_topic = st.text_area(
+ "Enter a business opportunity to analyze:",
+ value=sample_topics[0],
+ height=100,
+ placeholder="Describe a business opportunity, industry, or market you'd like to analyze for implementation planning..."
+)
+
+# Sample topics
+st.subheader("Or choose from sample business opportunities:")
+cols = st.columns(len(sample_topics))
+for i, topic in enumerate(sample_topics):
+ if cols[i].button(topic, key=f"topic_{i}"):
+ business_topic = topic
+ st.rerun()
+
+# Analysis button
+if st.button("π Generate Business Implementation Plan", type="primary"):
+ if business_topic.strip():
+ st.info("π Starting business analysis... This will research the market, perform SWOT analysis, develop strategy, and create an implementation plan.")
+
+ # Display the workflow
+ st.subheader("Business Analysis Workflow")
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ st.markdown("**1. Market Analysis**")
+ st.markdown("π Web search + competitive research")
+
+ with col2:
+ st.markdown("**2. SWOT Analysis**")
+ st.markdown("π Strengths, Weaknesses, Opportunities, Threats")
+
+ with col3:
+ st.markdown("**3. Strategy Development**")
+ st.markdown("π― Strategic objectives and action plans")
+
+ with col4:
+ st.markdown("**4. Implementation Planning**")
+ st.markdown("π Detailed roadmap and execution plan")
+
+ # Run the actual analysis
+ with st.spinner("Generating comprehensive business implementation plan..."):
+ try:
+ result = asyncio.run(analyze_business_intelligence(user_id, business_topic))
+
+ st.success("β
Business Implementation Plan Generated!")
+ st.subheader("Your Business Implementation Plan")
+ st.markdown(result)
+
+ except Exception as e:
+ st.error(f"β Error during analysis: {str(e)}")
+ st.info("Make sure you have set up your GOOGLE_API_KEY in the .env file")
+
+ else:
+ st.error("Please enter a business opportunity to analyze.")
+
+# How it works (in sidebar)
+with st.sidebar:
+ st.header("How It Works")
+ st.markdown("""
+ The **Business Implementation Plan Generator Agent** uses a sophisticated sequential workflow to create comprehensive business plans:
+
+ 1. **π Market Analysis Agent**: Uses web search to research current market conditions, competitors, and trends
+ 2. **π SWOT Analysis Agent**: Analyzes the market research to identify strategic insights
+ 3. **π― Strategy Development Agent**: Creates strategic objectives and action plans based on SWOT analysis
+ 4. **π Implementation Planning Agent**: Develops detailed execution roadmaps and resource requirements
+
+ **Key Innovation**: The Market Analysis Agent has access to a specialized Search Agent (wrapped as AgentTool) that can perform real-time web searches for current market intelligence.
+
+ Each agent builds upon the previous agent's output, creating a comprehensive business implementation plan ready for execution.
+ """)
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/requirements.txt b/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/requirements.txt
new file mode 100644
index 0000000..eb73986
--- /dev/null
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/9_multi_agent_patterns/9_1_sequential_agent/requirements.txt
@@ -0,0 +1,4 @@
+google-adk>=1.9.0
+streamlit>=1.28.0
+python-dotenv>=1.1.1
+pydantic>=2.0.0
diff --git a/ai_agent_framework_crash_course/google_adk_crash_course/README.md b/ai_agent_framework_crash_course/google_adk_crash_course/README.md
index b4a3af0..378cc9e 100644
--- a/ai_agent_framework_crash_course/google_adk_crash_course/README.md
+++ b/ai_agent_framework_crash_course/google_adk_crash_course/README.md
@@ -48,7 +48,20 @@ This crash course covers the essential concepts of Google ADK through hands-on t
- **[6.2 LLM Interaction Callbacks](./6_callbacks/6_2_llm_interaction_callbacks/README.md)** - Track model requests and responses
- **[6.3 Tool Execution Callbacks](./6_callbacks/6_3_tool_execution_callbacks/README.md)** - Monitor tool calls and results
-7. **More tutorials coming soon!**
+7. **[7_plugins](./7_plugins/README.md)** - Plugin system for cross-cutting concerns
+ - Global callback management
+ - Request/response modification
+ - Error handling and logging
+ - Usage analytics and monitoring
+
+8. **[8_simple_multi_agent](./8_simple_multi_agent/README.md)** - Multi-agent orchestration
+ - **[8.1 Multi-Agent Researcher](./8_simple_multi_agent/multi_agent_researcher/README.md)** - Research pipeline with specialized agents
+ - Coordinator agent with sub-agents
+ - Sequential workflow: Research β Summarize β Critique
+ - Web search integration and comprehensive analysis
+
+9. **[9_multi_agent_patterns](./9_multi_agent_patterns/README.md)** - Multi-Agent Patterns
+ - **[9.1 Sequential Agent](./9_multi_agent_patterns/9_1_sequential_agent/README.md)** β Deterministic pipeline of sub-agents (e.g., Draft β Critique β Improve)
## π οΈ Prerequisites
diff --git a/rag_tutorials/agentic_rag_gpt5/README.md b/rag_tutorials/agentic_rag_gpt5/README.md
new file mode 100644
index 0000000..556929d
--- /dev/null
+++ b/rag_tutorials/agentic_rag_gpt5/README.md
@@ -0,0 +1,153 @@
+# π§ Agentic RAG with GPT-5
+
+An agentic RAG application built with the Agno framework, featuring GPT-5 and LanceDB for efficient knowledge retrieval and question answering.
+
+## β¨ Features
+
+- **π€ GPT-5**: Latest OpenAI model for intelligent responses
+- **ποΈ LanceDB**: Lightweight vector database for fast similarity search
+- **π Agentic RAG**: Intelligent retrieval augmented generation
+- **π Markdown Formatting**: Beautiful, structured responses
+- **π Dynamic Knowledge**: Add URLs to expand knowledge base
+- **β‘ Real-time Streaming**: Watch answers generate live
+- **π― Clean Interface**: Simplified UI without configuration complexity
+
+## π Quick Start
+
+### Prerequisites
+
+- Python 3.11+
+- OpenAI API key with GPT-5 access
+
+### Installation
+
+1. **Clone and navigate to the project**
+ ```bash
+ cd rag_tutorials/agentic_rag_gpt5
+ ```
+
+2. **Install dependencies**
+ ```bash
+ pip install -r requirements.txt
+ ```
+
+3. **Set up your OpenAI API key**
+ ```bash
+ export OPENAI_API_KEY="your-api-key-here"
+ ```
+ Or create a `.env` file:
+ ```
+ OPENAI_API_KEY=your-api-key-here
+ ```
+
+4. **Run the application**
+ ```bash
+ streamlit run agentic_rag_gpt5.py
+ ```
+
+## π― How to Use
+
+1. **Enter your OpenAI API key** in the sidebar
+2. **Add knowledge sources** by entering URLs in the sidebar
+3. **Ask questions** using the text area or suggested prompts
+4. **Watch answers stream** in real-time with markdown formatting
+
+### Suggested Questions
+
+- **"What is Agno?"** - Learn about the Agno framework and agents
+- **"Teams in Agno"** - Understand how teams work in Agno
+- **"Build RAG system"** - Get a step-by-step guide to building RAG systems
+
+## ποΈ Architecture
+
+### Core Components
+
+- **`Agent`**: Orchestrates the entire Q&A process
+- **`UrlKnowledge`**: Manages document loading from URLs
+- **`LanceDb`**: Vector database for efficient similarity search
+- **`OpenAIEmbedder`**: Converts text to embeddings
+- **`OpenAIChat`**: GPT-5-nano model for generating responses
+
+### Data Flow
+
+1. **Knowledge Loading**: URLs are processed and stored in LanceDB
+2. **Vector Search**: OpenAI embeddings enable semantic search
+3. **Response Generation**: GPT-5-nano processes information and generates answers
+4. **Streaming Output**: Real-time display of formatted responses
+
+## π§ Configuration
+
+### Database Settings
+- **Vector DB**: LanceDB with local storage
+- **Table Name**: `agentic_rag_docs`
+- **Search Type**: Vector similarity search
+
+## π Knowledge Management
+
+### Adding Sources
+- Use the sidebar to add new URLs
+- Sources are automatically processed and indexed
+- Current sources are displayed as numbered list
+
+### Default Knowledge
+- Starts with Agno documentation: `https://docs.agno.com/introduction/agents.md`
+- Expandable with any web-based documentation
+
+## π¨ UI Features
+
+### Sidebar
+- **API Key Management**: Secure input for OpenAI credentials
+- **URL Addition**: Dynamic knowledge base expansion
+- **Current Sources**: Numbered list of loaded URLs
+
+### Main Interface
+- **Suggested Prompts**: Quick access to common questions
+- **Query Input**: Large text area for custom questions
+- **Real-time Streaming**: Live answer generation
+- **Markdown Rendering**: Beautiful formatted responses
+
+## π οΈ Technical Details
+
+### Dependencies
+```
+streamlit>=1.28.0
+agno>=0.1.0
+openai>=1.0.0
+lancedb>=0.4.0
+python-dotenv>=1.0.0
+```
+
+### Key Features
+- **Event Filtering**: Only shows `RunResponseContent` events for clean output
+- **Safe Attribute Access**: Prevents errors from missing attributes
+- **Caching**: Efficient resource loading with Streamlit caching
+- **Error Handling**: Graceful handling of API and processing errors
+
+## π Troubleshooting
+
+### Common Issues
+
+**ModelProviderError with max_tokens**
+- β
Fixed: Uses `max_completion_tokens` instead of `max_tokens`
+
+**Tool calls appearing in output**
+- β
Fixed: Filters to only show `RunResponseContent` events
+
+**Knowledge base not loading**
+- Check OpenAI API key is valid
+- Ensure URLs are accessible
+- Verify internet connection
+
+### Performance Tips
+- **Cache Resources**: Knowledge base and agent are cached for efficiency
+- **Streaming**: Real-time updates without blocking
+- **LanceDB**: Fast local vector search without external dependencies
+
+## π― Use Cases
+
+- **Documentation Q&A**: Ask questions about technical documentation
+- **Research Assistant**: Get answers from multiple knowledge sources
+- **Learning Tool**: Interactive exploration of complex topics
+- **Content Discovery**: Find relevant information across multiple sources
+
+**Built with β€οΈ using Agno, GPT-5, and LanceDB**
diff --git a/rag_tutorials/agentic_rag_gpt5/agentic_rag_gpt5.py b/rag_tutorials/agentic_rag_gpt5/agentic_rag_gpt5.py
new file mode 100644
index 0000000..a115662
--- /dev/null
+++ b/rag_tutorials/agentic_rag_gpt5/agentic_rag_gpt5.py
@@ -0,0 +1,208 @@
+import streamlit as st
+import os
+from agno.agent import Agent
+from agno.embedder.openai import OpenAIEmbedder
+from agno.knowledge.url import UrlKnowledge
+from agno.models.openai import OpenAIChat
+from agno.vectordb.lancedb import LanceDb, SearchType
+
+from dotenv import load_dotenv
+
+# Load environment variables
+load_dotenv()
+
+# Page configuration
+st.set_page_config(
+ page_title="Agentic RAG with GPT-5",
+ page_icon="π§ ",
+ layout="wide"
+)
+
+# Main title and description
+st.title("π§ Agentic RAG with GPT-5")
+st.markdown("""
+This app demonstrates an intelligent AI agent that:
+1. **Retrieves** relevant information from knowledge sources using LanceDB
+2. **Answers** your questions clearly and concisely
+
+Enter your OpenAI API key in the sidebar to get started!
+""")
+
+# Sidebar for API key and settings
+with st.sidebar:
+ st.header("π§ Configuration")
+
+ # OpenAI API Key
+ openai_key = st.text_input(
+ "OpenAI API Key",
+ type="password",
+ value=os.getenv("OPENAI_API_KEY", ""),
+ help="Get your key from https://platform.openai.com/"
+ )
+
+ # Add URLs to knowledge base
+ st.subheader("π Add Knowledge Sources")
+ new_url = st.text_input(
+ "Add URL",
+ placeholder="https://docs.agno.com/introduction",
+ help="Enter a URL to add to the knowledge base"
+ )
+
+ if st.button("β Add URL", type="primary"):
+ if new_url:
+ st.session_state.urls_to_add = new_url
+ st.success(f"URL added to queue: {new_url}")
+ else:
+ st.error("Please enter a URL")
+
+# Check if API key is provided
+if openai_key:
+ # Initialize knowledge base (cached to avoid reloading)
+ @st.cache_resource(show_spinner="π Loading knowledge base...")
+ def load_knowledge() -> UrlKnowledge:
+ """Load and initialize the knowledge base with LanceDB"""
+ kb = UrlKnowledge(
+ urls=["https://docs.agno.com/introduction/agents.md"], # Default URL
+ vector_db=LanceDb(
+ uri="tmp/lancedb",
+ table_name="agentic_rag_docs",
+ search_type=SearchType.vector, # Use vector search
+ embedder=OpenAIEmbedder(
+ api_key=openai_key
+ ),
+ ),
+ )
+ kb.load(recreate=True) # Load documents into LanceDB
+ return kb
+
+ # Initialize agent (cached to avoid reloading)
+ @st.cache_resource(show_spinner="π€ Loading agent...")
+ def load_agent(_kb: UrlKnowledge) -> Agent:
+ """Create an agent with reasoning capabilities"""
+ return Agent(
+ model=OpenAIChat(
+ id="gpt-5-nano",
+ api_key=openai_key
+ ),
+ knowledge=_kb,
+ search_knowledge=True, # Enable knowledge search
+ instructions=[
+ "Always search your knowledge before answering the question.",
+ "Provide clear, well-structured answers in markdown format.",
+ "Use proper markdown formatting with headers, lists, and emphasis where appropriate.",
+ "Structure your response with clear sections and bullet points when helpful.",
+ ],
+ markdown=True, # Enable markdown formatting
+ )
+
+ # Load knowledge and agent
+ knowledge = load_knowledge()
+ agent = load_agent(knowledge)
+
+ # Display current URLs in knowledge base
+ if knowledge.urls:
+ st.sidebar.subheader("π Current Knowledge Sources")
+ for i, url in enumerate(knowledge.urls, 1):
+ st.sidebar.markdown(f"{i}. {url}")
+
+ # Handle URL additions
+ if hasattr(st.session_state, 'urls_to_add') and st.session_state.urls_to_add:
+ with st.spinner("π₯ Loading new documents..."):
+ knowledge.urls.append(st.session_state.urls_to_add)
+ knowledge.load(
+ recreate=False, # Don't recreate DB
+ upsert=True, # Update existing docs
+ skip_existing=True # Skip already loaded docs
+ )
+ st.success(f"β
Added: {st.session_state.urls_to_add}")
+ del st.session_state.urls_to_add
+ st.rerun()
+
+ # Main query section
+ st.divider()
+ st.subheader("π€ Ask a Question")
+
+ # Suggested prompts
+ st.markdown("**Try these prompts:**")
+ col1, col2, col3 = st.columns(3)
+ with col1:
+ if st.button("What is Agno?", use_container_width=True):
+ st.session_state.query = "What is Agno and how do Agents work?"
+ with col2:
+ if st.button("Teams in Agno", use_container_width=True):
+ st.session_state.query = "What are Teams in Agno and how do they work?"
+ with col3:
+ if st.button("Build RAG system", use_container_width=True):
+ st.session_state.query = "Give me a step-by-step guide to building a RAG system."
+
+ # Query input
+ query = st.text_area(
+ "Your question:",
+ value=st.session_state.get("query", "What are AI Agents?"),
+ height=100,
+ help="Ask anything about the loaded knowledge sources"
+ )
+
+ # Run button
+ if st.button("π Get Answer", type="primary"):
+ if query:
+ # Create container for answer
+ st.markdown("### π‘ Answer")
+ answer_container = st.container()
+ answer_placeholder = answer_container.empty()
+
+ # Variables to accumulate content
+ answer_text = ""
+
+ # Stream the agent's response
+ with st.spinner("π Searching and generating answer..."):
+ for chunk in agent.run(
+ query,
+ stream=True, # Enable streaming
+ ):
+ # Update answer display - only show content from RunResponseContent events
+ if hasattr(chunk, 'event') and chunk.event == "RunResponseContent":
+ if hasattr(chunk, 'content') and chunk.content and isinstance(chunk.content, str):
+ answer_text += chunk.content
+ answer_placeholder.markdown(
+ answer_text,
+ unsafe_allow_html=True
+ )
+ else:
+ st.error("Please enter a question")
+
+else:
+ # Show instructions if API key is missing
+ st.info("""
+ π **Welcome! To use this app, you need:**
+
+ - **OpenAI API Key** (set it in the sidebar)
+ - Sign up at [platform.openai.com](https://platform.openai.com/)
+ - Generate a new API key
+
+ Once you enter the key, the app will load the knowledge base and agent.
+ """)
+
+# Footer with explanation
+st.divider()
+with st.expander("π How This Works"):
+ st.markdown("""
+ **This app uses the Agno framework to create an intelligent Q&A system:**
+
+ 1. **Knowledge Loading**: URLs are processed and stored in LanceDB vector database
+ 2. **Vector Search**: Uses OpenAI's embeddings for semantic search to find relevant information
+ 3. **GPT-5**: OpenAI's GPT-5 model processes the information and generates answers
+
+ **Key Components:**
+ - `UrlKnowledge`: Manages document loading from URLs
+ - `LanceDb`: Vector database for efficient similarity search
+ - `OpenAIEmbedder`: Converts text to embeddings using OpenAI's embedding model
+
+ - `Agent`: Orchestrates everything to answer questions
+
+ **Why LanceDB?**
+ - Lightweight and easy to set up
+ - No external database required
+ - Fast vector search capabilities
+ - Perfect for prototyping and small to medium-scale applications
+ """)
diff --git a/rag_tutorials/agentic_rag_gpt5/requirements.txt b/rag_tutorials/agentic_rag_gpt5/requirements.txt
new file mode 100644
index 0000000..a30e9e0
--- /dev/null
+++ b/rag_tutorials/agentic_rag_gpt5/requirements.txt
@@ -0,0 +1,5 @@
+streamlit
+agno
+openai
+lancedb
+python-dotenv