| from datetime import datetime, timedelta, timezone |
| import logging |
| from typing import Any, Dict, List |
| from sales.models import Deal, DealStage |
| from sqlalchemy.orm import Session |
|
|
| from core.automation_settings import get_automation_settings |
| from core.websockets import manager |
|
|
| logger = logging.getLogger(__name__) |
|
|
| try: |
| from integrations.atom_communication_ingestion_pipeline import ( |
| CommunicationAppType, |
| ingestion_pipeline, |
| ) |
| INGESTION_AVAILABLE = True |
| except ImportError: |
| INGESTION_AVAILABLE = False |
| logger.warning("Ingestion pipeline not available. Sales memory will be disabled.") |
|
|
| class SalesIntelligence: |
| """ |
| Analyzes deals to detect risks and health scores. |
| """ |
| def __init__(self, db: Session): |
| self.db = db |
| self.settings = get_automation_settings() |
|
|
| async def analyze_deal_health(self, deal: Deal) -> Dict[str, Any]: |
| """ |
| Calculate a composite health score (0-100) for a deal. |
| """ |
| if not self.settings.is_sales_enabled(): |
| return {"health_score": 0, "risk_level": "disabled"} |
|
|
| health = 70.0 |
| risks = [] |
|
|
| |
| now = datetime.now(timezone.utc) |
| days_in_stage = (now - (deal.updated_at or deal.created_at)).days |
| if days_in_stage > 14: |
| health -= 15 |
| risks.append("Deal stalled in stage for > 14 days") |
| |
| |
| if deal.last_engagement_at: |
| days_since_engagement = (now - deal.last_engagement_at).days |
| if days_since_engagement > 7: |
| health -= 20 |
| risks.append("No engagement in over a week") |
| else: |
| health -= 30 |
| risks.append("No recorded engagement") |
|
|
| |
| if deal.value > 10000 and deal.probability < 0.3: |
| health -= 10 |
| risks.append("High value deal with low win probability") |
|
|
| |
| deal.health_score = max(0, min(100, health)) |
| |
| if deal.health_score < 40: |
| deal.risk_level = "high" |
| elif deal.health_score < 70: |
| deal.risk_level = "medium" |
| else: |
| deal.risk_level = "low" |
|
|
| self.db.commit() |
|
|
| |
| try: |
| await manager.broadcast(f"workspace:{deal.workspace_id}", { |
| "type": "deal_update", |
| "workspace_id": deal.workspace_id, |
| "data": { |
| "id": deal.id, |
| "name": deal.name, |
| "health_score": deal.health_score, |
| "risk_level": deal.risk_level, |
| "risks": risks |
| } |
| }) |
| except Exception as e: |
| logger.error(f"Failed to broadcast deal update: {e}") |
|
|
| |
| if INGESTION_AVAILABLE: |
| try: |
| ingestion_pipeline.ingest_message( |
| CommunicationAppType.CRM_DEAL.value, |
| { |
| "id": deal.id, |
| "timestamp": datetime.now(timezone.utc).isoformat(), |
| "sender": "Sales Intelligence", |
| "subject": f"Deal Health: {deal.name}", |
| "content": f"Deal: {deal.name}. Health Score: {deal.health_score}. Risk Level: {deal.risk_level}. Risks: {', '.join(risks)}", |
| "metadata": { |
| "deal_id": deal.id, |
| "workspace_id": deal.workspace_id, |
| "health_score": deal.health_score, |
| "risk_level": deal.risk_level, |
| "risks": risks |
| } |
| } |
| ) |
| except Exception as e: |
| logger.error(f"Failed to ingest deal health into LanceDB: {e}") |
|
|
| return { |
| "health_score": deal.health_score, |
| "risk_level": deal.risk_level, |
| "risks": risks |
| } |
|
|
| def get_pipeline_forecast(self, workspace_id: str) -> Dict[str, Any]: |
| """ |
| Generate a simple weighted forecast. |
| """ |
| deals = self.db.query(Deal).filter( |
| Deal.workspace_id == workspace_id, |
| Deal.stage.notin_([DealStage.CLOSED_WON, DealStage.CLOSED_LOST]) |
| ).all() |
|
|
| total_weighted = sum(d.value * (d.probability or 0.5) for d in deals) |
| total_unweighted = sum(d.value for d in deals) |
|
|
| return { |
| "weighted_pipeline": total_weighted, |
| "unweighted_pipeline": total_unweighted, |
| "deal_count": len(deals) |
| } |
|
|