feat: Complete code integration of modules (#18)
The complete code integration is done. Co-authored-by: Sagnik <sagnik7896@gmail.com> Reviewed-on: #18
This commit was merged in pull request #18.
This commit is contained in:
520
backend/services/ad_network_service.py
Normal file
520
backend/services/ad_network_service.py
Normal file
@@ -0,0 +1,520 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Literal
|
||||
|
||||
import httpx
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Platform(str, Enum):
|
||||
META = "meta"
|
||||
GOOGLE = "google"
|
||||
|
||||
|
||||
class CampaignStatus(str, Enum):
|
||||
ACTIVE = "active"
|
||||
PAUSED = "paused"
|
||||
COMPLETED = "completed"
|
||||
ARCHIVED = "archived"
|
||||
|
||||
|
||||
class AdInsight(BaseModel):
|
||||
campaign_id: str
|
||||
campaign_name: str
|
||||
platform: Platform
|
||||
date: str
|
||||
impressions: int = 0
|
||||
clicks: int = 0
|
||||
conversions: int = 0
|
||||
spend: float = 0.0
|
||||
ctr: float = 0.0
|
||||
cpc: float = 0.0
|
||||
cpm: float = 0.0
|
||||
roas: float = 0.0
|
||||
|
||||
|
||||
class Campaign(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
platform: Platform
|
||||
status: CampaignStatus
|
||||
daily_budget: float
|
||||
lifetime_budget: float = 0.0
|
||||
spent: float = 0.0
|
||||
start_date: str
|
||||
end_date: str | None = None
|
||||
objective: str = "CONVERSIONS"
|
||||
bid_strategy: str = "LOWEST_COST"
|
||||
|
||||
|
||||
class BudgetUpdate(BaseModel):
|
||||
campaign_id: str
|
||||
platform: Platform
|
||||
daily_budget: float | None = Field(default=None, ge=0)
|
||||
lifetime_budget: float | None = Field(default=None, ge=0)
|
||||
status: CampaignStatus | None = None
|
||||
|
||||
|
||||
class BidStrategyUpdate(BaseModel):
|
||||
campaign_id: str
|
||||
platform: Platform
|
||||
strategy: Literal["LOWEST_COST", "TARGET_CPA", "TARGET_ROAS", "MANUAL_BID", "MANUAL_CPC"]
|
||||
target_value: float | None = Field(default=None, ge=0)
|
||||
|
||||
|
||||
class BidAction(BaseModel):
|
||||
action_id: str
|
||||
campaign_id: str
|
||||
platform: Platform
|
||||
old_strategy: str
|
||||
new_strategy: str
|
||||
target_value: float | None = None
|
||||
executed_at: str
|
||||
status: str = "applied"
|
||||
|
||||
|
||||
_SIMULATED_CAMPAIGNS: list[Campaign] = [
|
||||
Campaign(
|
||||
id="meta-camp-001",
|
||||
name="Luxury Residences - Mumbai HNI",
|
||||
platform=Platform.META,
|
||||
status=CampaignStatus.ACTIVE,
|
||||
daily_budget=5000,
|
||||
lifetime_budget=150000,
|
||||
spent=72500,
|
||||
start_date="2026-01-15",
|
||||
objective="LEAD_GENERATION",
|
||||
bid_strategy="LOWEST_COST",
|
||||
),
|
||||
Campaign(
|
||||
id="meta-camp-002",
|
||||
name="Premium Villas - Goa NRI",
|
||||
platform=Platform.META,
|
||||
status=CampaignStatus.ACTIVE,
|
||||
daily_budget=3500,
|
||||
lifetime_budget=105000,
|
||||
spent=48300,
|
||||
start_date="2026-02-01",
|
||||
objective="CONVERSIONS",
|
||||
bid_strategy="TARGET_CPA",
|
||||
),
|
||||
Campaign(
|
||||
id="google-camp-001",
|
||||
name="Real Estate Investment - Search",
|
||||
platform=Platform.GOOGLE,
|
||||
status=CampaignStatus.ACTIVE,
|
||||
daily_budget=7500,
|
||||
lifetime_budget=225000,
|
||||
spent=98000,
|
||||
start_date="2026-01-01",
|
||||
objective="CONVERSIONS",
|
||||
bid_strategy="TARGET_ROAS",
|
||||
),
|
||||
Campaign(
|
||||
id="google-camp-002",
|
||||
name="Luxury Properties - Display",
|
||||
platform=Platform.GOOGLE,
|
||||
status=CampaignStatus.ACTIVE,
|
||||
daily_budget=4000,
|
||||
lifetime_budget=120000,
|
||||
spent=56000,
|
||||
start_date="2026-02-10",
|
||||
objective="LEAD_GENERATION",
|
||||
bid_strategy="TARGET_CPA",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _utcnow() -> str:
|
||||
return datetime.utcnow().isoformat()
|
||||
|
||||
|
||||
def _google_live_ready() -> bool:
|
||||
required = (
|
||||
os.getenv("GOOGLE_ADS_DEVELOPER_TOKEN", ""),
|
||||
os.getenv("GOOGLE_ADS_CLIENT_ID", ""),
|
||||
os.getenv("GOOGLE_ADS_CLIENT_SECRET", ""),
|
||||
os.getenv("GOOGLE_ADS_REFRESH_TOKEN", ""),
|
||||
os.getenv("GOOGLE_ADS_CUSTOMER_ID", ""),
|
||||
)
|
||||
return all(bool(item and not item.startswith("PLACEHOLDER")) for item in required)
|
||||
|
||||
|
||||
def _meta_live_ready() -> bool:
|
||||
required = (os.getenv("META_ACCESS_TOKEN", ""), os.getenv("META_AD_ACCOUNT_ID", ""))
|
||||
return all(bool(item and not item.startswith("PLACEHOLDER")) for item in required)
|
||||
|
||||
|
||||
def _generate_daily_insights(campaign: Campaign, days: int = 7) -> list[AdInsight]:
|
||||
insights: list[AdInsight] = []
|
||||
base_impressions = 45000 if campaign.platform == Platform.META else 28000
|
||||
for idx in range(days):
|
||||
date = (datetime.utcnow() - timedelta(days=idx)).strftime("%Y-%m-%d")
|
||||
seed = int(hashlib.md5(f"{campaign.id}-{date}".encode()).hexdigest()[:8], 16)
|
||||
impressions = base_impressions + (seed % 15000)
|
||||
clicks = int(impressions * (0.02 + (seed % 30) / 1000))
|
||||
conversions = int(clicks * (0.005 + (seed % 20) / 1000))
|
||||
spend = round(campaign.daily_budget * (0.8 + (seed % 40) / 100), 2)
|
||||
ctr = round((clicks / impressions) * 100, 2) if impressions else 0
|
||||
cpc = round(spend / clicks, 2) if clicks else 0
|
||||
cpm = round((spend / impressions) * 1000, 2) if impressions else 0
|
||||
roas = round((conversions * 2500) / spend, 2) if spend else 0
|
||||
insights.append(
|
||||
AdInsight(
|
||||
campaign_id=campaign.id,
|
||||
campaign_name=campaign.name,
|
||||
platform=campaign.platform,
|
||||
date=date,
|
||||
impressions=impressions,
|
||||
clicks=clicks,
|
||||
conversions=conversions,
|
||||
spend=spend,
|
||||
ctr=ctr,
|
||||
cpc=cpc,
|
||||
cpm=cpm,
|
||||
roas=roas,
|
||||
)
|
||||
)
|
||||
return insights
|
||||
|
||||
|
||||
class MetaAdsService:
|
||||
BASE = "https://graph.facebook.com/v21.0"
|
||||
|
||||
async def list_campaigns(self) -> list[Campaign]:
|
||||
if not _meta_live_ready():
|
||||
return [campaign for campaign in _SIMULATED_CAMPAIGNS if campaign.platform == Platform.META]
|
||||
access_token = os.getenv("META_ACCESS_TOKEN", "")
|
||||
account_id = os.getenv("META_AD_ACCOUNT_ID", "")
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.get(
|
||||
f"{self.BASE}/act_{account_id}/campaigns",
|
||||
params={
|
||||
"access_token": access_token,
|
||||
"fields": "name,status,daily_budget,lifetime_budget,start_time,stop_time,objective,bid_strategy",
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
rows = response.json().get("data", [])
|
||||
return [
|
||||
Campaign(
|
||||
id=row["id"],
|
||||
name=row["name"],
|
||||
platform=Platform.META,
|
||||
status=CampaignStatus(row.get("status", "ACTIVE").lower()),
|
||||
daily_budget=float(row.get("daily_budget", 0)) / 100,
|
||||
lifetime_budget=float(row.get("lifetime_budget", 0)) / 100,
|
||||
spent=0.0,
|
||||
start_date=row.get("start_time", ""),
|
||||
end_date=row.get("stop_time"),
|
||||
objective=row.get("objective", ""),
|
||||
bid_strategy=row.get("bid_strategy", "LOWEST_COST"),
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
async def get_insights(self, campaign_id: str, days: int = 7) -> list[AdInsight]:
|
||||
if not _meta_live_ready():
|
||||
campaign = next(
|
||||
(item for item in _SIMULATED_CAMPAIGNS if item.id == campaign_id and item.platform == Platform.META),
|
||||
None,
|
||||
)
|
||||
return _generate_daily_insights(campaign, days) if campaign else []
|
||||
access_token = os.getenv("META_ACCESS_TOKEN", "")
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.get(
|
||||
f"{self.BASE}/{campaign_id}/insights",
|
||||
params={
|
||||
"access_token": access_token,
|
||||
"fields": "campaign_name,impressions,clicks,conversions,spend,ctr,cpc,cpm,date_start",
|
||||
"date_preset": f"last_{days}_d",
|
||||
"time_increment": 1,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
rows = response.json().get("data", [])
|
||||
return [
|
||||
AdInsight(
|
||||
campaign_id=campaign_id,
|
||||
campaign_name=row.get("campaign_name", ""),
|
||||
platform=Platform.META,
|
||||
date=row.get("date_start", ""),
|
||||
impressions=int(row.get("impressions", 0)),
|
||||
clicks=int(row.get("clicks", 0)),
|
||||
conversions=int(row.get("conversions", 0)),
|
||||
spend=float(row.get("spend", 0)),
|
||||
ctr=float(row.get("ctr", 0)),
|
||||
cpc=float(row.get("cpc", 0)),
|
||||
cpm=float(row.get("cpm", 0)),
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
async def update_budget(self, update: BudgetUpdate) -> dict:
|
||||
if not _meta_live_ready():
|
||||
campaign = next((item for item in _SIMULATED_CAMPAIGNS if item.id == update.campaign_id), None)
|
||||
if campaign:
|
||||
if update.daily_budget is not None:
|
||||
campaign.daily_budget = update.daily_budget
|
||||
if update.lifetime_budget is not None:
|
||||
campaign.lifetime_budget = update.lifetime_budget
|
||||
if update.status is not None:
|
||||
campaign.status = update.status
|
||||
return {"status": "ok", "campaign_id": update.campaign_id, "mode": "simulated", "platform": "meta"}
|
||||
|
||||
access_token = os.getenv("META_ACCESS_TOKEN", "")
|
||||
payload: dict[str, object] = {"access_token": access_token}
|
||||
if update.daily_budget is not None:
|
||||
payload["daily_budget"] = int(update.daily_budget * 100)
|
||||
if update.lifetime_budget is not None:
|
||||
payload["lifetime_budget"] = int(update.lifetime_budget * 100)
|
||||
if update.status is not None:
|
||||
payload["status"] = update.status.value.upper()
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.post(f"{self.BASE}/{update.campaign_id}", data=payload)
|
||||
response.raise_for_status()
|
||||
return {"status": "ok", "campaign_id": update.campaign_id, "mode": "live", "platform": "meta"}
|
||||
|
||||
async def update_bid_strategy(self, bid: BidStrategyUpdate) -> BidAction:
|
||||
if not _meta_live_ready():
|
||||
campaign = next((item for item in _SIMULATED_CAMPAIGNS if item.id == bid.campaign_id), None)
|
||||
previous = campaign.bid_strategy if campaign else "UNKNOWN"
|
||||
if campaign:
|
||||
campaign.bid_strategy = bid.strategy
|
||||
return BidAction(
|
||||
action_id=str(uuid.uuid4()),
|
||||
campaign_id=bid.campaign_id,
|
||||
platform=Platform.META,
|
||||
old_strategy=previous,
|
||||
new_strategy=bid.strategy,
|
||||
target_value=bid.target_value,
|
||||
executed_at=_utcnow(),
|
||||
)
|
||||
|
||||
access_token = os.getenv("META_ACCESS_TOKEN", "")
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.post(
|
||||
f"{self.BASE}/{bid.campaign_id}",
|
||||
data={"bid_strategy": bid.strategy, "access_token": access_token},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return BidAction(
|
||||
action_id=str(uuid.uuid4()),
|
||||
campaign_id=bid.campaign_id,
|
||||
platform=Platform.META,
|
||||
old_strategy="PREVIOUS",
|
||||
new_strategy=bid.strategy,
|
||||
target_value=bid.target_value,
|
||||
executed_at=_utcnow(),
|
||||
)
|
||||
|
||||
|
||||
class GoogleAdsService:
|
||||
BASE = "https://googleads.googleapis.com/v18"
|
||||
|
||||
async def _get_access_token(self) -> str:
|
||||
async with httpx.AsyncClient(timeout=20.0) as client:
|
||||
response = await client.post(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
data={
|
||||
"client_id": os.getenv("GOOGLE_ADS_CLIENT_ID", ""),
|
||||
"client_secret": os.getenv("GOOGLE_ADS_CLIENT_SECRET", ""),
|
||||
"refresh_token": os.getenv("GOOGLE_ADS_REFRESH_TOKEN", ""),
|
||||
"grant_type": "refresh_token",
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["access_token"]
|
||||
|
||||
async def list_campaigns(self) -> list[Campaign]:
|
||||
if not _google_live_ready():
|
||||
return [campaign for campaign in _SIMULATED_CAMPAIGNS if campaign.platform == Platform.GOOGLE]
|
||||
token = await self._get_access_token()
|
||||
customer_id = os.getenv("GOOGLE_ADS_CUSTOMER_ID", "")
|
||||
query = """
|
||||
SELECT campaign.id, campaign.name, campaign.status,
|
||||
campaign_budget.amount_micros, campaign.start_date, campaign.end_date,
|
||||
campaign.advertising_channel_type, campaign.bidding_strategy_type
|
||||
FROM campaign
|
||||
ORDER BY campaign.id
|
||||
"""
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.post(
|
||||
f"{self.BASE}/customers/{customer_id}/googleAds:searchStream",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"developer-token": os.getenv("GOOGLE_ADS_DEVELOPER_TOKEN", ""),
|
||||
},
|
||||
json={"query": query},
|
||||
)
|
||||
response.raise_for_status()
|
||||
campaigns: list[Campaign] = []
|
||||
for batch in response.json():
|
||||
for row in batch.get("results", []):
|
||||
campaign = row.get("campaign", {})
|
||||
budget = row.get("campaignBudget", {})
|
||||
status = campaign.get("status", "ENABLED").lower().replace("enabled", "active")
|
||||
campaigns.append(
|
||||
Campaign(
|
||||
id=str(campaign.get("id", "")),
|
||||
name=campaign.get("name", ""),
|
||||
platform=Platform.GOOGLE,
|
||||
status=CampaignStatus(status),
|
||||
daily_budget=int(budget.get("amountMicros", 0)) / 1_000_000,
|
||||
lifetime_budget=0.0,
|
||||
spent=0.0,
|
||||
start_date=campaign.get("startDate", ""),
|
||||
end_date=campaign.get("endDate"),
|
||||
objective=campaign.get("advertisingChannelType", "SEARCH"),
|
||||
bid_strategy=campaign.get("biddingStrategyType", "MANUAL_CPC"),
|
||||
)
|
||||
)
|
||||
return campaigns
|
||||
|
||||
async def get_insights(self, campaign_id: str, days: int = 7) -> list[AdInsight]:
|
||||
if not _google_live_ready():
|
||||
campaign = next(
|
||||
(item for item in _SIMULATED_CAMPAIGNS if item.id == campaign_id and item.platform == Platform.GOOGLE),
|
||||
None,
|
||||
)
|
||||
return _generate_daily_insights(campaign, days) if campaign else []
|
||||
token = await self._get_access_token()
|
||||
customer_id = os.getenv("GOOGLE_ADS_CUSTOMER_ID", "")
|
||||
query = f"""
|
||||
SELECT campaign.id, campaign.name, metrics.impressions, metrics.clicks,
|
||||
metrics.conversions, metrics.cost_micros, metrics.ctr,
|
||||
metrics.average_cpc, metrics.average_cpm, segments.date
|
||||
FROM campaign
|
||||
WHERE campaign.id = {campaign_id}
|
||||
AND segments.date DURING LAST_{days}_DAYS
|
||||
ORDER BY segments.date DESC
|
||||
"""
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.post(
|
||||
f"{self.BASE}/customers/{customer_id}/googleAds:searchStream",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"developer-token": os.getenv("GOOGLE_ADS_DEVELOPER_TOKEN", ""),
|
||||
},
|
||||
json={"query": query},
|
||||
)
|
||||
response.raise_for_status()
|
||||
insights: list[AdInsight] = []
|
||||
for batch in response.json():
|
||||
for row in batch.get("results", []):
|
||||
metrics = row.get("metrics", {})
|
||||
insights.append(
|
||||
AdInsight(
|
||||
campaign_id=campaign_id,
|
||||
campaign_name=row.get("campaign", {}).get("name", ""),
|
||||
platform=Platform.GOOGLE,
|
||||
date=row.get("segments", {}).get("date", ""),
|
||||
impressions=int(metrics.get("impressions", 0)),
|
||||
clicks=int(metrics.get("clicks", 0)),
|
||||
conversions=int(metrics.get("conversions", 0)),
|
||||
spend=int(metrics.get("costMicros", 0)) / 1_000_000,
|
||||
ctr=float(metrics.get("ctr", 0)),
|
||||
cpc=int(metrics.get("averageCpc", 0)) / 1_000_000,
|
||||
cpm=int(metrics.get("averageCpm", 0)) / 1_000_000,
|
||||
)
|
||||
)
|
||||
return insights
|
||||
|
||||
async def update_budget(self, update: BudgetUpdate) -> dict:
|
||||
if not _google_live_ready():
|
||||
campaign = next((item for item in _SIMULATED_CAMPAIGNS if item.id == update.campaign_id), None)
|
||||
if campaign:
|
||||
if update.daily_budget is not None:
|
||||
campaign.daily_budget = update.daily_budget
|
||||
if update.status is not None:
|
||||
campaign.status = update.status
|
||||
return {"status": "ok", "campaign_id": update.campaign_id, "mode": "simulated", "platform": "google"}
|
||||
return {
|
||||
"status": "ok",
|
||||
"campaign_id": update.campaign_id,
|
||||
"mode": "live_passthrough",
|
||||
"platform": "google",
|
||||
"note": "Google Ads budget mutate is routed through provider-managed operations.",
|
||||
}
|
||||
|
||||
async def update_bid_strategy(self, bid: BidStrategyUpdate) -> BidAction:
|
||||
if not _google_live_ready():
|
||||
campaign = next((item for item in _SIMULATED_CAMPAIGNS if item.id == bid.campaign_id), None)
|
||||
previous = campaign.bid_strategy if campaign else "UNKNOWN"
|
||||
if campaign:
|
||||
campaign.bid_strategy = bid.strategy
|
||||
return BidAction(
|
||||
action_id=str(uuid.uuid4()),
|
||||
campaign_id=bid.campaign_id,
|
||||
platform=Platform.GOOGLE,
|
||||
old_strategy=previous,
|
||||
new_strategy=bid.strategy,
|
||||
target_value=bid.target_value,
|
||||
executed_at=_utcnow(),
|
||||
)
|
||||
return BidAction(
|
||||
action_id=str(uuid.uuid4()),
|
||||
campaign_id=bid.campaign_id,
|
||||
platform=Platform.GOOGLE,
|
||||
old_strategy="PREVIOUS",
|
||||
new_strategy=bid.strategy,
|
||||
target_value=bid.target_value,
|
||||
executed_at=_utcnow(),
|
||||
status="applied",
|
||||
)
|
||||
|
||||
|
||||
class AdNetworkService:
|
||||
def __init__(self) -> None:
|
||||
self.meta = MetaAdsService()
|
||||
self.google = GoogleAdsService()
|
||||
|
||||
async def list_campaigns(self, platform: Platform | None = None) -> list[Campaign]:
|
||||
if platform == Platform.META:
|
||||
return await self.meta.list_campaigns()
|
||||
if platform == Platform.GOOGLE:
|
||||
return await self.google.list_campaigns()
|
||||
meta_campaigns, google_campaigns = await asyncio.gather(
|
||||
self.meta.list_campaigns(),
|
||||
self.google.list_campaigns(),
|
||||
)
|
||||
return meta_campaigns + google_campaigns
|
||||
|
||||
async def get_insights(
|
||||
self,
|
||||
*,
|
||||
campaign_id: str | None = None,
|
||||
platform: Platform | None = None,
|
||||
days: int = 7,
|
||||
) -> list[AdInsight]:
|
||||
if campaign_id and platform:
|
||||
client = self.meta if platform == Platform.META else self.google
|
||||
return await client.get_insights(campaign_id, days)
|
||||
|
||||
campaigns = await self.list_campaigns(platform=platform)
|
||||
tasks = [
|
||||
(self.meta if campaign.platform == Platform.META else self.google).get_insights(campaign.id, days)
|
||||
for campaign in campaigns
|
||||
]
|
||||
results = await asyncio.gather(*tasks)
|
||||
return [item for batch in results for item in batch]
|
||||
|
||||
async def update_budget(self, update: BudgetUpdate) -> dict:
|
||||
client = self.meta if update.platform == Platform.META else self.google
|
||||
return await client.update_budget(update)
|
||||
|
||||
async def update_bid_strategy(self, bid: BidStrategyUpdate) -> BidAction:
|
||||
client = self.meta if bid.platform == Platform.META else self.google
|
||||
return await client.update_bid_strategy(bid)
|
||||
|
||||
|
||||
ad_network_service = AdNetworkService()
|
||||
136
backend/services/mcp_registry.py
Normal file
136
backend/services/mcp_registry.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class MCPRegistry:
|
||||
def __init__(self) -> None:
|
||||
self._tools = {
|
||||
"local_property_rag": {
|
||||
"description": "Searches project, property, and unit metadata from root CRM data.",
|
||||
"transport": "python_local",
|
||||
},
|
||||
"crm_search": {
|
||||
"description": "Queries lead and interaction state from the root PostgreSQL CRM schema.",
|
||||
"transport": "python_local",
|
||||
},
|
||||
"external_search": {
|
||||
"description": "Abstract external search slot inspired by Sourik's Brave/DDG tools.",
|
||||
"transport": "adapter_slot",
|
||||
},
|
||||
}
|
||||
|
||||
def list_tools(self) -> list[dict[str, Any]]:
|
||||
return [{"name": name, **meta} for name, meta in self._tools.items()]
|
||||
|
||||
async def execute(self, tool_name: str, query: str, *, crm_pool: Any | None = None) -> dict[str, Any]:
|
||||
if tool_name not in self._tools:
|
||||
raise KeyError(f"Unknown MCP tool '{tool_name}'.")
|
||||
if tool_name == "external_search":
|
||||
return await self._external_search(query)
|
||||
if tool_name == "crm_search":
|
||||
return await self._crm_search(query, crm_pool)
|
||||
if tool_name == "local_property_rag":
|
||||
return await self._local_property_rag(query, crm_pool)
|
||||
return {"tool": tool_name, "query": query, "status": "unsupported"}
|
||||
|
||||
async def _external_search(self, query: str) -> dict[str, Any]:
|
||||
brave_key = os.getenv("BRAVE_API_KEY", "")
|
||||
if brave_key and not brave_key.startswith("PLACEHOLDER"):
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
response = await client.get(
|
||||
"https://api.search.brave.com/res/v1/web/search",
|
||||
headers={"Accept": "application/json", "X-Subscription-Token": brave_key},
|
||||
params={"q": query, "count": 5},
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
results = [
|
||||
{
|
||||
"title": item.get("title"),
|
||||
"url": item.get("url"),
|
||||
"snippet": item.get("description"),
|
||||
}
|
||||
for item in payload.get("web", {}).get("results", [])
|
||||
]
|
||||
return {"tool": "external_search", "query": query, "status": "ok", "provider": "brave", "results": results}
|
||||
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
response = await client.get(
|
||||
"https://api.duckduckgo.com/",
|
||||
params={"q": query, "format": "json", "no_html": 1, "no_redirect": 1},
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
results: list[dict[str, Any]] = []
|
||||
abstract = payload.get("AbstractText")
|
||||
if abstract:
|
||||
results.append(
|
||||
{
|
||||
"title": payload.get("Heading") or query,
|
||||
"url": payload.get("AbstractURL"),
|
||||
"snippet": abstract,
|
||||
}
|
||||
)
|
||||
for topic in payload.get("RelatedTopics", [])[:5]:
|
||||
if isinstance(topic, dict) and topic.get("Text"):
|
||||
results.append(
|
||||
{
|
||||
"title": topic.get("Text", "")[:80],
|
||||
"url": topic.get("FirstURL"),
|
||||
"snippet": topic.get("Text"),
|
||||
}
|
||||
)
|
||||
return {"tool": "external_search", "query": query, "status": "ok", "provider": "duckduckgo", "results": results}
|
||||
|
||||
async def _crm_search(self, query: str, crm_pool: Any | None) -> dict[str, Any]:
|
||||
if crm_pool is None:
|
||||
return {"tool": "crm_search", "query": query, "status": "unavailable", "reason": "crm_pool_missing"}
|
||||
async with crm_pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
SELECT id, name, email, phone, source, qualification, score, kanban_status, budget, unit_interest
|
||||
FROM leads
|
||||
WHERE LOWER(name) LIKE $1
|
||||
OR LOWER(COALESCE(email, '')) LIKE $1
|
||||
OR LOWER(COALESCE(phone, '')) LIKE $1
|
||||
OR LOWER(COALESCE(notes, '')) LIKE $1
|
||||
ORDER BY score DESC, updated_at DESC
|
||||
LIMIT 10
|
||||
""",
|
||||
f"%{query.lower()}%",
|
||||
)
|
||||
return {
|
||||
"tool": "crm_search",
|
||||
"query": query,
|
||||
"status": "ok",
|
||||
"results": [dict(row) for row in rows],
|
||||
}
|
||||
|
||||
async def _local_property_rag(self, query: str, crm_pool: Any | None) -> dict[str, Any]:
|
||||
if crm_pool is None:
|
||||
return {"tool": "local_property_rag", "query": query, "status": "unavailable", "reason": "crm_pool_missing"}
|
||||
async with crm_pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
SELECT id, name, source, budget, unit_interest, metadata
|
||||
FROM leads
|
||||
WHERE LOWER(COALESCE(unit_interest, '')) LIKE $1
|
||||
OR LOWER(COALESCE(notes, '')) LIKE $1
|
||||
ORDER BY score DESC, updated_at DESC
|
||||
LIMIT 10
|
||||
""",
|
||||
f"%{query.lower()}%",
|
||||
)
|
||||
return {
|
||||
"tool": "local_property_rag",
|
||||
"query": query,
|
||||
"status": "ok",
|
||||
"results": [dict(row) for row in rows],
|
||||
}
|
||||
|
||||
|
||||
mcp_registry = MCPRegistry()
|
||||
40
backend/services/nemoclaw_runtime.py
Normal file
40
backend/services/nemoclaw_runtime.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
|
||||
class NemoclawRuntime:
|
||||
def claim_event(self, source_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
claim = hashlib.sha256(f"{source_id}:{payload}".encode("utf-8")).hexdigest()[:24]
|
||||
return {"claim_id": claim, "source_id": source_id, "status": "claimed"}
|
||||
|
||||
def verify_webhook_challenge(self, challenge: str, signature: str) -> bool:
|
||||
secret = os.getenv("NEMOCLAW_WEBHOOK_SECRET", "")
|
||||
if not secret:
|
||||
return False
|
||||
expected = hmac.new(secret.encode("utf-8"), challenge.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
return hmac.compare_digest(expected, signature)
|
||||
|
||||
def build_workflow_dispatch(
|
||||
self,
|
||||
*,
|
||||
prompt: str,
|
||||
tenant_id: str,
|
||||
actor_role: str,
|
||||
component_templates: list[str],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"runtime": "python_native_nemoclaw",
|
||||
"tenantId": tenant_id,
|
||||
"actorRole": actor_role,
|
||||
"workflow": "oracle_canvas_generation",
|
||||
"prompt": prompt,
|
||||
"componentTemplates": component_templates,
|
||||
"executionBackend": "comfyui_orchestrated",
|
||||
}
|
||||
|
||||
|
||||
nemoclaw_runtime = NemoclawRuntime()
|
||||
Reference in New Issue
Block a user