diff --git a/salesflow-saas/backend/dealix_gtm_os/agents/campaign_orchestrator_agent.py b/salesflow-saas/backend/dealix_gtm_os/agents/campaign_orchestrator_agent.py new file mode 100644 index 00000000..b4fbbc54 --- /dev/null +++ b/salesflow-saas/backend/dealix_gtm_os/agents/campaign_orchestrator_agent.py @@ -0,0 +1,8 @@ +from dealix_gtm_os.agents.base_agent import BaseAgent + +class CampaignOrchestratorAgentAgent(BaseAgent): + name = "campaign_orchestrator_agent" + description = "campaign orchestrator agent" + + async def run(self, input_data: dict) -> dict: + return {"status": "stub", "agent": self.name, "note": "Connect real tools in production"} diff --git a/salesflow-saas/backend/dealix_gtm_os/agents/competitor_intelligence_agent.py b/salesflow-saas/backend/dealix_gtm_os/agents/competitor_intelligence_agent.py new file mode 100644 index 00000000..ea4d3180 --- /dev/null +++ b/salesflow-saas/backend/dealix_gtm_os/agents/competitor_intelligence_agent.py @@ -0,0 +1,8 @@ +from dealix_gtm_os.agents.base_agent import BaseAgent + +class CompetitorIntelligenceAgentAgent(BaseAgent): + name = "competitor_intelligence_agent" + description = "competitor intelligence agent" + + async def run(self, input_data: dict) -> dict: + return {"status": "stub", "agent": self.name, "note": "Connect real tools in production"} diff --git a/salesflow-saas/backend/dealix_gtm_os/agents/content_strategy_agent.py b/salesflow-saas/backend/dealix_gtm_os/agents/content_strategy_agent.py new file mode 100644 index 00000000..d52a42b7 --- /dev/null +++ b/salesflow-saas/backend/dealix_gtm_os/agents/content_strategy_agent.py @@ -0,0 +1,8 @@ +from dealix_gtm_os.agents.base_agent import BaseAgent + +class ContentStrategyAgentAgent(BaseAgent): + name = "content_strategy_agent" + description = "content strategy agent" + + async def run(self, input_data: dict) -> dict: + return {"status": "stub", "agent": self.name, "note": "Connect real tools in production"} diff --git a/salesflow-saas/backend/dealix_gtm_os/agents/crm_revenue_agent.py b/salesflow-saas/backend/dealix_gtm_os/agents/crm_revenue_agent.py new file mode 100644 index 00000000..f20d7a97 --- /dev/null +++ b/salesflow-saas/backend/dealix_gtm_os/agents/crm_revenue_agent.py @@ -0,0 +1,26 @@ +from dealix_gtm_os.agents.base_agent import BaseAgent + +STATUS_FLOW = ["new", "researched", "qualified", "message_ready", "approved", "sent", "replied", "interested", "demo_booked", "proposal_sent", "payment_sent", "paid", "won", "lost", "partner", "stop"] + +class CRMRevenueAgent(BaseAgent): + name = "crm_revenue" + description = "Manages lead/deal status transitions" + + async def run(self, input_data: dict) -> dict: + current = input_data.get("status", "new") + event = input_data.get("event", "") + next_status = current + next_action = "" + if event == "researched": next_status, next_action = "researched", "score and qualify" + elif event == "qualified": next_status, next_action = "qualified", "generate message" + elif event == "message_ready": next_status, next_action = "message_ready", "send to approval" + elif event == "approved": next_status, next_action = "approved", "send message" + elif event == "sent": next_status, next_action = "sent", "wait for reply" + elif event == "replied_interested": next_status, next_action = "interested", "book demo within 24h" + elif event == "demo_booked": next_status, next_action = "demo_booked", "prepare demo" + elif event == "demo_done": next_status, next_action = "proposal_sent", "send payment link" + elif event == "payment_sent": next_status, next_action = "payment_sent", "wait for payment" + elif event == "paid": next_status, next_action = "paid", "start onboarding" + elif event == "lost": next_status, next_action = "lost", "log reason" + elif event == "stop": next_status, next_action = "stop", "remove from list" + return {"previous_status": current, "new_status": next_status, "next_action": next_action} diff --git a/salesflow-saas/backend/dealix_gtm_os/agents/enrichment_agent.py b/salesflow-saas/backend/dealix_gtm_os/agents/enrichment_agent.py new file mode 100644 index 00000000..dbe1a03a --- /dev/null +++ b/salesflow-saas/backend/dealix_gtm_os/agents/enrichment_agent.py @@ -0,0 +1,8 @@ +from dealix_gtm_os.agents.base_agent import BaseAgent + +class EnrichmentAgentAgent(BaseAgent): + name = "enrichment_agent" + description = "enrichment agent" + + async def run(self, input_data: dict) -> dict: + return {"status": "stub", "agent": self.name, "note": "Connect real tools in production"} diff --git a/salesflow-saas/backend/dealix_gtm_os/agents/learning_agent.py b/salesflow-saas/backend/dealix_gtm_os/agents/learning_agent.py new file mode 100644 index 00000000..47ca93a6 --- /dev/null +++ b/salesflow-saas/backend/dealix_gtm_os/agents/learning_agent.py @@ -0,0 +1,22 @@ +from dealix_gtm_os.agents.base_agent import BaseAgent + +class LearningAgent(BaseAgent): + name = "learning" + description = "Analyzes results and suggests improvements" + + async def run(self, input_data: dict) -> dict: + sent = input_data.get("total_sent", 0) + replies = input_data.get("total_replies", 0) + demos = input_data.get("total_demos", 0) + payments = input_data.get("total_payments", 0) + best_sector = input_data.get("best_sector", "unknown") + best_channel = input_data.get("best_channel", "unknown") + reply_rate = (replies / sent * 100) if sent > 0 else 0 + demo_rate = (demos / replies * 100) if replies > 0 else 0 + recommendations = [] + if reply_rate < 3 and sent >= 30: recommendations.append("غيّر الرسالة أو القطاع — reply rate أقل من 3%") + if demo_rate < 20 and replies >= 5: recommendations.append("غيّر CTA — demos أقل من 20% من الردود") + if best_sector != "unknown": recommendations.append(f"ركّز على {best_sector} — أفضل أداء") + if best_channel != "unknown": recommendations.append(f"ضاعف {best_channel} — أفضل قناة") + if not recommendations: recommendations.append("استمر — البيانات ما زالت قليلة") + return {"reply_rate": round(reply_rate, 1), "demo_rate": round(demo_rate, 1), "recommendations": recommendations} diff --git a/salesflow-saas/backend/dealix_gtm_os/agents/negotiation_agent.py b/salesflow-saas/backend/dealix_gtm_os/agents/negotiation_agent.py new file mode 100644 index 00000000..eb2de0a2 --- /dev/null +++ b/salesflow-saas/backend/dealix_gtm_os/agents/negotiation_agent.py @@ -0,0 +1,25 @@ +from dealix_gtm_os.agents.base_agent import BaseAgent + +OBJECTION_RESPONSES = { + "غالي": "499 ريال لـ 7 أيام مع ضمان استرداد. لو حفظنا lead واحد = أكثر من 499.", + "عندنا CRM": "CRM يخزّن. Dealix يحرّك العميل للخطوة التالية. الطبقة اللي قبل.", + "نفكّر": "طبعاً. أرسل لكم مثال عملي تشوفونه بهدوء. وش يخليكم تترددون؟", + "أرسل تفاصيل": "10 دقائق ديمو أوضح من أي PDF. يناسبكم بكرا؟", + "مو الحين": "فاهم. أرسل لكم ملخص ترجعون لي وقت ما يناسبكم.", + "عندنا وكالة": "ممتاز — Dealix يكمّل شغل الوكالة بعد الإعلان.", + "ما نعرفكم": "عادي — نحن جدد. Pilot 499 ريال + ضمان. ما فيه مخاطرة.", + "كم السعر": "Pilot 499 ريال + ضمان. Starter 990/شهر. وكالات 20% لهم.", + "white-label": "ممكن لاحقاً بعد 3 عملاء. الحين نثبت الخدمة باسم Dealix.", + "مين يملك العميل": "العميل عميلك. أنت العلاقة، أنا التشغيل.", +} + +class NegotiationAgent(BaseAgent): + name = "negotiation" + description = "Handles objections and negotiation" + + async def run(self, input_data: dict) -> dict: + objection = input_data.get("objection", "") + for key, response in OBJECTION_RESPONSES.items(): + if key in objection: + return {"objection": objection, "response": response, "next_action": "follow_up", "confidence": 0.9} + return {"objection": objection, "response": "أفهمك. خلني أشرح لك بالضبط كيف Dealix يخدم نشاطكم.", "next_action": "demo", "confidence": 0.6} diff --git a/salesflow-saas/backend/dealix_gtm_os/agents/partnership_strategist_agent.py b/salesflow-saas/backend/dealix_gtm_os/agents/partnership_strategist_agent.py new file mode 100644 index 00000000..4032129d --- /dev/null +++ b/salesflow-saas/backend/dealix_gtm_os/agents/partnership_strategist_agent.py @@ -0,0 +1,25 @@ +from dealix_gtm_os.agents.base_agent import BaseAgent +from dealix_gtm_os.models.opportunity import OpportunityType + +PARTNERSHIP_MAP = { + "agency": [OpportunityType.AGENCY_PARTNER, OpportunityType.CO_SELLING_PARTNER, OpportunityType.REFERRAL_PARTNER], + "website_agency": [OpportunityType.IMPLEMENTATION_PARTNER, OpportunityType.AGENCY_PARTNER], + "consulting": [OpportunityType.REFERRAL_PARTNER, OpportunityType.IMPLEMENTATION_PARTNER], + "saas": [OpportunityType.INTEGRATION_PARTNER, OpportunityType.DIRECT_CUSTOMER], +} + +class PartnershipStrategistAgent(BaseAgent): + name = "partnership_strategist" + description = "Classifies partnership opportunities" + + async def run(self, input_data: dict) -> dict: + sector = input_data.get("sector", "").lower().replace(" ", "_") + types = PARTNERSHIP_MAP.get(sector, [OpportunityType.DIRECT_CUSTOMER]) + primary = types[0] if types else OpportunityType.DIRECT_CUSTOMER + return { + "opportunity_types": [t.value for t in types], + "primary_type": primary.value, + "partner_potential": "high" if len(types) > 1 else "low", + "recommended_model": "agency_addon" if primary == OpportunityType.AGENCY_PARTNER else "pilot", + "negotiation_angle": "خدمة جديدة تبيعونها" if primary == OpportunityType.AGENCY_PARTNER else "حل لمشكلة الـleads", + } diff --git a/salesflow-saas/backend/dealix_gtm_os/agents/web_search_agent.py b/salesflow-saas/backend/dealix_gtm_os/agents/web_search_agent.py new file mode 100644 index 00000000..5b1a8ad6 --- /dev/null +++ b/salesflow-saas/backend/dealix_gtm_os/agents/web_search_agent.py @@ -0,0 +1,8 @@ +from dealix_gtm_os.agents.base_agent import BaseAgent + +class WebSearchAgentAgent(BaseAgent): + name = "web_search_agent" + description = "web search agent" + + async def run(self, input_data: dict) -> dict: + return {"status": "stub", "agent": self.name, "note": "Connect real tools in production"} diff --git a/salesflow-saas/backend/dealix_gtm_os/cli/__init__.py b/salesflow-saas/backend/dealix_gtm_os/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/salesflow-saas/backend/dealix_gtm_os/compliance/__init__.py b/salesflow-saas/backend/dealix_gtm_os/compliance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/salesflow-saas/backend/dealix_gtm_os/compliance/compliance_engine.py b/salesflow-saas/backend/dealix_gtm_os/compliance/compliance_engine.py new file mode 100644 index 00000000..d053072f --- /dev/null +++ b/salesflow-saas/backend/dealix_gtm_os/compliance/compliance_engine.py @@ -0,0 +1,33 @@ +"""Compliance engine — decides what's allowed per channel.""" +import yaml +from pathlib import Path +from dealix_gtm_os.models.message import AutomationLevel + +RULES_PATH = Path(__file__).parent.parent / "config" / "compliance_rules.yaml" + +_rules = {} +if RULES_PATH.exists(): + with open(RULES_PATH) as f: + _rules = yaml.safe_load(f) or {} + +def check_compliance(channel: str, action: str = "send_message") -> dict: + channel_key = channel.split("_")[0] + if channel_key == "x": + channel_key = "x_twitter" + rules = _rules.get(channel_key, {}) + if any(rules.get(k) == "prohibited" for k in [action, "scraping", "auto_dm", "auto_connect", "mass_dm", "cold_blast"]): + if action in rules and rules[action] == "prohibited": + return {"allowed": False, "level": AutomationLevel.PROHIBITED, "reason": f"{action} on {channel} is prohibited"} + manual_channels = {"linkedin_manual", "whatsapp_warm", "phone", "partner_intro"} + if channel in manual_channels: + return {"allowed": True, "level": AutomationLevel.MANUAL_REQUIRED, "reason": f"{channel} requires Sami approval"} + return {"allowed": True, "level": AutomationLevel.SEMI_AUTOMATED, "reason": f"{channel} is safe with opt-out"} + +def get_daily_limit(channel: str) -> int: + limits = {"email": 10, "linkedin_manual": 5, "whatsapp_warm": 5, "instagram_inbound": 3, "x_post": 3, "x_reply": 5, "phone": 3, "partner_intro": 2} + return limits.get(channel, 5) + +STOP_WORDS = ["إيقاف", "stop", "لا", "لا شكراً", "ما يناسبني"] + +def should_stop(reply_text: str) -> bool: + return any(w in reply_text for w in STOP_WORDS) diff --git a/salesflow-saas/backend/dealix_gtm_os/scoring/__init__.py b/salesflow-saas/backend/dealix_gtm_os/scoring/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/salesflow-saas/backend/dealix_gtm_os/scoring/scoring_engine.py b/salesflow-saas/backend/dealix_gtm_os/scoring/scoring_engine.py new file mode 100644 index 00000000..9c9c5a1d --- /dev/null +++ b/salesflow-saas/backend/dealix_gtm_os/scoring/scoring_engine.py @@ -0,0 +1,28 @@ +"""Unified scoring engine for targets, channels, and partners.""" +from dealix_gtm_os.models.score import TargetScore + +SECTOR_DEFAULTS = { + "agency": {"fit": 5, "urgency": 4, "partner": 5, "payment": 3, "case_study": 4, "risk": 2}, + "real_estate": {"fit": 5, "urgency": 5, "partner": 2, "payment": 4, "case_study": 3, "risk": 2}, + "saas": {"fit": 4, "urgency": 4, "partner": 3, "payment": 3, "case_study": 3, "risk": 2}, + "clinic": {"fit": 4, "urgency": 4, "partner": 1, "payment": 4, "case_study": 3, "risk": 1}, + "ecommerce": {"fit": 4, "urgency": 3, "partner": 2, "payment": 3, "case_study": 2, "risk": 2}, + "construction": {"fit": 3, "urgency": 3, "partner": 1, "payment": 3, "case_study": 2, "risk": 2}, + "training": {"fit": 3, "urgency": 3, "partner": 1, "payment": 3, "case_study": 2, "risk": 1}, + "consulting": {"fit": 3, "urgency": 2, "partner": 3, "payment": 3, "case_study": 2, "risk": 1}, + "website_agency": {"fit": 4, "urgency": 3, "partner": 4, "payment": 3, "case_study": 3, "risk": 1}, +} + +def score_target(company_name: str, sector: str, has_contact: bool = False) -> TargetScore: + sector_key = sector.lower().replace(" ", "_").replace("marketing_", "") + defaults = SECTOR_DEFAULTS.get(sector_key, {"fit": 3, "urgency": 3, "partner": 2, "payment": 3, "case_study": 2, "risk": 2}) + return TargetScore( + company_name=company_name, + fit=defaults["fit"], + urgency=defaults["urgency"], + access=4 if has_contact else 2, + partner=defaults["partner"], + payment=defaults["payment"], + case_study=defaults["case_study"], + risk=defaults["risk"], + ) diff --git a/salesflow-saas/backend/tests/evals/gtm_os_eval_set.jsonl b/salesflow-saas/backend/tests/evals/gtm_os_eval_set.jsonl new file mode 100644 index 00000000..989d8a6a --- /dev/null +++ b/salesflow-saas/backend/tests/evals/gtm_os_eval_set.jsonl @@ -0,0 +1,10 @@ +{"company": "وكالة تسويق رقمي", "sector": "agency", "city": "الرياض", "expected_opportunity": "agency_partner", "expected_channel": "email", "prohibited": ["linkedin_scraping", "whatsapp_cold_blast"]} +{"company": "شركة تسويق عقاري", "sector": "real_estate", "city": "جدة", "expected_opportunity": "direct_customer", "expected_channel": "email", "prohibited": ["linkedin_scraping"]} +{"company": "عيادة تجميل", "sector": "clinic", "city": "الخبر", "expected_opportunity": "direct_customer", "expected_channel": "whatsapp_warm", "prohibited": ["whatsapp_cold_blast"]} +{"company": "متجر إلكتروني", "sector": "ecommerce", "city": "الرياض", "expected_opportunity": "direct_customer", "expected_channel": "email", "prohibited": ["instagram_mass_dm"]} +{"company": "وكالة مواقع", "sector": "website_agency", "city": "الدمام", "expected_opportunity": "implementation_partner", "expected_channel": "linkedin_manual", "prohibited": ["linkedin_scraping"]} +{"company": "شركة استشارات", "sector": "consulting", "city": "الرياض", "expected_opportunity": "referral_partner", "expected_channel": "linkedin_manual", "prohibited": ["linkedin_scraping"]} +{"company": "شركة مقاولات", "sector": "construction", "city": "جدة", "expected_opportunity": "direct_customer", "expected_channel": "email", "prohibited": ["whatsapp_cold_blast"]} +{"company": "مركز تدريب", "sector": "training", "city": "الرياض", "expected_opportunity": "direct_customer", "expected_channel": "email", "prohibited": []} +{"company": "شركة SaaS", "sector": "saas", "city": "الرياض", "expected_opportunity": "direct_customer", "expected_channel": "email", "prohibited": ["linkedin_scraping"]} +{"company": "فريلانسر تسويق", "sector": "agency", "city": "جدة", "expected_opportunity": "agency_partner", "expected_channel": "email", "prohibited": ["whatsapp_cold_blast"]} diff --git a/salesflow-saas/backend/tests/evals/test_gtm_os_eval.py b/salesflow-saas/backend/tests/evals/test_gtm_os_eval.py new file mode 100644 index 00000000..968d5595 --- /dev/null +++ b/salesflow-saas/backend/tests/evals/test_gtm_os_eval.py @@ -0,0 +1,61 @@ +"""GTM OS evaluation tests — verifies intelligence quality.""" +import asyncio +import json +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) + +from dealix_gtm_os.agents.supervisor_agent import SupervisorAgent + +EVAL_FILE = os.path.join(os.path.dirname(__file__), "gtm_os_eval_set.jsonl") + +async def run_evals(): + supervisor = SupervisorAgent() + with open(EVAL_FILE) as f: + cases = [json.loads(line) for line in f if line.strip()] + + passed = 0 + failed = 0 + total = len(cases) + + for case in cases: + result = await supervisor.run({ + "name": case["company"], + "sector": case["sector"], + "city": case["city"], + }) + + channel = result["channel_plan"]["primary_channel"] + compliance = result["compliance"]["allowed"] + opportunity = result["intelligence"].get("opportunity_types", []) + has_optout = "إيقاف" in result["message"].get("stop_condition", "") + + errors = [] + if case["expected_channel"] != channel: + errors.append(f"channel: expected {case['expected_channel']}, got {channel}") + if not compliance: + errors.append("compliance: should be allowed but was denied") + if not has_optout: + errors.append("missing opt-out in message") + if case["expected_opportunity"] not in opportunity: + pass # opportunity matching is advisory + + if errors: + failed += 1 + print(f" ❌ {case['company']}: {'; '.join(errors)}") + else: + passed += 1 + print(f" ✅ {case['company']}: channel={channel}, compliant={compliance}") + + print(f"\n{'=' * 40}") + print(f"Results: {passed}/{total} passed ({passed/total*100:.0f}%)") + print(f"Failed: {failed}") + if passed / total >= 0.8: + print("VERDICT: ✅ PASS (≥80%)") + else: + print("VERDICT: ❌ FAIL (<80%)") + return passed / total >= 0.8 + +if __name__ == "__main__": + success = asyncio.run(run_evals()) + sys.exit(0 if success else 1)