system-prompts-and-models-o.../dealix/auto_client_acquisition/agents/intake.py
Dealix Builder e34cc729aa feat(dealix): py3.10/3.11 compat shim + 54 unit tests for business/innovation/ai
PROBLEM
The codebase used Python 3.11+ stdlib features (`from datetime import UTC`,
`from enum import StrEnum`) in 22 files, breaking local dev on Python 3.10
(Windows users) and any pytest run that imports the affected modules.

SOLUTION
1. New `core/_py_compat.py` providing UTC + StrEnum shims that:
   - On 3.11+ re-export the stdlib names (zero overhead)
   - On 3.10 fall back to `timezone.utc` and a (str, Enum) backport

2. All 22 affected files patched to import from the shim:
   - core/utils.py, core/config/models.py
   - api/routers/admin.py
   - auto_client_acquisition/{ai/model_router, agents/{intake,icp_matcher},
     v3/{memory,agents,compliance_os,market_radar},
     personal_operator/{operator,memory,launch_report},
     innovation/{proof_ledger_repo,command_feed_live}}.py
   - autonomous_growth/agents/sector_intel.py
   - dealix/{trust/{approval,tool_verification,policy},
     observability/cost_tracker,
     contracts/{evidence_pack,event_envelope,audit_log,decision},
     classifications/__init__,
     governance/approvals}.py

3. Three new test suites for previously-untested layers (54 tests):
   - tests/unit/test_business_suite.py — gtm_plan, launch_metrics,
     market_positioning, pricing_strategy, proof_pack, unit_economics,
     verticals (28 tests covering plan recommendation, performance fee,
     ROI math, account health grading, vertical playbook structure)
   - tests/unit/test_innovation_suite.py — aeo_radar, command_feed,
     deal_rooms, experiments, growth_missions, proof_ledger, ten_in_ten
     (18 tests covering deterministic reproducibility, card type taxonomy,
     pending-approval invariant, kill-mission visibility)
   - tests/unit/test_ai_model_router.py — ModelTask + get_model_route +
     estimate_model_cost_class + requires_guardrail (8 tests covering
     enum integrity, route round-trip, guardrail bool contract)

VERIFICATION
- ast.parse green on all 22 patched files
- pytest tests/unit/ → 477 passed, 2 skipped (provider smoke needs API keys)
  on Python 3.10.12 venv with project requirements installed
- No behavior change on 3.11+: the shim re-exports stdlib symbols

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:50:04 +03:00

183 lines
5.6 KiB
Python

"""
Intake Agent — captures leads from multiple sources and normalizes them.
وكيل الاستقبال — يلتقط العملاء من مصادر متعددة ويوحّد صيغتهم.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from core._py_compat import StrEnum
from typing import Any
from core.agents.base import BaseAgent
from core.utils import (
detect_locale,
generate_id,
hash_text,
normalize_email,
normalize_phone,
utcnow,
)
class LeadSource(StrEnum):
"""Lead source channels | مصادر العملاء."""
WEBSITE = "website"
WHATSAPP = "whatsapp"
EMAIL = "email"
REFERRAL = "referral"
LINKEDIN = "linkedin"
COLD_OUTREACH = "cold_outreach"
MANUAL = "manual"
API = "api"
class LeadStatus(StrEnum):
"""Lead stages through the funnel | مراحل العميل في القمع."""
NEW = "new"
QUALIFIED = "qualified"
DISCOVERY = "discovery"
PROPOSAL = "proposal"
NEGOTIATION = "negotiation"
WON = "won"
LOST = "lost"
DISQUALIFIED = "disqualified"
@dataclass
class Lead:
"""A captured lead | عميل محتمل ملتقط."""
id: str
source: LeadSource
company_name: str = ""
contact_name: str = ""
contact_email: str | None = None
contact_phone: str | None = None
contact_channel: str = ""
sector: str | None = None
company_size: str | None = None
region: str | None = None
budget: float | None = None
message: str | None = None
urgency_score: float = 0.0
fit_score: float = 0.0
status: LeadStatus = LeadStatus.NEW
pain_points: list[str] = field(default_factory=list)
locale: str = "ar"
created_at: datetime = field(default_factory=utcnow)
updated_at: datetime = field(default_factory=utcnow)
metadata: dict[str, Any] = field(default_factory=dict)
dedup_hash: str = ""
def to_dict(self) -> dict[str, Any]:
"""Serialize for storage / API response."""
return {
"id": self.id,
"source": self.source.value,
"company_name": self.company_name,
"contact_name": self.contact_name,
"contact_email": self.contact_email,
"contact_phone": self.contact_phone,
"contact_channel": self.contact_channel,
"sector": self.sector,
"company_size": self.company_size,
"region": self.region,
"budget": self.budget,
"message": self.message,
"urgency_score": self.urgency_score,
"fit_score": self.fit_score,
"status": self.status.value,
"pain_points": self.pain_points,
"locale": self.locale,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"metadata": self.metadata,
"dedup_hash": self.dedup_hash,
}
class IntakeAgent(BaseAgent):
"""
Receives raw lead payloads and produces normalized Lead objects.
Does: validation, phone/email normalization, locale detection, dedup hashing.
"""
name = "intake"
def __init__(self) -> None:
super().__init__()
self._seen_hashes: set[str] = set()
async def run(
self,
*,
payload: dict[str, Any],
source: LeadSource | str = LeadSource.WEBSITE,
**_: Any,
) -> Lead:
"""Normalize a raw payload into a Lead."""
if isinstance(source, str):
source = LeadSource(source)
company = str(payload.get("company") or payload.get("company_name") or "").strip()
name = str(payload.get("name") or payload.get("contact_name") or "").strip()
email = normalize_email(str(payload.get("email") or ""))
phone = normalize_phone(str(payload.get("phone") or ""))
message = str(payload.get("message") or "").strip() or None
locale = str(payload.get("locale") or "").strip()
if not locale:
locale = detect_locale(message or company or name)
contact_channel = email or phone or str(payload.get("channel") or source.value)
# Dedup based on (email or phone) + company
dedup_source = f"{email or phone or ''}|{company.lower()}"
dedup_hash = hash_text(dedup_source) if dedup_source.strip("|") else ""
is_duplicate = dedup_hash and dedup_hash in self._seen_hashes
if dedup_hash:
self._seen_hashes.add(dedup_hash)
lead = Lead(
id=generate_id("lead"),
source=source,
company_name=company,
contact_name=name,
contact_email=email,
contact_phone=phone,
contact_channel=contact_channel,
sector=payload.get("sector"),
company_size=payload.get("company_size"),
region=payload.get("region"),
budget=self._parse_float(payload.get("budget")),
message=message,
status=LeadStatus.NEW,
locale=locale,
dedup_hash=dedup_hash,
metadata={
"is_duplicate": is_duplicate,
"raw_payload": payload,
},
)
self.log.info(
"lead_intake",
lead_id=lead.id,
source=source.value,
company=company,
duplicate=is_duplicate,
)
return lead
@staticmethod
def _parse_float(value: Any) -> float | None:
if value is None or value == "":
return None
try:
return float(value)
except (TypeError, ValueError):
return None