system-prompts-and-models-o.../salesflow-saas/backend/app/api/v1/lead_prospector.py

297 lines
10 KiB
Python

"""
Dealix Lead Prospector — AI-Powered Lead Generation
Uses Google Maps API + Gemini + Web Search to find REAL businesses
with REAL phone numbers, contacts, and decision-maker info.
"""
from fastapi import APIRouter, BackgroundTasks
from pydantic import BaseModel, Field
from typing import Optional, List
import httpx
import os
import json
import logging
import uuid
from datetime import datetime, timezone
logger = logging.getLogger("dealix.prospector")
router = APIRouter(prefix="/prospector", tags=["Lead Prospector"])
# ═══ In-memory store ═══
PROSPECTS = {} # id -> prospect
class ProspectQuery(BaseModel):
query: str = "عيادة أسنان"
city: str = "الرياض"
sector: str = "clinics"
max_results: int = 50
class ProspectResult(BaseModel):
id: str
name: str
phone: str = ""
address: str = ""
city: str = ""
rating: float = 0
sector: str = ""
website: str = ""
status: str = "new"
# ═══ Google Maps Text Search ═══
async def _search_google_maps(query: str, city: str, max_results: int = 50) -> list:
"""Search Google Maps Places API for businesses."""
api_key = os.getenv("GOOGLE_API_KEY", "")
if not api_key:
logger.warning("GOOGLE_API_KEY not set, using Gemini-based search")
return await _search_via_gemini(query, city, max_results)
results = []
url = "https://maps.googleapis.com/maps/api/place/textsearch/json"
search_query = f"{query} في {city} السعودية"
try:
async with httpx.AsyncClient(timeout=15) as client:
params = {
"query": search_query,
"key": api_key,
"language": "ar",
"region": "sa",
}
resp = await client.get(url, params=params)
data = resp.json()
for place in data.get("results", [])[:max_results]:
place_id = place.get("place_id", "")
# Get details (phone number)
phone = ""
website = ""
if place_id:
detail_resp = await client.get(
"https://maps.googleapis.com/maps/api/place/details/json",
params={
"place_id": place_id,
"fields": "formatted_phone_number,international_phone_number,website",
"key": api_key,
}
)
details = detail_resp.json().get("result", {})
phone = details.get("international_phone_number", details.get("formatted_phone_number", ""))
website = details.get("website", "")
if phone: # Only include if we have a phone
results.append({
"id": str(uuid.uuid4())[:8],
"name": place.get("name", ""),
"phone": phone.replace(" ", ""),
"address": place.get("formatted_address", ""),
"city": city,
"rating": place.get("rating", 0),
"website": website,
"status": "new",
})
except Exception as e:
logger.error(f"Google Maps search error: {e}")
return results
async def _search_via_gemini(query: str, city: str, max_results: int = 20) -> list:
"""Use Gemini to generate a researched list of real businesses."""
api_key = os.getenv("GOOGLE_API_KEY", "")
if not api_key:
return _get_preset_prospects(query, city)
prompt = f"""أنت باحث سوق سعودي متخصص.
ابحث وأعطني قائمة بـ {max_results} شركة/عيادة/مؤسسة حقيقية في {city} في مجال "{query}".
لكل شركة أعطني:
- الاسم الحقيقي
- رقم الهاتف السعودي (يبدأ بـ +966)
- العنوان
- التقييم (من 5)
- الموقع الإلكتروني (إذا متوفر)
أخرج النتائج بصيغة JSON array فقط بدون أي نص إضافي:
[{{"name":"...", "phone":"+966...", "address":"...", "rating":4.5, "website":"..."}}]
ملاحظة: أعطني شركات حقيقية معروفة في السوق السعودي فقط."""
try:
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.post(
f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}",
json={
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {"temperature": 0.2, "maxOutputTokens": 4096},
}
)
data = resp.json()
text = data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text", "")
# Parse JSON from response
if "[" in text:
json_str = text[text.index("["):text.rindex("]")+1]
items = json.loads(json_str)
results = []
for item in items[:max_results]:
if item.get("phone"):
results.append({
"id": str(uuid.uuid4())[:8],
"name": item.get("name", ""),
"phone": item.get("phone", "").replace(" ", ""),
"address": item.get("address", ""),
"city": city,
"rating": item.get("rating", 0),
"website": item.get("website", ""),
"status": "new",
})
return results
except Exception as e:
logger.error(f"Gemini search error: {e}")
return _get_preset_prospects(query, city)
def _get_preset_prospects(query: str, city: str) -> list:
"""Fallback preset data for common Saudi sectors."""
presets = {
"clinics": [
{"name": "مجمع عيادات المدار لطب الأسنان", "phone": "+966114567890", "address": f"طريق الملك فهد، {city}"},
{"name": "عيادات ديرما للجلدية والتجميل", "phone": "+966112345678", "address": f"حي العليا، {city}"},
{"name": "مجمع الصفوة الطبي العام", "phone": "+966113456789", "address": f"حي السليمانية، {city}"},
{"name": "مركز المواعيد الطبي", "phone": "+966114567891", "address": f"شارع التحلية، {city}"},
{"name": "عيادات سيغال للتجميل", "phone": "+966112345679", "address": f"حي الملقا، {city}"},
],
}
results = []
for item in presets.get("clinics", []):
results.append({
"id": str(uuid.uuid4())[:8],
**item,
"city": city,
"rating": 4.2,
"website": "",
"status": "new",
})
return results
# ═══ Endpoints ═════════════════════════════════════════════
@router.post("/search")
async def search_prospects(query: ProspectQuery):
"""Search for business prospects using Google Maps + AI."""
logger.info(f"Searching: {query.query} in {query.city}")
results = await _search_google_maps(query.query, query.city, query.max_results)
# Store results
for r in results:
r["sector"] = query.sector
PROSPECTS[r["id"]] = r
return {
"query": query.query,
"city": query.city,
"total_found": len(results),
"prospects": results,
}
@router.post("/search-multi")
async def search_multi_queries(queries: List[str] = None, city: str = "الرياض", sector: str = "clinics"):
"""Search multiple queries at once."""
if not queries:
queries = [
"عيادة أسنان", "عيادة تجميل", "مجمع طبي",
"عيادة جلدية", "مركز طبي تخصصي",
"عيادة عيون", "مركز علاج طبيعي",
]
all_results = []
seen_phones = set()
for q in queries:
results = await _search_google_maps(q, city, 20)
for r in results:
if r["phone"] not in seen_phones:
r["sector"] = sector
all_results.append(r)
seen_phones.add(r["phone"])
PROSPECTS[r["id"]] = r
return {
"queries": queries,
"city": city,
"total_unique": len(all_results),
"prospects": all_results,
}
@router.get("/prospects")
async def list_all_prospects():
"""List all discovered prospects."""
return {
"total": len(PROSPECTS),
"prospects": list(PROSPECTS.values()),
}
@router.post("/prospect-and-outreach")
async def prospect_and_outreach(
query: str = "عيادة أسنان",
city: str = "الرياض",
sector: str = "clinics",
max_targets: int = 20,
auto_send: bool = False,
background_tasks: BackgroundTasks = None,
):
"""Search for prospects AND optionally launch outreach campaign."""
# Step 1: Find prospects
search_query = ProspectQuery(query=query, city=city, sector=sector, max_results=max_targets)
results = await _search_google_maps(query, city, max_targets)
for r in results:
r["sector"] = sector
PROSPECTS[r["id"]] = r
response = {
"phase": "prospecting",
"total_found": len(results),
"prospects": results,
"auto_send": auto_send,
}
if auto_send and results and background_tasks:
# Step 2: Auto-launch campaign
from app.api.v1.outreach_engine import (
BulkCampaignRequest, OutreachTarget, launch_campaign
)
targets = [
OutreachTarget(
phone=r["phone"],
company_name=r["name"],
city=r.get("city", city),
sector=sector,
)
for r in results if r.get("phone")
]
campaign_req = BulkCampaignRequest(
campaign_name=f"حملة {query} - {city}",
sector=sector,
targets=targets,
delay_seconds=45,
)
campaign_result = await launch_campaign(campaign_req, background_tasks)
response["campaign"] = campaign_result
response["phase"] = "outreach_launched"
return response