mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 07:19:35 +00:00
Complete outreach automation that generates drafts → Sami approves → system sends:
1. OutreachDraft model (models/outreach_draft.py):
DB-persisted draft queue. Every message starts as status='draft'.
Fields: company, channel, subject, body, followups, sector, scores,
status (draft→approved→sent→replied→opted_out→bounced), timestamps.
2. Daily Pipeline (automation.py → /daily-pipeline/run):
Generates N targets per sector/city, runs compliance check,
creates personalized emails with Arabic pain maps, stores as
draft rows in DB. Returns batch_id for approval.
3. Draft Queue API (drafts.py):
- GET /drafts — list by status/channel/batch
- GET /drafts/stats — counts per status
- GET /drafts/{id} — full draft with body + followups
- POST /drafts/{id}/approve — mark approved
- POST /drafts/approve-batch — approve entire batch
- POST /drafts/{id}/send — dispatch via email/whatsapp/sms
- POST /drafts/{id}/skip — archive draft
- PATCH /drafts/{id} — edit before approving
- POST /drafts/{id}/log-reply — paste reply → auto-classify →
generate suggested response → update status
4. Send dispatch uses existing integrations:
- Email: integrations/email_sender.py (SMTP)
- WhatsApp: integrations/whatsapp.py (Business API + mock)
- SMS: integrations/sms.py (Unifonic)
- LinkedIn: manual_required (copy from dashboard)
Safety:
- All drafts require approval (approval_required=True default)
- Unsubscribe reply → immediate opt_out status
- Compliance gate blocks: opt_out, bounced, high_risk, no_source
- Personal email → warning to use manual channel
- Rate limits enforced at send level
https://claude.ai/code/session_01W1rJthWDkasijTdXCfxVHs
78 lines
3.1 KiB
Python
78 lines
3.1 KiB
Python
"""OutreachDraft — DB-persisted draft queue for all outreach channels.
|
|
|
|
Every generated message starts as status='draft'. Sami reviews and
|
|
approves before any send. Approved drafts are dispatched via existing
|
|
Celery send tasks.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Dict, Optional
|
|
|
|
from sqlalchemy import Column, String, Integer, Boolean, DateTime, Text, JSON
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
|
|
try:
|
|
from app.database import Base
|
|
except ImportError:
|
|
from sqlalchemy.orm import DeclarativeBase
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
|
|
class OutreachDraft(Base):
|
|
__tablename__ = "outreach_drafts"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
batch_id = Column(String(64), index=True)
|
|
company = Column(String(255), nullable=False)
|
|
contact_name = Column(String(255), default="")
|
|
contact_email = Column(String(255), default="")
|
|
contact_phone = Column(String(32), default="")
|
|
channel = Column(String(20), nullable=False) # email | whatsapp | sms | linkedin
|
|
subject = Column(String(500), default="")
|
|
body = Column(Text, nullable=False)
|
|
followup_2d = Column(Text, default="")
|
|
followup_5d = Column(Text, default="")
|
|
call_script = Column(Text, default="")
|
|
sector = Column(String(100), default="")
|
|
city = Column(String(100), default="")
|
|
pain_hypothesis = Column(Text, default="")
|
|
fit_score = Column(Integer, default=0)
|
|
risk_score = Column(Integer, default=0)
|
|
status = Column(String(20), default="draft", index=True)
|
|
# draft | approved | sent | replied | opted_out | bounced | skipped
|
|
approval_required = Column(Boolean, default=True)
|
|
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
|
approved_at = Column(DateTime(timezone=True), nullable=True)
|
|
sent_at = Column(DateTime(timezone=True), nullable=True)
|
|
replied_at = Column(DateTime(timezone=True), nullable=True)
|
|
reply_text = Column(Text, nullable=True)
|
|
reply_category = Column(String(50), nullable=True)
|
|
next_action = Column(String(100), nullable=True)
|
|
source = Column(String(100), default="daily_pipeline")
|
|
metadata_ = Column("metadata", JSON, default=dict)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"id": str(self.id),
|
|
"batch_id": self.batch_id,
|
|
"company": self.company,
|
|
"contact_name": self.contact_name,
|
|
"contact_email": self.contact_email,
|
|
"channel": self.channel,
|
|
"subject": self.subject,
|
|
"body": self.body[:200] + "..." if len(self.body or "") > 200 else self.body,
|
|
"sector": self.sector,
|
|
"city": self.city,
|
|
"fit_score": self.fit_score,
|
|
"risk_score": self.risk_score,
|
|
"status": self.status,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"sent_at": self.sent_at.isoformat() if self.sent_at else None,
|
|
"reply_category": self.reply_category,
|
|
"next_action": self.next_action,
|
|
}
|