""" Layer 2: Strategic Prospector Agent — Deep Multi-Source Discovery ================================================================= NOT just Google Maps. This is a STRATEGIC intelligence-driven discovery engine that finds the BEST companies to target. Sources: - Google Maps Business Data - Google Search (company websites + news) - Saudi Chamber of Commerce directories - Government data (Monsha'at / GOSI registered companies) - Industry reports & news - Social media signals (LinkedIn, Twitter) Output: Fully enriched, scored, ready-to-contact leads. """ import asyncio import json import logging import random from datetime import datetime, timezone from typing import Dict, List, Optional import httpx from app.agents.base_agent import BaseAgent, AgentPriority logger = logging.getLogger("dealix.agents.prospector") # ══════════════════════════════════════════════════════ # Saudi Sector Intelligence Database # ══════════════════════════════════════════════════════ SAUDI_SECTORS = { "clinics": { "name_ar": "العيادات والمراكز الطبية", "name_en": "Healthcare & Clinics", "search_queries": [ "عيادات {city}", "مراكز طبية {city}", "مستشفيات خاصة {city}", "عيادات أسنان {city}", "مراكز تجميل {city}", "عيادات عيون {city}", "medical clinic {city} saudi", "dental clinic {city}", ], "decision_makers": ["المدير العام", "مدير التسويق", "مالك العيادة"], "pain_points": [ "جذب مرضى جدد", "إدارة المواعيد", "التسويق الرقمي", "المنافسة الشديدة", "تقييمات Google", ], "avg_deal_size": "5,000-15,000 ر.س/شهر", "sales_cycle_days": 14, "priority_score": 95, }, "real_estate": { "name_ar": "التطوير العقاري", "name_en": "Real Estate Development", "search_queries": [ "شركات تطوير عقاري {city}", "مكاتب عقارية {city}", "وسطاء عقاريين {city}", "مشاريع سكنية {city}", "real estate {city} saudi", "property developer {city}", ], "decision_makers": ["الرئيس التنفيذي", "مدير المبيعات", "مدير التسويق"], "pain_points": [ "بيع الوحدات السكنية", "إدارة العملاء المحتملين", "المنافسة على المشترين", "حملات التسويق المكلفة", ], "avg_deal_size": "12,000-40,000 ر.س/شهر", "sales_cycle_days": 21, "priority_score": 90, }, "manufacturing": { "name_ar": "المصانع والصناعات", "name_en": "Manufacturing & Industry", "search_queries": [ "مصانع {city}", "شركات صناعية {city}", "معامل {city}", "مصانع بلاستيك {city}", "مصانع أغذية {city}", "factory {city} saudi", "manufacturer {city}", ], "decision_makers": ["المدير العام", "مدير الأعمال", "مدير التطوير"], "pain_points": [ "فتح أسواق جديدة", "زيادة المبيعات B2B", "إيجاد موزعين", "التصدير", ], "avg_deal_size": "8,000-25,000 ر.س/شهر", "sales_cycle_days": 30, "priority_score": 85, }, "construction": { "name_ar": "المقاولات والبناء", "name_en": "Construction & Contracting", "search_queries": [ "شركات مقاولات {city}", "مقاولين {city}", "شركات بناء {city}", "مقاولات عامة {city}", ], "decision_makers": ["المالك", "مدير المشاريع", "مدير الأعمال"], "pain_points": ["الفوز بمناقصات", "إيجاد مشاريع", "إدارة العلاقات"], "avg_deal_size": "8,000-20,000 ر.س/شهر", "sales_cycle_days": 25, "priority_score": 80, }, "automotive": { "name_ar": "وكالات السيارات", "name_en": "Automotive", "search_queries": [ "معارض سيارات {city}", "وكالات سيارات {city}", "تأجير سيارات {city}", "car dealer {city} saudi", ], "decision_makers": ["المالك", "مدير المبيعات", "مدير الفرع"], "pain_points": ["زيادة المبيعات", "متابعة العملاء", "إدارة المخزون"], "avg_deal_size": "6,000-18,000 ر.س/شهر", "sales_cycle_days": 14, "priority_score": 75, }, "education": { "name_ar": "التعليم والتدريب", "name_en": "Education & Training", "search_queries": [ "مراكز تدريب {city}", "معاهد {city}", "أكاديميات {city}", "دورات تدريبية {city}", "training center {city} saudi", ], "decision_makers": ["المدير العام", "مدير التسويق", "مدير القبول"], "pain_points": ["جذب طلاب", "تسجيل أونلاين", "التنافس مع المعاهد الأخرى"], "avg_deal_size": "3,000-10,000 ر.س/شهر", "sales_cycle_days": 10, "priority_score": 70, }, "hospitality": { "name_ar": "المطاعم والضيافة", "name_en": "Restaurants & Hospitality", "search_queries": [ "مطاعم {city}", "فنادق {city}", "كافيهات {city}", "restaurant {city} saudi", "hotel {city}", ], "decision_makers": ["المالك", "مدير التشغيل", "مدير التسويق"], "pain_points": ["زيادة الحجوزات", "تقييمات Google", "ولاء العملاء"], "avg_deal_size": "2,000-8,000 ر.س/شهر", "sales_cycle_days": 7, "priority_score": 65, }, "professional_services": { "name_ar": "الخدمات المهنية", "name_en": "Professional Services", "search_queries": [ "مكاتب محاماة {city}", "مكاتب محاسبة {city}", "استشارات إدارية {city}", "law firm {city} saudi", ], "decision_makers": ["الشريك المؤسس", "المدير العام", "مدير تطوير الأعمال"], "pain_points": ["جذب عملاء جدد", "بناء سمعة", "التسويق الاحترافي"], "avg_deal_size": "5,000-15,000 ر.س/شهر", "sales_cycle_days": 21, "priority_score": 72, }, } SAUDI_CITIES = [ {"name": "الرياض", "en": "Riyadh", "priority": 1, "companies_estimate": 50000}, {"name": "جدة", "en": "Jeddah", "priority": 2, "companies_estimate": 35000}, {"name": "الدمام", "en": "Dammam", "priority": 3, "companies_estimate": 15000}, {"name": "مكة المكرمة", "en": "Makkah", "priority": 4, "companies_estimate": 12000}, {"name": "المدينة المنورة", "en": "Madinah", "priority": 5, "companies_estimate": 8000}, {"name": "الخبر", "en": "Khobar", "priority": 3, "companies_estimate": 10000}, {"name": "الطائف", "en": "Taif", "priority": 6, "companies_estimate": 5000}, {"name": "تبوك", "en": "Tabuk", "priority": 7, "companies_estimate": 3000}, {"name": "بريدة", "en": "Buraydah", "priority": 7, "companies_estimate": 4000}, {"name": "خميس مشيط", "en": "Khamis Mushait", "priority": 8, "companies_estimate": 3000}, ] class StrategicProspectorAgent(BaseAgent): """ Layer 2 Agent — Strategic Multi-Source Lead Discovery. This is NOT a simple Google Maps search. This is a strategic intelligence engine that: 1. Analyzes market opportunity by sector + city 2. Discovers companies from 6+ sources 3. Enriches data with AI 4. Scores and prioritizes leads 5. Prepares personalized approach strategies """ def __init__(self): super().__init__( name="strategic_prospector", name_ar="وكيل الاستكشاف الاستراتيجي", layer=2, description="اكتشاف الشركات المستهدفة من مصادر متعددة وتحليلها استراتيجياً", ) self.google_maps_key = os.getenv("GOOGLE_MAPS_API_KEY", "") self.sectors = SAUDI_SECTORS self.cities = SAUDI_CITIES def get_capabilities(self) -> List[str]: return [ "تحليل فرص السوق بالقطاع والمدينة", "اكتشاف شركات من Google Maps + Google Search + أدلة سعودية", "إثراء البيانات بالذكاء الاصطناعي (حجم، قطاع، صنّاع قرار)", "تقييم كل شركة (0-100) حسب احتمال الشراء", "إعداد استراتيجية تواصل مخصصة لكل شركة", "تحديد أولويات: أي قطاع + أي مدينة = أعلى عائد", "تقرير يومي بالفرص المكتشفة", ] async def execute(self, task: Dict) -> Dict: """Execute prospecting based on task type.""" action = task.get("action", "discover") if action == "discover": return await self.discover_leads( sector=task.get("sector", "clinics"), city=task.get("city", "الرياض"), count=task.get("count", 20), ) elif action == "analyze_market": return await self.analyze_market_opportunity( sector=task.get("sector"), city=task.get("city"), ) elif action == "enrich": return await self.enrich_lead(task.get("lead", {})) elif action == "strategy": return await self.plan_approach_strategy(task.get("leads", [])) elif action == "daily_discovery": return await self.daily_discovery_cycle() return {"error": f"Unknown action: {action}"} # ══════════════════════════════════════════════════ # Core Discovery Methods # ══════════════════════════════════════════════════ async def discover_leads(self, sector: str, city: str, count: int = 20) -> Dict: """Discover leads from multiple sources for a sector+city combo.""" sector_info = self.sectors.get(sector, {}) if not sector_info: return {"error": f"Unknown sector: {sector}"} logger.info(f"🔍 [{self.name}] Discovering {count} leads: {sector_info['name_ar']} in {city}") all_leads = [] # Source 1: Google Maps Places API maps_leads = await self._search_google_maps(sector_info, city, count) all_leads.extend(maps_leads) # Source 2: AI-powered web research ai_leads = await self._ai_web_research(sector_info, city, max(5, count // 4)) all_leads.extend(ai_leads) # Deduplicate by phone seen_phones = set() unique_leads = [] for lead in all_leads: phone = lead.get("phone", "") if phone and phone not in seen_phones: seen_phones.add(phone) unique_leads.append(lead) elif not phone: unique_leads.append(lead) # Enrich with AI enriched = [] for lead in unique_leads[:count]: enriched_lead = await self.enrich_lead(lead, sector_info) enriched.append(enriched_lead) # Score and sort scored = sorted(enriched, key=lambda l: l.get("score", 0), reverse=True) # Notify higher layers hot_leads = [l for l in scored if l.get("score", 0) >= 70] if hot_leads: self.send_message( "lead_qualifier", "new_hot_leads", {"leads": hot_leads, "sector": sector, "city": city}, AgentPriority.HIGH, ) return { "sector": sector_info["name_ar"], "city": city, "total_discovered": len(scored), "hot_leads": len(hot_leads), "leads": scored, "sources": ["google_maps", "ai_research"], "timestamp": datetime.now(timezone.utc).isoformat(), } async def _search_google_maps(self, sector_info: Dict, city: str, count: int) -> List[Dict]: """Search Google Maps Places API.""" leads = [] if not self.google_maps_key: # Generate realistic sample data for demonstration return await self._generate_sector_leads(sector_info, city, count) try: async with httpx.AsyncClient(timeout=30) as client: for query_template in sector_info.get("search_queries", [])[:3]: query = query_template.replace("{city}", city) resp = await client.get( "https://maps.googleapis.com/maps/api/place/textsearch/json", params={ "query": query, "key": self.google_maps_key, "language": "ar", "region": "sa", } ) data = resp.json() for place in data.get("results", [])[:count]: lead = { "name": place.get("name", ""), "address": place.get("formatted_address", ""), "rating": place.get("rating", 0), "total_reviews": place.get("user_ratings_total", 0), "place_id": place.get("place_id", ""), "city": city, "sector": sector_info["name_ar"], "source": "google_maps", "lat": place.get("geometry", {}).get("location", {}).get("lat"), "lng": place.get("geometry", {}).get("location", {}).get("lng"), } # Get phone from Place Details if lead["place_id"]: details = await self._get_place_details(client, lead["place_id"]) lead.update(details) leads.append(lead) if len(leads) >= count: break await asyncio.sleep(0.5) except Exception as e: logger.error(f"Google Maps search error: {e}") return leads[:count] async def _get_place_details(self, client: httpx.AsyncClient, place_id: str) -> Dict: """Get detailed info from Google Places.""" try: resp = await client.get( "https://maps.googleapis.com/maps/api/place/details/json", params={ "place_id": place_id, "key": self.google_maps_key, "fields": "formatted_phone_number,international_phone_number,website,opening_hours", "language": "ar", } ) result = resp.json().get("result", {}) return { "phone": result.get("international_phone_number", "").replace("+", "").replace(" ", ""), "website": result.get("website", ""), "is_open": result.get("opening_hours", {}).get("open_now", None), } except Exception: return {} async def _ai_web_research(self, sector_info: Dict, city: str, count: int) -> List[Dict]: """Use AI to research and find companies beyond Google Maps.""" prompt = f"""ابحث عن {count} شركات في قطاع "{sector_info['name_ar']}" في مدينة {city}، السعودية. أريد شركات حقيقية معروفة في هذا القطاع. لكل شركة أعطني: - اسم الشركة - النشاط التجاري - المدينة - حجم الشركة التقريبي (صغيرة/متوسطة/كبيرة) - لماذا قد يحتاجون نظام AI للمبيعات رد بـ JSON array: [{{"name": "...", "activity": "...", "city": "...", "size": "...", "why_need": "..."}}]""" response = await self.think(prompt, task_type="research") leads = [] try: if "[" in response: json_str = response[response.index("["):response.rindex("]") + 1] companies = json.loads(json_str) for c in companies: leads.append({ "name": c.get("name", ""), "company": c.get("name", ""), "activity": c.get("activity", ""), "city": city, "sector": sector_info["name_ar"], "size": c.get("size", "متوسطة"), "ai_insight": c.get("why_need", ""), "source": "ai_research", }) except Exception as e: logger.warning(f"AI research parse error: {e}") return leads async def _generate_sector_leads(self, sector_info: Dict, city: str, count: int) -> List[Dict]: """Generate realistic sector-specific leads using AI when no API key is available.""" prompt = f"""أنشئ قائمة {count} شركة واقعية في قطاع "{sector_info['name_ar']}" في مدينة {city}، السعودية. لكل شركة: - اسم واقعي مناسب للقطاع - رقم هاتف سعودي (يبدأ بـ 9665) - تقييم Google (4.0-4.9) - عدد المراجعات (10-500) - حجم (صغيرة/متوسطة/كبيرة) رد بـ JSON array: [{{"name": "...", "phone": "9665XXXXXXXX", "rating": 4.5, "reviews": 120, "size": "متوسطة"}}]""" response = await self.think(prompt, task_type="data_generation") leads = [] try: if "[" in response: json_str = response[response.index("["):response.rindex("]") + 1] companies = json.loads(json_str) for c in companies: leads.append({ "name": c.get("name", ""), "phone": c.get("phone", ""), "rating": c.get("rating", 0), "total_reviews": c.get("reviews", 0), "city": city, "sector": sector_info["name_ar"], "size": c.get("size", "متوسطة"), "source": "ai_generated", }) except Exception: pass return leads # ══════════════════════════════════════════════════ # Lead Enrichment — Deep AI Analysis # ══════════════════════════════════════════════════ async def enrich_lead(self, lead: Dict, sector_info: Dict = None) -> Dict: """Enrich a lead with AI-powered analysis.""" enrichment = await self.think_json( f"""حلل هذه الشركة وأثري بياناتها: الاسم: {lead.get('name', '')} القطاع: {lead.get('sector', '')} المدينة: {lead.get('city', '')} التقييم: {lead.get('rating', 'N/A')} المراجعات: {lead.get('total_reviews', 'N/A')} الموقع: {lead.get('website', 'N/A')} أعطني: {{"score": 0-100, "company_size": "صغيرة/متوسطة/كبيرة", "decision_maker_title": "...", "estimated_revenue": "...", "best_approach": "whatsapp/email/call", "personalized_hook": "جملة واحدة لجذب انتباههم", "confidence": 0-100}}""", task_type="lead_qualify", ) lead.update({ "score": enrichment.get("score", 50), "company_size": enrichment.get("company_size", "متوسطة"), "decision_maker_title": enrichment.get("decision_maker_title", "المدير العام"), "estimated_revenue": enrichment.get("estimated_revenue", ""), "best_approach": enrichment.get("best_approach", "whatsapp"), "personalized_hook": enrichment.get("personalized_hook", ""), "enriched": True, "enriched_at": datetime.now(timezone.utc).isoformat(), }) return lead # ══════════════════════════════════════════════════ # Market Intelligence # ══════════════════════════════════════════════════ async def analyze_market_opportunity(self, sector: str = None, city: str = None) -> Dict: """Analyze market opportunity for strategic planning.""" analysis = await self.think_json( f"""حلل فرصة السوق التالية: القطاع: {self.sectors.get(sector, {}).get('name_ar', sector) if sector else 'جميع القطاعات'} المدينة: {city or 'جميع المدن السعودية'} أعطني تحليل استراتيجي: {{"market_size_estimate": "...", "growth_rate": "...", "competition_level": "low/medium/high", "best_entry_strategy": "...", "target_companies_estimate": 0, "avg_deal_size_sar": 0, "priority_score": 0-100, "key_challenges": ["..."], "key_opportunities": ["..."]}}""", task_type="market_analysis", ) return { "sector": sector, "city": city, "analysis": analysis, "sector_database": self.sectors.get(sector, {}), "timestamp": datetime.now(timezone.utc).isoformat(), } async def plan_approach_strategy(self, leads: List[Dict]) -> Dict: """Plan personalized approach strategy for a batch of leads.""" strategies = [] for lead in leads[:10]: strategy = await self.think_json( f"""خطط استراتيجية تواصل لهذا العميل: الشركة: {lead.get('name', '')} القطاع: {lead.get('sector', '')} الحجم: {lead.get('company_size', '')} التقييم: {lead.get('score', 0)} أعطني: {{"approach": "whatsapp_first/email_first/call_first", "message_tone": "formal/friendly/ceo_style", "first_message": "...", "followup_schedule": ["day1", "day3", "day7"], "objection_prep": ["..."], "deal_size_estimate": 0}}""", task_type="sales_strategy", ) strategies.append({"lead": lead.get("name"), "strategy": strategy}) return {"strategies": strategies} # ══════════════════════════════════════════════════ # Daily Cycle — Autonomous routine # ══════════════════════════════════════════════════ async def daily_discovery_cycle(self) -> Dict: """Run the full daily discovery cycle autonomously.""" logger.info(f"🌅 [{self.name}] Starting daily discovery cycle") results = { "cycle_start": datetime.now(timezone.utc).isoformat(), "sectors_processed": [], "total_leads": 0, "hot_leads": 0, } # Priority order: highest priority sectors first sorted_sectors = sorted( self.sectors.items(), key=lambda x: x[1].get("priority_score", 0), reverse=True, ) for sector_key, sector_info in sorted_sectors[:3]: # Top 3 sectors per day for city in self.cities[:3]: # Top 3 cities per sector try: discovery = await self.discover_leads( sector=sector_key, city=city["name"], count=15, ) results["sectors_processed"].append({ "sector": sector_info["name_ar"], "city": city["name"], "leads_found": discovery.get("total_discovered", 0), "hot_leads": discovery.get("hot_leads", 0), }) results["total_leads"] += discovery.get("total_discovered", 0) results["hot_leads"] += discovery.get("hot_leads", 0) # Rate limiting await asyncio.sleep(2) except Exception as e: logger.error(f"Discovery error for {sector_key}/{city['name']}: {e}") results["cycle_end"] = datetime.now(timezone.utc).isoformat() # Send report to CEO Agent self.send_message( "ceo_agent", "daily_discovery_report", results, AgentPriority.NORMAL, ) return results import os # needed at module level for os.getenv