import httpx import logging from datetime import datetime, timedelta, timezone from typing import Dict, Any, Optional from app.config import get_settings logger = logging.getLogger("dealix.salesforce") settings = get_settings() class SalesforceAgentforceSync: """ Layer 4: Deep Integration with Salesforce Agentforce 360. Treats Salesforce structured data as external ground truth and allows Native Agents (Agentforce) to interact with Dealix outputs. """ def __init__(self): domain = settings.SALESFORCE_DOMAIN.strip() or "login.salesforce.com" self.base_auth_url = f"https://{domain}" self.api_url = f"{self.base_auth_url}/services/data/{settings.SALESFORCE_API_VERSION}" self.client_id = settings.SALESFORCE_CLIENT_ID self.client_secret = settings.SALESFORCE_CLIENT_SECRET self.refresh_token = settings.SALESFORCE_REFRESH_TOKEN self.access_token = settings.SALESFORCE_ACCESS_TOKEN self.token_expiry: Optional[datetime] = None async def _refresh_access_token(self) -> bool: if not (self.client_id and self.client_secret and self.refresh_token): return False try: async with httpx.AsyncClient(timeout=20) as client: resp = await client.post( f"{self.base_auth_url}/services/oauth2/token", data={ "grant_type": "refresh_token", "client_id": self.client_id, "client_secret": self.client_secret, "refresh_token": self.refresh_token, }, ) resp.raise_for_status() data = resp.json() self.access_token = data.get("access_token", "") self.token_expiry = datetime.now(timezone.utc) + timedelta(minutes=50) instance_url = data.get("instance_url") if instance_url: self.api_url = f"{instance_url}/services/data/{settings.SALESFORCE_API_VERSION}" return bool(self.access_token) except Exception as e: logger.error(f"Salesforce token refresh failed: {e}") return False async def _ensure_token(self) -> bool: if self.access_token and self.token_expiry and datetime.now(timezone.utc) < self.token_expiry: return True return await self._refresh_access_token() async def get_account_360(self, account_name: str) -> Dict[str, Any]: if not await self._ensure_token(): return { "account_name": account_name, "mode": "mock", "opportunities": [], "note": "Salesforce OAuth not configured", } soql = f"SELECT Id, Name, Industry FROM Account WHERE Name = '{account_name}' LIMIT 1" query_url = f"{self.api_url}/query" try: async with httpx.AsyncClient(timeout=20) as client: resp = await client.get( query_url, params={"q": soql}, headers={"Authorization": f"Bearer {self.access_token}"}, ) resp.raise_for_status() records = resp.json().get("records", []) return {"account_name": account_name, "mode": "live", "records": records} except Exception as e: logger.error(f"Salesforce Account 360 fetch failed: {e}") return {"account_name": account_name, "mode": "error", "error": str(e)} async def sync_deal(self, deal_state: Dict[str, Any]) -> bool: """ Synchronizes Dealix's final deal state with Salesforce Pipeline Management Agent. """ company = deal_state.get("company_name", "Unknown") stage = deal_state.get("deal_stage", "Prospecting") action = deal_state.get("next_action_payload", "") payload = { "Name": f"Dealix Auto: {company}", "StageName": stage, "CloseDate": "2026-12-31", "Description": f"Generated by Dealix Autonomous OS.\n\nLatest AI Action: {action[:200]}..." } headers = { "Authorization": f"Bearer {self.access_token}", "Content-Type": "application/json" } if not await self._ensure_token(): logger.info(f"Mock Sync to Salesforce Agentforce: {company} -> {stage}") return True try: async with httpx.AsyncClient() as client: resp = await client.post(f"{self.api_url}/sobjects/Opportunity/", json=payload, headers=headers) resp.raise_for_status() logger.info(f"Salesforce Sync Successful: {resp.json().get('id')}") return True except Exception as e: logger.error(f"Salesforce Agentforce Sync Failed: {e}") return False # Singleton agentforce_service = SalesforceAgentforceSync()