system-prompts-and-models-o.../dealix/auto_client_acquisition/v3/market_radar.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

125 lines
7.0 KiB
Python

"""Saudi Market Radar for Dealix v3."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from core._py_compat import UTC
from math import exp
from typing import Any
@dataclass(frozen=True)
class MarketSignal:
company: str
sector: str
city: str
signal_type: str
strength: float
days_old: int = 0
evidence: str = ""
def score(self) -> float:
freshness = exp(-self.days_old / 21)
sector_boost = {
"clinics": 1.20,
"real_estate": 1.15,
"logistics": 1.10,
"training": 1.08,
"hospitality": 1.05,
}.get(self.sector, 1.0)
return round(max(0.0, min(100.0, self.strength * freshness * sector_boost)), 2)
def why_now_ar(self) -> str:
labels = {
"hiring_sales": "الشركة توظف في المبيعات، وهذا غالباً يعني توسع أو ضغط على توليد الطلب.",
"new_branch": "يوجد مؤشر توسع/فرع جديد، وهذا وقت ممتاز لعرض نظام نمو أسرع.",
"booking_link": "لديهم مسار حجز واضح، ويمكن تحسين الردود والتحويل عبر واتساب.",
"website_change": "تغير في الموقع يدل على تحديث عرض أو حملة جديدة.",
"event_participation": "مشاركتهم في فعالية تعني استعداد أعلى لعلاقات وشراكات جديدة.",
"website_updated": "تحديث الموقع يعني حملة أو منتج جديد يستحق رسالة ذات سياق.",
"new_ad_activity": "نشاط إعلاني جديد يعني استثمار في الطلب.",
"new_funding": "تمويل جديد يعني نافذة شراء وتوسع.",
"tender_opportunity": "مناقصة أو فرصة توريد تفتح باب B2B مباشر.",
"review_spike": "تغير في التقييمات قد يعني ضغط تشغيل أو نمو حركة.",
"job_posts": "وظائف متعددة تشير إلى نمو تنظيمي.",
"crm_detected": "أثر تقني/CRM يعني نضج عمليات المبيعات.",
"whatsapp_heavy_business": "أعمال تعتمد واتساب بشكل كبير — قناة مناسبة لكن بموافقة.",
"slow_response_risk": "بطء الرد قد يعني تسريب فرص — فرصة لتحسين SLA.",
"competitor_campaign": "حركة منافس تستدعي رداً استراتيجياً وليس تقليداً أعمى.",
"new_product_launch": "إطلاق منتج يفتح محادثات شراكة أو توسعة.",
"new_partnership": "شراكة جديدة تدل على انفتاح على قنوات.",
}
return labels.get(self.signal_type, "يوجد مؤشر سوق يستحق المتابعة الآن.")
def to_dict(self) -> dict[str, Any]:
return {
"company": self.company,
"sector": self.sector,
"city": self.city,
"signal_type": self.signal_type,
"strength": self.strength,
"days_old": self.days_old,
"score": self.score(),
"evidence": self.evidence,
"why_now_ar": self.why_now_ar(),
}
def rank_opportunities(signals: list[MarketSignal], limit: int = 20) -> list[dict[str, Any]]:
ranked = sorted(signals, key=lambda item: item.score(), reverse=True)
return [item.to_dict() for item in ranked[:limit]]
def demo_signals() -> list[MarketSignal]:
return [
MarketSignal("عيادة نمو الرياض", "clinics", "Riyadh", "hiring_sales", 92, 2, "3 sales roles posted"),
MarketSignal("وسيط عقار جدة", "real_estate", "Jeddah", "new_branch", 85, 5, "new branch page"),
MarketSignal("أكاديمية تدريب الشرقية", "training", "Dammam", "booking_link", 80, 1, "public booking link"),
]
def signal_catalog() -> list[dict[str, Any]]:
"""Deterministic metadata for GTM/docs — confidence is illustrative until wired to data feeds."""
types = [
("hiring_sales", "توسع فريق المبيعات غالباً يعني ضغط على الأنابيب.", ["b2b_saas", "logistics"], 0.72),
("opening_branch", "فرع جديد = توسع جغرافي وشراء.", ["real_estate", "hospitality"], 0.68),
("website_updated", "تحديث الموقع = حملة أو تموضع جديد.", ["agencies", "b2b_saas"], 0.55),
("booking_link_found", "رابط حجز واضح يسهّل متابعة منظمة.", ["clinics", "training"], 0.7),
("new_ad_activity", "إعلانات جديدة = استثمار في الطلب.", ["restaurants", "real_estate"], 0.5),
("new_funding", "تمويل يفتح ميزانية ومبادرات.", ["b2b_saas"], 0.8),
("event_participation", "فعاليات = networking وفرص شراكة.", ["training", "agencies"], 0.62),
("new_partnership", "شراكة تدل على قنوات جديدة.", ["logistics", "construction"], 0.58),
("new_product_launch", "منتج جديد يحتاج رسائل تفعيل.", ["b2b_saas"], 0.65),
("tender_opportunity", "مناقصات تتطلب دقة وامتثال.", ["construction", "logistics"], 0.75),
("review_spike", "تقييمات متغيرة تستدعي متابعة تجربة.", ["restaurants", "hospitality"], 0.45),
("job_posts", "وظائف متعددة = نمو.", ["b2b_saas", "training"], 0.52),
("crm_detected", "نضج عمليات = فرصة لطبقة إيرادات.", ["b2b_saas"], 0.48),
("whatsapp_heavy_business", "اعتماد واتساب عالٍ — مناسب لكن بموافقة.", ["clinics", "real_estate"], 0.6),
("slow_response_risk", "بطء ردود = تسريب فرص.", ["agencies", "b2b_saas"], 0.5),
("competitor_campaign", "حركة منافس — رد استراتيجي.", ["b2b_saas"], 0.55),
]
out: list[dict[str, Any]] = []
for st, why, sectors, conf in types:
out.append(
{
"signal_type": st,
"why_it_matters_ar": why,
"applicable_sectors": sectors,
"suggested_message_angle_ar": "ركّز على «لماذا الآن» بدون مبالغة؛ اربط الإشارة بحل Dealix.",
"confidence_demo": conf,
"risk_compliance_notes_ar": "تأكد من opt-in قبل واتساب تسويقي؛ لا إرسال بارد.",
}
)
return out
def sector_heatmap(signals: list[MarketSignal]) -> list[dict[str, Any]]:
buckets: dict[str, list[float]] = {}
for signal in signals:
buckets.setdefault(signal.sector, []).append(signal.score())
return [
{"sector": sector, "avg_intent": round(sum(scores) / len(scores), 2), "signals": len(scores)}
for sector, scores in sorted(buckets.items(), key=lambda item: sum(item[1]) / len(item[1]), reverse=True)
]