mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 15:29:36 +00:00
297 lines
10 KiB
Python
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
|