mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 07:19:35 +00:00
Continuing Phase 3-6 implementation: - AI: conversation_intelligence.py (Arabic dialogue analysis, buying signals) - AI: message_writer.py (Arabic/English multi-channel message generation) - AI: sales_agent.py (autonomous WhatsApp qualification bot) - API: compliance.py (PDPL consent & data rights endpoints) - API: inbox.py (unified multi-channel inbox) - API: proposals.py (CPQ quote management endpoints) - API: sequences.py (multi-channel sequence management) - Services: territory_manager.py (Saudi region-based lead routing) - Seeds: contracting_template.json (Saudi contracting industry template) - Updated: router.py, consent_manager.py, data_rights.py https://claude.ai/code/session_01LsnvBa7HwF5hs99VZbgLGj
240 lines
7.4 KiB
Python
240 lines
7.4 KiB
Python
"""Sequences API -- create, manage, and analyze multi-channel outreach sequences."""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from pydantic import BaseModel as Schema
|
|
from sqlalchemy import select, func
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.api.deps import get_current_user
|
|
from app.models.user import User
|
|
from app.models.sequence import Sequence, SequenceStep, SequenceEnrollment
|
|
from app.services.sequence_engine import (
|
|
SequenceEngine, SequenceCreateInput, EnrollInput, SequenceAnalytics,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/sequences", tags=["Sequences"])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Schemas
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class StepSchema(Schema):
|
|
channel: str
|
|
delay_minutes: int = 0
|
|
template_content: str
|
|
template_content_ar: Optional[str] = None
|
|
variant: Optional[str] = None
|
|
conditions: dict = {}
|
|
|
|
|
|
class SequenceCreateRequest(Schema):
|
|
name: str
|
|
name_ar: Optional[str] = None
|
|
description: Optional[str] = None
|
|
trigger_event: Optional[str] = None
|
|
steps: list[StepSchema] = []
|
|
|
|
|
|
class SequenceUpdateRequest(Schema):
|
|
name: Optional[str] = None
|
|
name_ar: Optional[str] = None
|
|
description: Optional[str] = None
|
|
trigger_event: Optional[str] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
class SequenceResponse(Schema):
|
|
id: UUID
|
|
name: str
|
|
name_ar: Optional[str] = None
|
|
description: Optional[str] = None
|
|
trigger_event: Optional[str] = None
|
|
is_active: bool
|
|
created_at: object
|
|
step_count: int = 0
|
|
enrollment_count: int = 0
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class SequenceListResponse(Schema):
|
|
items: list[SequenceResponse]
|
|
total: int
|
|
|
|
|
|
class EnrollRequest(Schema):
|
|
lead_id: UUID
|
|
|
|
|
|
class EnrollmentResponse(Schema):
|
|
id: UUID
|
|
sequence_id: UUID
|
|
lead_id: UUID
|
|
current_step: int
|
|
status: str
|
|
enrolled_at: object
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("", response_model=SequenceListResponse)
|
|
async def list_sequences(
|
|
page: int = Query(1, ge=1),
|
|
per_page: int = Query(20, ge=1, le=100),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""List sequences with analytics summary."""
|
|
|
|
tid = current_user.tenant_id
|
|
total = (await db.execute(
|
|
select(func.count()).where(Sequence.tenant_id == tid)
|
|
)).scalar() or 0
|
|
|
|
rows = await db.execute(
|
|
select(Sequence)
|
|
.where(Sequence.tenant_id == tid)
|
|
.order_by(Sequence.created_at.desc())
|
|
.offset((page - 1) * per_page).limit(per_page)
|
|
)
|
|
sequences = rows.scalars().all()
|
|
|
|
items = []
|
|
for seq in sequences:
|
|
step_count = (await db.execute(
|
|
select(func.count()).where(SequenceStep.sequence_id == seq.id)
|
|
)).scalar() or 0
|
|
enroll_count = (await db.execute(
|
|
select(func.count()).where(SequenceEnrollment.sequence_id == seq.id)
|
|
)).scalar() or 0
|
|
resp = SequenceResponse.model_validate(seq)
|
|
resp.step_count = step_count
|
|
resp.enrollment_count = enroll_count
|
|
items.append(resp)
|
|
|
|
return SequenceListResponse(items=items, total=total)
|
|
|
|
|
|
@router.post("", response_model=SequenceResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_sequence(
|
|
data: SequenceCreateRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Create a new sequence with steps."""
|
|
|
|
engine = SequenceEngine(db)
|
|
seq = await engine.create_sequence(SequenceCreateInput(
|
|
tenant_id=current_user.tenant_id,
|
|
name=data.name,
|
|
name_ar=data.name_ar,
|
|
description=data.description,
|
|
trigger_event=data.trigger_event,
|
|
created_by=current_user.id,
|
|
steps=[s.model_dump() for s in data.steps],
|
|
))
|
|
|
|
resp = SequenceResponse.model_validate(seq)
|
|
resp.step_count = len(data.steps)
|
|
return resp
|
|
|
|
|
|
@router.put("/{sequence_id}", response_model=SequenceResponse)
|
|
async def update_sequence(
|
|
sequence_id: UUID,
|
|
data: SequenceUpdateRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Update sequence metadata."""
|
|
|
|
seq = (await db.execute(
|
|
select(Sequence).where(Sequence.id == sequence_id, Sequence.tenant_id == current_user.tenant_id)
|
|
)).scalar_one_or_none()
|
|
if not seq:
|
|
raise HTTPException(status_code=404, detail="التسلسل غير موجود")
|
|
|
|
for field, val in data.model_dump(exclude_none=True).items():
|
|
setattr(seq, field, val)
|
|
await db.flush()
|
|
await db.refresh(seq)
|
|
logger.info("Sequence updated: id=%s", sequence_id)
|
|
return SequenceResponse.model_validate(seq)
|
|
|
|
|
|
@router.post("/{sequence_id}/enroll", response_model=EnrollmentResponse, status_code=status.HTTP_201_CREATED)
|
|
async def enroll_lead(
|
|
sequence_id: UUID,
|
|
data: EnrollRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Enroll a lead into a sequence."""
|
|
|
|
seq = (await db.execute(
|
|
select(Sequence).where(Sequence.id == sequence_id, Sequence.tenant_id == current_user.tenant_id)
|
|
)).scalar_one_or_none()
|
|
if not seq:
|
|
raise HTTPException(status_code=404, detail="التسلسل غير موجود")
|
|
if not seq.is_active:
|
|
raise HTTPException(status_code=400, detail="التسلسل غير نشط")
|
|
|
|
engine = SequenceEngine(db)
|
|
try:
|
|
enrollment = await engine.enroll_lead(EnrollInput(sequence_id=sequence_id, lead_id=data.lead_id))
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc))
|
|
return EnrollmentResponse.model_validate(enrollment)
|
|
|
|
|
|
@router.delete("/{sequence_id}/enrollments/{enrollment_id}", status_code=status.HTTP_200_OK)
|
|
async def stop_enrollment(
|
|
sequence_id: UUID,
|
|
enrollment_id: UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Stop an active enrollment."""
|
|
|
|
enrollment = (await db.execute(
|
|
select(SequenceEnrollment).where(
|
|
SequenceEnrollment.id == enrollment_id,
|
|
SequenceEnrollment.sequence_id == sequence_id,
|
|
)
|
|
)).scalar_one_or_none()
|
|
if not enrollment:
|
|
raise HTTPException(status_code=404, detail="التسجيل غير موجود")
|
|
|
|
engine = SequenceEngine(db)
|
|
await engine.stop_enrollment(enrollment_id)
|
|
return {"detail": "تم إيقاف التسجيل بنجاح", "enrollment_id": str(enrollment_id)}
|
|
|
|
|
|
@router.get("/{sequence_id}/analytics", response_model=SequenceAnalytics)
|
|
async def get_analytics(
|
|
sequence_id: UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Detailed analytics for a sequence."""
|
|
|
|
seq = (await db.execute(
|
|
select(Sequence).where(Sequence.id == sequence_id, Sequence.tenant_id == current_user.tenant_id)
|
|
)).scalar_one_or_none()
|
|
if not seq:
|
|
raise HTTPException(status_code=404, detail="التسلسل غير موجود")
|
|
|
|
engine = SequenceEngine(db)
|
|
return await engine.get_sequence_analytics(sequence_id)
|