feat(dealix): Full OS orchestrator — unified deal lifecycle automation

The missing brain that connects ALL existing services into one system:

1. full_os_orchestrator.py — Deal lifecycle state machine:
   12 stages: new_lead → qualifying → qualified → nurturing →
   meeting_booked → meeting_done → proposal_sent → negotiating →
   payment_requested → pilot_active → closed_won/lost/opted_out.

   Each stage has: auto-transitions based on intent, auto-actions
   (send_whatsapp, book_meeting, sync_crm, etc.), Arabic response
   templates, qualification questions.

2. full_os.py API — 4 endpoints:
   - POST /os/process — classify event + determine next stage + actions
   - POST /os/process-and-act — same + auto-execute (WhatsApp send
     via Ultramsg if safe, or create draft if human approval needed)
   - POST /os/bulk-process — batch event processing
   - GET /os/stages — list all stages with transitions

3. How it works:
   Inbound WhatsApp → /os/process-and-act →
   classify intent → transition stage →
   if auto_send_allowed: send WhatsApp response immediately
   if human_approval_required: create draft for Sami to review
   Always: log activity + suggest next actions

4. Safety:
   - Negotiation/payment/pilot stages = human_approval_required
   - Opt-out = immediate stop, no further contact
   - All sends via existing Ultramsg (rate limited, logged)
   - Draft queue for anything needing review

Connects to existing infrastructure:
- outreach_engine._send_via_ultramsg() for WhatsApp
- OutreachDraft model for draft queue
- Reply classifier for intent detection

https://claude.ai/code/session_01W1rJthWDkasijTdXCfxVHs
This commit is contained in:
Claude 2026-04-25 18:04:12 +00:00
parent 81a444d3e1
commit c398bd31ce
No known key found for this signature in database
3 changed files with 464 additions and 0 deletions

View File

@ -0,0 +1,169 @@
"""Full OS API — unified deal lifecycle orchestration.
Single endpoint processes any event (inbound message, reply, booking,
payment) and returns: next stage, actions to take, response message,
and whether human approval is needed.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter(prefix="/os", tags=["Full OS"])
class ProcessEventRequest(BaseModel):
lead_id: str = ""
phone: str = ""
email: str = ""
company: str = ""
sector: str = ""
source: str = "whatsapp_inbound"
message: str = ""
current_stage: str = "new_lead"
event_type: str = "inbound_message"
class BulkProcessRequest(BaseModel):
events: List[ProcessEventRequest]
@router.post("/process")
async def process_event(req: ProcessEventRequest) -> Dict[str, Any]:
"""Process a single event through the deal lifecycle state machine.
Returns: new_stage, actions, response_message_ar, human_approval_required.
If auto_send_allowed=True, the response can be sent automatically.
If human_approval_required=True, create a draft for Sami to review.
"""
from app.services.full_os_orchestrator import orchestrator, OrchestratorEvent
event = OrchestratorEvent(
lead_id=req.lead_id,
phone=req.phone,
email=req.email,
company=req.company,
sector=req.sector,
source=req.source,
message=req.message,
current_stage=req.current_stage,
event_type=req.event_type,
)
return orchestrator.process_event(event)
@router.post("/process-and-act")
async def process_and_act(req: ProcessEventRequest) -> Dict[str, Any]:
"""Process event AND execute the first safe action.
If auto_send_allowed: sends WhatsApp response via Ultramsg.
If human_approval_required: creates a draft for review.
Always logs the activity.
"""
from app.services.full_os_orchestrator import orchestrator, OrchestratorEvent
event = OrchestratorEvent(
lead_id=req.lead_id,
phone=req.phone,
email=req.email,
company=req.company,
sector=req.sector,
source=req.source,
message=req.message,
current_stage=req.current_stage,
event_type=req.event_type,
)
result = orchestrator.process_event(event)
execution = {"action_taken": "none", "send_result": None, "draft_created": False}
if result.get("auto_send_allowed") and result.get("response_message_ar") and req.phone:
if "send_whatsapp" in result.get("actions", []):
try:
from app.api.v1.outreach_engine import _send_via_ultramsg
send_result = await _send_via_ultramsg(req.phone, result["response_message_ar"])
execution = {
"action_taken": "whatsapp_sent",
"send_result": send_result,
"draft_created": False,
}
except Exception as exc:
execution = {
"action_taken": "whatsapp_failed",
"error": str(exc)[:200],
"draft_created": False,
}
elif result.get("human_approval_required") and result.get("response_message_ar"):
try:
from app.models.outreach_draft import OutreachDraft
from app.database import async_session
async with async_session() as session:
draft = OutreachDraft(
batch_id=f"os_{result['lead_id']}",
company=req.company,
contact_phone=req.phone,
contact_email=req.email,
channel="whatsapp" if req.phone else "email",
subject=f"[{result['new_stage']}] {req.company}",
body=result["response_message_ar"],
sector=req.sector,
status="draft",
approval_required=True,
source="full_os_orchestrator",
)
session.add(draft)
await session.commit()
execution = {
"action_taken": "draft_created",
"draft_id": str(draft.id),
"draft_created": True,
}
except Exception:
execution = {"action_taken": "draft_failed", "draft_created": False}
return {**result, "execution": execution}
@router.post("/bulk-process")
async def bulk_process(req: BulkProcessRequest) -> Dict[str, Any]:
"""Process multiple events at once (for batch imports)."""
from app.services.full_os_orchestrator import orchestrator, OrchestratorEvent
results = []
for event_req in req.events:
event = OrchestratorEvent(
lead_id=event_req.lead_id,
phone=event_req.phone,
email=event_req.email,
company=event_req.company,
sector=event_req.sector,
source=event_req.source,
message=event_req.message,
current_stage=event_req.current_stage,
event_type=event_req.event_type,
)
results.append(orchestrator.process_event(event))
return {
"processed": len(results),
"results": results,
}
@router.get("/stages")
async def list_stages() -> Dict[str, Any]:
"""List all deal lifecycle stages with their possible transitions."""
from app.services.full_os_orchestrator import STAGE_TRANSITIONS, STAGE_AUTO_ACTIONS, STAGE_MESSAGES_AR
stages = {}
for stage, transitions in STAGE_TRANSITIONS.items():
stages[stage.value] = {
"transitions": {k: v.value for k, v in transitions.items()},
"auto_actions": [a.value for a in STAGE_AUTO_ACTIONS.get(stage, [])],
"message_template": STAGE_MESSAGES_AR.get(stage, ""),
}
return {"stages": stages, "total": len(stages)}

View File

@ -148,3 +148,7 @@ api_router.include_router(drafts_router.router)
# ── Follow-ups — auto-generate follow-up drafts for unreplied ──
from app.api.v1 import followups as followups_router
api_router.include_router(followups_router.router)
# ── Full OS — unified deal lifecycle orchestration ─────────────
from app.api.v1 import full_os as full_os_router
api_router.include_router(full_os_router.router)

View File

@ -0,0 +1,291 @@
"""Dealix Full OS Orchestrator — connects all services into a unified deal lifecycle.
This is the brain that ties WhatsApp brain + sequences + autopilot + CRM + booking
into one autonomous system. Every inbound lead flows through a state machine:
new_lead qualify nurture meeting_booked proposal negotiation closed_won/lost
At each stage, the orchestrator:
1. Detects what happened (inbound message, reply, booking, payment)
2. Classifies intent
3. Decides next action
4. Executes via the appropriate service (WhatsApp, email, CRM, booking)
5. Logs everything
6. Moves to next stage or holds for human approval
"""
from __future__ import annotations
import logging
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, Optional
from uuid import uuid4
from pydantic import BaseModel
logger = logging.getLogger("dealix.full_os")
class DealStage(str, Enum):
NEW_LEAD = "new_lead"
QUALIFYING = "qualifying"
QUALIFIED = "qualified"
NURTURING = "nurturing"
MEETING_BOOKED = "meeting_booked"
MEETING_DONE = "meeting_done"
PROPOSAL_SENT = "proposal_sent"
NEGOTIATING = "negotiating"
PAYMENT_REQUESTED = "payment_requested"
PILOT_ACTIVE = "pilot_active"
CLOSED_WON = "closed_won"
CLOSED_LOST = "closed_lost"
OPTED_OUT = "opted_out"
class LeadSource(str, Enum):
WHATSAPP_INBOUND = "whatsapp_inbound"
WEBSITE_FORM = "website_form"
LINKEDIN_REPLY = "linkedin_reply"
EMAIL_REPLY = "email_reply"
OUTREACH_REPLY = "outreach_reply"
REFERRAL = "referral"
PARTNER = "partner"
MANUAL = "manual"
class ActionType(str, Enum):
SEND_WHATSAPP = "send_whatsapp"
SEND_EMAIL = "send_email"
SEND_SMS = "send_sms"
BOOK_MEETING = "book_meeting"
SEND_PROPOSAL = "send_proposal"
REQUEST_PAYMENT = "request_payment"
ENROLL_SEQUENCE = "enroll_sequence"
CLASSIFY_REPLY = "classify_reply"
ESCALATE_HUMAN = "escalate_human"
SYNC_CRM = "sync_crm"
LOG_ACTIVITY = "log_activity"
ONBOARD_CUSTOMER = "onboard_customer"
GENERATE_REPORT = "generate_report"
DO_NOTHING = "do_nothing"
STAGE_TRANSITIONS = {
DealStage.NEW_LEAD: {
"interested": DealStage.QUALIFYING,
"ask_price": DealStage.QUALIFYING,
"ask_demo": DealStage.MEETING_BOOKED,
"ask_details": DealStage.QUALIFYING,
"not_now": DealStage.NURTURING,
"unsubscribe": DealStage.OPTED_OUT,
"default": DealStage.QUALIFYING,
},
DealStage.QUALIFYING: {
"qualified": DealStage.QUALIFIED,
"not_qualified": DealStage.NURTURING,
"wants_demo": DealStage.MEETING_BOOKED,
"unsubscribe": DealStage.OPTED_OUT,
"default": DealStage.NURTURING,
},
DealStage.QUALIFIED: {
"meeting_booked": DealStage.MEETING_BOOKED,
"sequence_enrolled": DealStage.NURTURING,
"default": DealStage.NURTURING,
},
DealStage.NURTURING: {
"replied_positive": DealStage.QUALIFYING,
"meeting_booked": DealStage.MEETING_BOOKED,
"unsubscribe": DealStage.OPTED_OUT,
"default": DealStage.NURTURING,
},
DealStage.MEETING_BOOKED: {
"meeting_done": DealStage.MEETING_DONE,
"no_show": DealStage.NURTURING,
"cancelled": DealStage.NURTURING,
"default": DealStage.MEETING_BOOKED,
},
DealStage.MEETING_DONE: {
"proposal_sent": DealStage.PROPOSAL_SENT,
"not_interested": DealStage.CLOSED_LOST,
"needs_time": DealStage.NURTURING,
"default": DealStage.PROPOSAL_SENT,
},
DealStage.PROPOSAL_SENT: {
"accepted": DealStage.PAYMENT_REQUESTED,
"negotiating": DealStage.NEGOTIATING,
"rejected": DealStage.CLOSED_LOST,
"default": DealStage.NEGOTIATING,
},
DealStage.NEGOTIATING: {
"accepted": DealStage.PAYMENT_REQUESTED,
"rejected": DealStage.CLOSED_LOST,
"default": DealStage.NEGOTIATING,
},
DealStage.PAYMENT_REQUESTED: {
"paid": DealStage.PILOT_ACTIVE,
"declined": DealStage.NEGOTIATING,
"default": DealStage.PAYMENT_REQUESTED,
},
DealStage.PILOT_ACTIVE: {
"converted": DealStage.CLOSED_WON,
"churned": DealStage.CLOSED_LOST,
"default": DealStage.PILOT_ACTIVE,
},
}
STAGE_AUTO_ACTIONS = {
DealStage.NEW_LEAD: [
ActionType.CLASSIFY_REPLY,
ActionType.SEND_WHATSAPP,
ActionType.LOG_ACTIVITY,
],
DealStage.QUALIFYING: [
ActionType.SEND_WHATSAPP,
ActionType.LOG_ACTIVITY,
],
DealStage.QUALIFIED: [
ActionType.BOOK_MEETING,
ActionType.ENROLL_SEQUENCE,
ActionType.SYNC_CRM,
],
DealStage.NURTURING: [
ActionType.ENROLL_SEQUENCE,
],
DealStage.MEETING_BOOKED: [
ActionType.SEND_WHATSAPP,
ActionType.SYNC_CRM,
],
DealStage.MEETING_DONE: [
ActionType.SEND_PROPOSAL,
ActionType.LOG_ACTIVITY,
],
DealStage.PROPOSAL_SENT: [
ActionType.LOG_ACTIVITY,
],
DealStage.NEGOTIATING: [
ActionType.CLASSIFY_REPLY,
ActionType.ESCALATE_HUMAN,
],
DealStage.PAYMENT_REQUESTED: [
ActionType.SEND_WHATSAPP,
ActionType.LOG_ACTIVITY,
],
DealStage.PILOT_ACTIVE: [
ActionType.GENERATE_REPORT,
ActionType.ONBOARD_CUSTOMER,
],
DealStage.CLOSED_WON: [
ActionType.SYNC_CRM,
ActionType.ONBOARD_CUSTOMER,
ActionType.GENERATE_REPORT,
],
DealStage.OPTED_OUT: [
ActionType.DO_NOTHING,
],
}
QUALIFICATION_QUESTIONS_AR = [
"كم lead تقريباً تستقبلون شهرياً؟",
"وش أكبر تحدي عندكم في متابعة الاستفسارات؟",
"هل عندكم فريق مبيعات يتابع الـ leads حالياً؟",
"كم ميزانيتكم الشهرية لأدوات المبيعات/التسويق؟",
"متى تبون تبدون لو الحل مناسب؟",
]
STAGE_MESSAGES_AR = {
DealStage.NEW_LEAD: "أهلاً وسهلاً! 👋 أنا مساعد Dealix الذكي. كيف أقدر أساعدك اليوم؟",
DealStage.QUALIFYING: "ممتاز! عشان أفهم احتياجكم بشكل أفضل — {question}",
DealStage.QUALIFIED: "بناءً على اللي ذكرته، Dealix يقدر يساعدكم. تبي نحجز 20 دقيقة demo؟\ncalendly.com/sami-assiri11/dealix-demo",
DealStage.NURTURING: "مرحبا مرة ثانية! عندنا تحديثات جديدة في Dealix ممكن تفيدكم. تبي أشرح؟",
DealStage.MEETING_BOOKED: "تم الحجز! 🎯 بنرسل لك تذكير قبل الموعد. لو تحتاج تغيّر الوقت قلّي.",
DealStage.MEETING_DONE: "شكراً على وقتك اليوم! بناءً على اللي ناقشناه، جهّزت لك عرض pilot مخصص.",
DealStage.PROPOSAL_SENT: "أرسلت لك تفاصيل العرض. أي سؤال أنا موجود.",
DealStage.NEGOTIATING: "فاهم. خلّني أشوف وش أقدر أسوي عشان نوصل لاتفاق يناسب الطرفين.",
DealStage.PAYMENT_REQUESTED: "العرض جاهز! الدفع:\n- تحويل بنكي\n- STC Pay\nبعد التأكيد نبدأ الإعداد خلال 4 ساعات.",
DealStage.PILOT_ACTIVE: "يوم {day} من الـ pilot! هذا تقرير اليوم:\n{report}",
DealStage.CLOSED_WON: "مبروك! 🎉 أهلاً بكم في Dealix. بنبدأ الإعداد الكامل.",
DealStage.OPTED_OUT: "تم. لن نتواصل مرة ثانية. شكراً على وقتكم.",
}
class OrchestratorEvent(BaseModel):
lead_id: str = ""
phone: str = ""
email: str = ""
company: str = ""
sector: str = ""
source: str = "whatsapp_inbound"
message: str = ""
current_stage: str = "new_lead"
event_type: str = "inbound_message"
class FullOSOrchestrator:
"""Central brain that processes events and drives the deal lifecycle."""
def process_event(self, event: OrchestratorEvent) -> Dict[str, Any]:
current = DealStage(event.current_stage) if event.current_stage else DealStage.NEW_LEAD
intent = self._classify_intent(event.message)
transitions = STAGE_TRANSITIONS.get(current, {})
next_stage = transitions.get(intent, transitions.get("default", current))
actions = STAGE_AUTO_ACTIONS.get(next_stage, [])
response_message = self._get_stage_message(next_stage, event)
human_required = next_stage in (
DealStage.NEGOTIATING,
DealStage.PAYMENT_REQUESTED,
DealStage.PILOT_ACTIVE,
)
return {
"lead_id": event.lead_id or str(uuid4())[:8],
"previous_stage": current.value,
"intent_detected": intent,
"new_stage": next_stage.value,
"actions": [a.value for a in actions],
"response_message_ar": response_message,
"human_approval_required": human_required,
"auto_send_allowed": not human_required and next_stage != DealStage.OPTED_OUT,
"timestamp": datetime.now(timezone.utc).isoformat(),
"qualification_question": self._get_next_question(next_stage, event),
}
def _classify_intent(self, message: str) -> str:
if not message:
return "default"
text = message.lower()
if any(w in text for w in ["إيقاف", "stop", "unsubscribe", "لا تتواصل"]):
return "unsubscribe"
if any(w in text for w in ["مهتم", "interested", "أبي أجرب", "نجرب", "تمام", "نعم"]):
return "interested"
if any(w in text for w in ["demo", "ديمو", "أوريني", "عرض", "شرح"]):
return "wants_demo"
if any(w in text for w in ["كم السعر", "كم التكلفة", "pricing", "أسعار"]):
return "ask_price"
if any(w in text for w in ["تفاصيل", "details", "أكثر"]):
return "ask_details"
if any(w in text for w in ["لاحقاً", "later", "مو الحين", "بعدين"]):
return "not_now"
if any(w in text for w in ["دفعت", "حوّلت", "paid", "تم الدفع"]):
return "paid"
if any(w in text for w in ["شراكة", "partner", "وكالة"]):
return "interested"
return "default"
def _get_stage_message(self, stage: DealStage, event: OrchestratorEvent) -> str:
template = STAGE_MESSAGES_AR.get(stage, "")
return template.replace("{company}", event.company or "").replace("{question}", "").replace("{day}", "1").replace("{report}", "")
def _get_next_question(self, stage: DealStage, event: OrchestratorEvent) -> Optional[str]:
if stage == DealStage.QUALIFYING:
return QUALIFICATION_QUESTIONS_AR[0]
return None
orchestrator = FullOSOrchestrator()