mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 14:59:35 +00:00
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:
parent
81a444d3e1
commit
c398bd31ce
169
salesflow-saas/backend/app/api/v1/full_os.py
Normal file
169
salesflow-saas/backend/app/api/v1/full_os.py
Normal 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)}
|
||||
@ -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)
|
||||
|
||||
291
salesflow-saas/backend/app/services/full_os_orchestrator.py
Normal file
291
salesflow-saas/backend/app/services/full_os_orchestrator.py
Normal 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()
|
||||
Loading…
Reference in New Issue
Block a user