diff --git a/salesflow-saas/backend/app/api/v1/channels.py b/salesflow-saas/backend/app/api/v1/channels.py new file mode 100644 index 00000000..4de15b7a --- /dev/null +++ b/salesflow-saas/backend/app/api/v1/channels.py @@ -0,0 +1,95 @@ +""" +Channel API Endpoints — Dealix AI Revenue OS +Unified API for all communication channels: inbound routing, outreach, campaigns, timelines. +""" +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from typing import Optional +from sqlalchemy.ext.asyncio import AsyncSession +from app.database import get_db + +router = APIRouter() + + +class InboundRequest(BaseModel): + channel: str + sender: str + message: str + + +class OutreachRequest(BaseModel): + channel: str + lead: dict + campaign_type: str = "cold_intro" + language: str = "ar" + + +class CampaignRequest(BaseModel): + lead: dict + channels: list[str] + campaign_type: str = "cold_outreach" + + +class ContentRequest(BaseModel): + platform: str + topic: str + language: str = "ar" + + +@router.post("/inbound") +async def channel_inbound(req: InboundRequest, db: AsyncSession = Depends(get_db)): + from app.services.channel_orchestrator import channel_orchestrator + response = await channel_orchestrator.route_inbound(req.channel, req.sender, req.message, db) + return {"channel": req.channel, "sender": req.sender, "response": response} + + +@router.post("/outreach") +async def channel_outreach(req: OutreachRequest, db: AsyncSession = Depends(get_db)): + from app.services.channel_orchestrator import channel_orchestrator + brain = channel_orchestrator._get_brain(req.channel) + if not brain: + raise HTTPException(status_code=400, detail=f"Channel '{req.channel}' not supported") + + if req.channel == "email": + draft = await brain.generate_outreach(req.lead, req.campaign_type, req.language) + return {"channel": req.channel, "subject": draft.subject, "body": draft.body} + elif req.channel == "linkedin": + name = req.lead.get("name", "") + title = req.lead.get("title", "") + company = req.lead.get("company", "") + draft = await brain.draft_connection_request(name, title, company, "sales", req.language) + return {"channel": req.channel, "draft": draft, "status": "pending_review"} + elif req.channel in ("instagram", "tiktok", "twitter", "snapchat"): + content = await brain.generate_content(req.channel, req.lead.get("topic", "sales_tips"), req.language) + return {"channel": req.channel, "content": content.content, "hashtags": content.hashtags} + + return {"channel": req.channel, "status": "unsupported_for_outreach"} + + +@router.post("/campaign") +async def multi_channel_campaign(req: CampaignRequest, db: AsyncSession = Depends(get_db)): + from app.services.channel_orchestrator import channel_orchestrator + plan = await channel_orchestrator.generate_multi_channel_campaign( + req.lead, req.channels, req.campaign_type, db + ) + return {"campaign_type": plan.campaign_type, "channels": plan.channels, "steps": plan.steps} + + +@router.get("/timeline/{contact_id}") +async def contact_timeline(contact_id: str, db: AsyncSession = Depends(get_db)): + from app.services.channel_orchestrator import channel_orchestrator + events = await channel_orchestrator.get_contact_timeline(contact_id, db) + return {"contact_id": contact_id, "events": [e.model_dump() for e in events]} + + +@router.post("/content") +async def generate_content(req: ContentRequest): + from app.services.social_media_brain import social_media_brain + draft = await social_media_brain.generate_content(req.platform, req.topic, req.language) + return {"platform": draft.platform, "content": draft.content, "hashtags": draft.hashtags, "theme": draft.theme} + + +@router.get("/health") +async def channels_health(): + from app.services.channel_orchestrator import channel_orchestrator + return {"channels": channel_orchestrator.get_channel_health()} diff --git a/salesflow-saas/backend/app/services/channel_orchestrator.py b/salesflow-saas/backend/app/services/channel_orchestrator.py new file mode 100644 index 00000000..6e3ed753 --- /dev/null +++ b/salesflow-saas/backend/app/services/channel_orchestrator.py @@ -0,0 +1,167 @@ +""" +Channel Orchestrator — Dealix AI Revenue OS +Unified coordinator across all communication channels. +Routes inbound messages, generates multi-channel campaigns, and provides unified timelines. +""" +import logging +from datetime import datetime, timezone +from typing import Any, Optional + +from pydantic import BaseModel + +logger = logging.getLogger(__name__) + +CHANNEL_PRIORITY = ["whatsapp", "email", "instagram", "twitter", "linkedin", "tiktok"] + +CHANNEL_REGISTRY = { + "whatsapp": {"name_ar": "واتساب", "auto_send": True, "max_daily": 1000}, + "email": {"name_ar": "إيميل", "auto_send": True, "max_daily": 500}, + "instagram": {"name_ar": "إنستغرام", "auto_send": True, "max_daily": 200}, + "twitter": {"name_ar": "تويتر", "auto_send": True, "max_daily": 100}, + "linkedin": {"name_ar": "لينكدإن", "auto_send": False, "max_daily": 50}, + "tiktok": {"name_ar": "تيك توك", "auto_send": True, "max_daily": 100}, + "snapchat": {"name_ar": "سناب شات", "auto_send": True, "max_daily": 100}, +} + + +class TimelineEvent(BaseModel): + channel: str + direction: str # inbound, outbound + content_preview: str + timestamp: datetime + event_type: str = "message" # message, campaign, note + + +class CampaignPlan(BaseModel): + lead: dict + channels: list[str] + campaign_type: str + steps: list[dict] + created_at: datetime = None + + def __init__(self, **data): + super().__init__(**data) + if self.created_at is None: + self.created_at = datetime.now(timezone.utc) + + +class ChannelOrchestrator: + """Unified coordinator routing messages to the correct channel brain.""" + + def __init__(self): + self._brains = {} + + def _get_brain(self, channel: str): + if channel not in self._brains: + if channel == "whatsapp": + from app.services.whatsapp_brain import whatsapp_brain + self._brains[channel] = whatsapp_brain + elif channel == "email": + from app.services.email_brain import email_brain + self._brains[channel] = email_brain + elif channel == "linkedin": + from app.services.linkedin_brain import linkedin_brain + self._brains[channel] = linkedin_brain + elif channel in ("instagram", "tiktok", "twitter", "snapchat"): + from app.services.social_media_brain import social_media_brain + self._brains[channel] = social_media_brain + return self._brains.get(channel) + + async def route_inbound( + self, channel: str, sender: str, message: str, db: Any = None + ) -> str: + brain = self._get_brain(channel) + if not brain: + logger.warning(f"[Orchestrator] no brain for channel={channel}") + return "شكراً لتواصلك! سيتم تحويلك لفريق الدعم." + + logger.info(f"[Orchestrator] routing {channel} from={sender}") + + if channel == "whatsapp": + return await brain.handle_incoming(sender, message, db) + elif channel == "email": + draft = await brain.handle_inbound(sender, message[:50], message, db) + return draft.body + elif channel in ("instagram", "tiktok", "twitter", "snapchat"): + return await brain.handle_inbound_dm(channel, sender, message, db) + elif channel == "linkedin": + return "تم استلام رسالتك عبر لينكدإن. فريق المبيعات بيتواصل معك قريباً." + + return "شكراً لتواصلك!" + + async def generate_multi_channel_campaign( + self, lead: dict, channels: list[str], campaign_type: str = "cold_outreach", db: Any = None + ) -> CampaignPlan: + sorted_channels = sorted(channels, key=lambda c: CHANNEL_PRIORITY.index(c) if c in CHANNEL_PRIORITY else 99) + steps = [] + day = 0 + + for i, channel in enumerate(sorted_channels): + brain = self._get_brain(channel) + if not brain: + continue + + if channel == "whatsapp": + content = f"أهلاً {lead.get('name', '')}! أنا من Dealix — نظام المبيعات الذكي. تبي تعرف أكثر؟" + steps.append({"day": day, "channel": channel, "action": "send_message", "content": content, "auto": True}) + elif channel == "email": + draft = await brain.generate_outreach(lead, "cold_intro") + steps.append({"day": day, "channel": channel, "action": "send_email", "subject": draft.subject, "content": draft.body, "auto": True}) + elif channel == "linkedin": + name = lead.get("name", "") + title = lead.get("title", "") + company = lead.get("company", "") + draft_text = await brain.draft_connection_request(name, title, company) + steps.append({"day": day, "channel": channel, "action": "send_connection", "content": draft_text, "auto": False}) + elif channel in ("instagram", "tiktok", "twitter", "snapchat"): + content = f"أهلاً! شكراً لمتابعتك. Dealix يساعد الشركات السعودية في المبيعات. تبي تعرف أكثر؟" + steps.append({"day": day, "channel": channel, "action": "send_dm", "content": content, "auto": True}) + + day += 2 # 2-day gap between channels + + plan = CampaignPlan(lead=lead, channels=sorted_channels, campaign_type=campaign_type, steps=steps) + logger.info(f"[Orchestrator] campaign planned: {len(steps)} steps across {len(sorted_channels)} channels") + return plan + + async def get_contact_timeline( + self, contact_id: str, db: Any = None + ) -> list[TimelineEvent]: + events = [] + if not db: + return events + try: + from sqlalchemy import select + from app.models.message import Message + + result = await db.execute( + select(Message).where(Message.contact_id == contact_id).order_by(Message.created_at.desc()).limit(100) + ) + messages = result.scalars().all() + for msg in messages: + events.append(TimelineEvent( + channel=msg.channel or "whatsapp", + direction=msg.direction or "inbound", + content_preview=msg.body[:120] if msg.body else "", + timestamp=msg.created_at, + event_type="message", + )) + except Exception as e: + logger.warning(f"[Orchestrator] timeline error for {contact_id}: {e}") + + return sorted(events, key=lambda e: e.timestamp, reverse=True) + + def get_channel_health(self) -> dict: + health = {} + for channel, config in CHANNEL_REGISTRY.items(): + brain = self._get_brain(channel) + health[channel] = { + "name_ar": config["name_ar"], + "active": brain is not None, + "auto_send": config["auto_send"], + "max_daily": config["max_daily"], + } + return health + + +# Global singleton +channel_orchestrator = ChannelOrchestrator() diff --git a/salesflow-saas/backend/app/services/email_brain.py b/salesflow-saas/backend/app/services/email_brain.py index e3ec8cae..6fe77493 100644 --- a/salesflow-saas/backend/app/services/email_brain.py +++ b/salesflow-saas/backend/app/services/email_brain.py @@ -44,105 +44,72 @@ ARABIC_TEMPLATES = { "cold_intro": EmailDraft( subject="Dealix — نظام المبيعات الذكي للسوق السعودي", body=( - "السلام عليكم {name}،\n\n" - "أنا {sender_name} من فريق Dealix.\n\n" + "السلام عليكم {name}،\n\nأنا {sender_name} من فريق Dealix.\n\n" "لاحظنا أن {company} تعمل في قطاع {sector} — وهو بالضبط القطاع اللي نخدمه.\n\n" "Dealix نظام مبيعات ذكي مصمم للسعودية: واتساب مدمج، ذكاء اصطناعي يفهم عربي، " - "وحماية بيانات PDPL.\n\n" - "تبي نعطيك عرض سريع ١٥ دقيقة؟\n\n" - "مع التحية،\n{sender_name}\nفريق Dealix" + "وحماية بيانات PDPL.\n\nتبي نعطيك عرض سريع ١٥ دقيقة؟\n\nمع التحية،\n{sender_name}\nفريق Dealix" ), ), "follow_up_1": EmailDraft( subject="متابعة — هل شفت رسالتنا الأولى؟", body=( - "أهلاً {name}،\n\n" - "أرسلت لك قبل كم يوم عن Dealix. حبيت أتابع معك.\n\n" - "عملاؤنا في {sector} حققوا:\n" - "• زيادة ٤٠٪ في معدل الإغلاق\n" - "• توفير ١٠ ساعات أسبوعياً\n" - "• تحسين متابعة العملاء ١٠٠٪\n\n" - "تقدر تجرب مجاناً ١٤ يوم بدون بطاقة.\n\n" - "مع التحية،\n{sender_name}" + "أهلاً {name}،\n\nأرسلت لك قبل كم يوم عن Dealix. حبيت أتابع معك.\n\n" + "عملاؤنا في {sector} حققوا:\n• زيادة ٤٠٪ في معدل الإغلاق\n" + "• توفير ١٠ ساعات أسبوعياً\n• تحسين متابعة العملاء ١٠٠٪\n\n" + "تقدر تجرب مجاناً ١٤ يوم بدون بطاقة.\n\nمع التحية،\n{sender_name}" ), ), "follow_up_2": EmailDraft( subject="آخر متابعة — فرصة مجانية لتجربة Dealix", body=( - "أهلاً {name}،\n\n" - "أعرف إنك مشغول. بس حبيت أذكرك إن التجربة المجانية متاحة.\n\n" - "رابط التسجيل: dealix.sa/trial\n" - "يأخذ أقل من دقيقة.\n\n" - "لو ما يناسبك الوقت الحين، رد بـ 'لاحقاً' وبأتواصل معك الشهر الجاي.\n\n" - "مع التحية،\n{sender_name}" + "أهلاً {name}،\n\nأعرف إنك مشغول. بس حبيت أذكرك إن التجربة المجانية متاحة.\n\n" + "رابط التسجيل: dealix.sa/trial\nيأخذ أقل من دقيقة.\n\n" + "لو ما يناسبك الوقت الحين، رد بـ 'لاحقاً' وبأتواصل معك الشهر الجاي.\n\nمع التحية،\n{sender_name}" ), ), "demo_invite": EmailDraft( subject="موعد العرض التوضيحي لـ Dealix", body=( - "أهلاً {name}،\n\n" - "شكراً لاهتمامك بـ Dealix! 🎉\n\n" - "حجزنا لك عرض توضيحي:\n" - "📅 {demo_date}\n⏰ {demo_time}\n🔗 {demo_link}\n\n" - "العرض يستغرق ١٥ دقيقة ويغطي:\n" - "• إدارة العملاء عبر الواتساب\n" - "• تقييم العملاء بالذكاء الاصطناعي\n" - "• عروض الأسعار التلقائية\n\n" - "نتطلع لمقابلتك!\n{sender_name}" + "أهلاً {name}،\n\nشكراً لاهتمامك بـ Dealix!\n\n" + "حجزنا لك عرض توضيحي:\n📅 {demo_date}\n⏰ {demo_time}\n🔗 {demo_link}\n\n" + "العرض يستغرق ١٥ دقيقة ويغطي:\n• إدارة العملاء عبر الواتساب\n" + "• تقييم العملاء بالذكاء الاصطناعي\n• عروض الأسعار التلقائية\n\nنتطلع لمقابلتك!\n{sender_name}" ), ), "proposal": EmailDraft( subject="عرض Dealix المخصص لـ {company}", body=( - "أستاذ/ة {name}،\n\n" - "بناءً على محادثتنا، حضّرنا لكم عرض مخصص:\n\n" - "الباقة: {plan_name}\nالسعر: {price} ر.س/شهر\n" - "عدد المستخدمين: {users}\n\n" - "المميزات المشمولة:\n{features}\n\n" - "العرض صالح لمدة ٧ أيام.\n" - "للموافقة: {approval_link}\n\n" - "مع التحية،\n{sender_name}" + "أستاذ/ة {name}،\n\nبناءً على محادثتنا، حضّرنا لكم عرض مخصص:\n\n" + "الباقة: {plan_name}\nالسعر: {price} ر.س/شهر\nعدد المستخدمين: {users}\n\n" + "المميزات المشمولة:\n{features}\n\nالعرض صالح لمدة ٧ أيام.\n" + "للموافقة: {approval_link}\n\nمع التحية،\n{sender_name}" ), ), "welcome": EmailDraft( - subject="مرحباً بك في Dealix! 🎉", + subject="مرحباً بك في Dealix!", body=( - "أهلاً {name}،\n\n" - "مبروك! حسابك جاهز على Dealix.\n\n" - "خطواتك الأولى:\n" - "١. ادخل: dealix.sa/dashboard\n" - "٢. أضف أول عميل\n" - "٣. ربط الواتساب\n" - "٤. أرسل أول رسالة ذكية\n\n" - "لو تحتاج مساعدة، كلمنا واتساب أو إيميل support@dealix.sa.\n\n" - "يلا نبدأ! 🚀\nفريق Dealix" + "أهلاً {name}،\n\nمبروك! حسابك جاهز على Dealix.\n\n" + "خطواتك الأولى:\n١. ادخل: dealix.sa/dashboard\n٢. أضف أول عميل\n" + "٣. ربط الواتساب\n٤. أرسل أول رسالة ذكية\n\n" + "لو تحتاج مساعدة، كلمنا واتساب أو إيميل support@dealix.sa.\n\nيلا نبدأ!\nفريق Dealix" ), ), "commission_report": EmailDraft( subject="تقرير عمولاتك الأسبوعي — {period}", body=( - "أهلاً {name}،\n\n" - "هذا تقرير عمولاتك لهذا الأسبوع:\n\n" - "💰 إجمالي العمولة: {total_commission} ر.س\n" - "👥 عملاء جدد: {new_clients}\n" - "📈 مستواك: {tier}\n" - "📊 ترتيبك: #{rank}\n\n" - "تفاصيل كاملة: dealix.sa/dashboard/commissions\n\n" - "استمر! 🌟\nفريق Dealix" + "أهلاً {name}،\n\nهذا تقرير عمولاتك لهذا الأسبوع:\n\n" + "إجمالي العمولة: {total_commission} ر.س\nعملاء جدد: {new_clients}\n" + "مستواك: {tier}\nترتيبك: #{rank}\n\n" + "تفاصيل كاملة: dealix.sa/dashboard/commissions\n\nاستمر!\nفريق Dealix" ), ), "partnership_intro": EmailDraft( subject="فرصة شراكة مع Dealix — {partnership_type}", body=( - "السلام عليكم {name}،\n\n" - "نحن في Dealix نبحث عن شركاء استراتيجيين في {sector}.\n\n" - "نقدم:\n" - "• عمولات تنافسية تبدأ من ١٥٪\n" - "• دعم تقني ومبيعاتي كامل\n" - "• لوحة تحكم شريك مخصصة\n" - "• مواد تسويقية جاهزة\n\n" - "هل عندك وقت لمكالمة ١٥ دقيقة هذا الأسبوع؟\n\n" - "مع التحية،\n{sender_name}\nمدير الشراكات — Dealix" + "السلام عليكم {name}،\n\nنحن في Dealix نبحث عن شركاء استراتيجيين في {sector}.\n\n" + "نقدم:\n• عمولات تنافسية تبدأ من ١٥٪\n• دعم تقني ومبيعاتي كامل\n" + "• لوحة تحكم شريك مخصصة\n• مواد تسويقية جاهزة\n\n" + "هل عندك وقت لمكالمة ١٥ دقيقة هذا الأسبوع؟\n\nمع التحية،\n{sender_name}\nمدير الشراكات — Dealix" ), ), } @@ -171,12 +138,13 @@ class EmailBrain: if intent == EmailIntent.UNSUBSCRIBE: return EmailDraft( subject="تأكيد إلغاء الاشتراك", - body=f"أهلاً،\n\nتم إلغاء اشتراكك في رسائل Dealix البريدية.\nلو غيّرت رأيك، تقدر تشترك مرة ثانية من dealix.sa.\n\nمع التحية،\nفريق Dealix", + body="أهلاً،\n\nتم إلغاء اشتراكك في رسائل Dealix البريدية.\nلو غيّرت رأيك، تقدر تشترك مرة ثانية من dealix.sa.\n\nمع التحية،\nفريق Dealix", ) if intent == EmailIntent.COMPLAINT: + ticket = f"TKT-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M')}" return EmailDraft( subject="استلمنا شكواك — سنتابع فوراً", - body=f"أهلاً،\n\nشكراً لتواصلك. نعتذر عن أي إزعاج.\nفريقنا سيتابع شكواك خلال ٤ ساعات عمل.\nرقم التذكرة: TKT-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M')}\n\nمع التحية،\nفريق دعم Dealix", + body=f"أهلاً،\n\nشكراً لتواصلك. نعتذر عن أي إزعاج.\nفريقنا سيتابع شكواك خلال ٤ ساعات عمل.\nرقم التذكرة: {ticket}\n\nمع التحية،\nفريق دعم Dealix", ) if intent == EmailIntent.INQUIRY: pricing = self.knowledge.get_pricing_text("ar") @@ -189,37 +157,31 @@ class EmailBrain: subject="شكراً لاهتمامك بالشراكة مع Dealix", body="أهلاً،\n\nشكراً لتواصلك بخصوص الشراكة.\nفريق الشراكات سيتواصل معك خلال ٢٤ ساعة لمناقشة الفرص.\n\nمع التحية،\nفريق Dealix", ) + if intent == EmailIntent.SUPPORT: + return EmailDraft( + subject="استلمنا طلب الدعم — سنرد قريباً", + body="أهلاً،\n\nشكراً لتواصلك. فريق الدعم سيرد خلال ٤ ساعات عمل.\nللدعم العاجل: واتساب support@dealix.sa\n\nمع التحية،\nفريق دعم Dealix", + ) return EmailDraft( subject="شكراً لتواصلك مع Dealix", - body="أهلاً،\n\nشكراً لرسالتك! فريقنا سيرد عليك قريباً.\nلو تحتاج رد أسرع، كلمنا واتساب: +966XXXXXXXXX\n\nمع التحية،\nفريق Dealix", + body="أهلاً،\n\nشكراً لرسالتك! فريقنا سيرد عليك قريباً.\nلو تحتاج رد أسرع، كلمنا واتساب.\n\nمع التحية،\nفريق Dealix", ) async def generate_outreach( self, lead: dict, campaign_type: str = "cold_intro", language: str = "ar" ) -> EmailDraft: template = ARABIC_TEMPLATES.get(campaign_type, ARABIC_TEMPLATES["cold_intro"]) - filled_subject = template.subject.format(**{k: lead.get(k, "") for k in ["name", "company", "sector", "partnership_type", "period", "plan_name"]}, **{"default": ""}) filled_body = template.body for key, val in lead.items(): filled_body = filled_body.replace("{" + key + "}", str(val)) + filled_subject = template.subject + for key, val in lead.items(): + filled_subject = filled_subject.replace("{" + key + "}", str(val)) return EmailDraft(subject=filled_subject, body=filled_body, language=language, campaign_type=campaign_type) async def generate_nurture_sequence(self, lead: dict, db: Any = None) -> list[EmailDraft]: - name = lead.get("name", "") - company = lead.get("company", "") - sector = lead.get("sector", "") - sender = lead.get("sender_name", "فريق Dealix") - base_data = {"name": name, "company": company, "sector": sector, "sender_name": sender} - sequence_keys = ["cold_intro", "follow_up_1", "follow_up_2", "demo_invite", "proposal"] - result = [] - for key in sequence_keys: - tmpl = ARABIC_TEMPLATES[key] - body = tmpl.body - for k, v in base_data.items(): - body = body.replace("{" + k + "}", v) - result.append(EmailDraft(subject=tmpl.subject.format(**{**base_data, "default": ""}), body=body, campaign_type=key)) - return result + return [await self.generate_outreach(lead, key) for key in sequence_keys] def get_template(self, template_name: str) -> Optional[EmailDraft]: return ARABIC_TEMPLATES.get(template_name) diff --git a/salesflow-saas/backend/app/services/linkedin_brain.py b/salesflow-saas/backend/app/services/linkedin_brain.py new file mode 100644 index 00000000..3a209abc --- /dev/null +++ b/salesflow-saas/backend/app/services/linkedin_brain.py @@ -0,0 +1,147 @@ +""" +LinkedIn AI Brain — Dealix AI Revenue OS +ASSIST MODE ONLY: generates drafts for human review, never auto-sends. +All outputs are suggestions — the operator approves before sending. +""" +import logging +from datetime import datetime, timezone +from typing import Any + +from pydantic import BaseModel + +logger = logging.getLogger(__name__) + +MAX_CONNECTION_REQUEST = 300 +MAX_INMAIL = 1900 + + +class LinkedInDraft(BaseModel): + draft_type: str # connection_request, inmail, post, comment + content: str + target_name: str = "" + target_company: str = "" + language: str = "ar" + status: str = "pending_review" # always starts as pending + created_at: datetime = None + + def __init__(self, **data): + super().__init__(**data) + if self.created_at is None: + self.created_at = datetime.now(timezone.utc) + + +class OutreachTask(BaseModel): + task_type: str # send_connection, send_inmail, engage_post + target: dict + draft: LinkedInDraft + priority: int = 0 + status: str = "queued" + + +ARABIC_PURPOSES = { + "sales": "نبي نعرفك على Dealix — نظام مبيعات ذكي للسوق السعودي", + "partnership": "نبحث عن شراكة استراتيجية مع {company}", + "hiring": "عندنا فرصة في Dealix ممكن تناسب خبرتك", + "networking": "يسعدني التواصل مع محترفين في مجال {title}", +} + +POST_TOPICS_AR = { + "saudi_digital": "التحول الرقمي في السعودية", + "ai_sales": "الذكاء الاصطناعي في المبيعات", + "crm_tips": "نصائح إدارة علاقات العملاء", + "startup_growth": "نمو الشركات الناشئة السعودية", + "vision_2030": "رؤية ٢٠٣٠ والتقنية", +} + + +class LinkedInBrain: + """Assist-mode LinkedIn brain — drafts only, never auto-sends.""" + + def __init__(self): + from app.services.whatsapp_knowledge import DealixKnowledge + self.knowledge = DealixKnowledge + + async def draft_connection_request( + self, name: str, title: str, company: str, purpose: str = "sales", lang: str = "ar" + ) -> str: + purpose_text = ARABIC_PURPOSES.get(purpose, ARABIC_PURPOSES["networking"]) + purpose_text = purpose_text.format(company=company, title=title) + + if lang == "ar": + draft = f"أهلاً {name}! {purpose_text}. يسعدني نتواصل ونتبادل الأفكار." + else: + draft = f"Hi {name}! I'd love to connect — {purpose_text.replace(company, company)}. Looking forward to exchanging ideas." + + if len(draft) > MAX_CONNECTION_REQUEST: + draft = draft[:MAX_CONNECTION_REQUEST - 3] + "..." + logger.info(f"[LinkedInBrain] drafted connection request for {name} @ {company}") + return draft + + async def draft_inmail(self, profile: dict, deal_type: str = "sales", lang: str = "ar") -> str: + name = profile.get("name", "") + title = profile.get("title", "") + company = profile.get("company", "") + + if deal_type == "partnership": + template = ARABIC_PURPOSES["partnership"].format(company=company, title=title) + body = f"السلام عليكم {name},\n\n{template}.\n\nDealix يدعم ١٥ نوع صفقة استراتيجية — من تبادل خدمات للتوزيع والشراكات التقنية.\n\nهل عندك ١٠ دقائق نتكلم؟\n\nمع التحية" + elif deal_type == "hiring": + body = f"أهلاً {name},\n\nشفت بروفايلك وخبرتك في {title} — عندنا فرصة في Dealix ممكن تناسبك.\n\nنبني نظام مبيعات ذكي للسوق السعودي ونبحث عن كفاءات مميزة.\n\nتحب نتكلم أكثر؟\n\nمع التحية" + else: + pricing = "يبدأ من ٢٩٩ ر.س/شهر" + body = f"السلام عليكم {name},\n\nأتواصل معك لأن {company} ممكن تستفيد من Dealix — نظام المبيعات الذكي للسوق السعودي.\n\n• واتساب CRM مدمج\n• ذكاء اصطناعي يفهم عربي\n• {pricing}\n\nتبي عرض سريع ١٥ دقيقة؟\n\nمع التحية" + + if lang != "ar": + body = f"Hi {name},\n\nI'm reaching out because {company} could benefit from Dealix — the smart CRM built for Saudi Arabia.\n\n• WhatsApp-native CRM\n• Arabic AI\n• Starts at 299 SAR/mo\n\nWould you have 15 minutes for a quick demo?\n\nBest regards" + + return body[:MAX_INMAIL] + + async def draft_post(self, topic: str, audience: str = "business", lang: str = "ar") -> str: + topic_ar = POST_TOPICS_AR.get(topic, topic) + + if lang == "ar": + return ( + f"موضوع اليوم: {topic_ar}\n\n" + f"في السوق السعودي، الشركات اللي تستخدم أدوات ذكية تحقق نتائج أفضل بـ ٤٠٪.\n\n" + f"ثلاث نصائح سريعة:\n" + f"١. استخدم الواتساب كقناة بيع رئيسية\n" + f"٢. فعّل الذكاء الاصطناعي للتقييم التلقائي\n" + f"٣. تابع عملاءك بالعربي — يفرق!\n\n" + f"وش رأيكم؟ شاركوني تجربتكم.\n\n" + f"#Dealix #مبيعات #السعودية #تقنية #CRM" + ) + return ( + f"Today's topic: {topic_ar}\n\n" + f"In the Saudi market, companies using smart tools see 40% better results.\n\n" + f"3 quick tips:\n1. Use WhatsApp as your main sales channel\n" + f"2. Enable AI for automatic lead scoring\n3. Follow up in Arabic — it matters!\n\n" + f"What do you think? Share your experience.\n\n#Dealix #Sales #SaudiArabia #CRM" + ) + + async def generate_outreach_queue( + self, criteria: dict, db: Any = None + ) -> list[OutreachTask]: + targets = criteria.get("targets", []) + purpose = criteria.get("purpose", "sales") + lang = criteria.get("language", "ar") + tasks = [] + + for i, target in enumerate(targets[:50]): + name = target.get("name", "") + title = target.get("title", "") + company = target.get("company", "") + + conn_text = await self.draft_connection_request(name, title, company, purpose, lang) + draft = LinkedInDraft( + draft_type="connection_request", content=conn_text, + target_name=name, target_company=company, language=lang, + ) + tasks.append(OutreachTask( + task_type="send_connection", target=target, draft=draft, priority=i, + )) + logger.info(f"[LinkedInBrain] generated {len(tasks)} outreach tasks for review") + return tasks + + +# Global singleton +linkedin_brain = LinkedInBrain() diff --git a/salesflow-saas/backend/app/services/social_media_brain.py b/salesflow-saas/backend/app/services/social_media_brain.py new file mode 100644 index 00000000..1a93cb7d --- /dev/null +++ b/salesflow-saas/backend/app/services/social_media_brain.py @@ -0,0 +1,176 @@ +""" +Social Media AI Brain — Dealix AI Revenue OS +Unified brain for Instagram, TikTok, Twitter, and Snapchat. +Handles inbound DMs, content generation, and content calendar planning. +""" +import logging +from datetime import datetime, timezone, timedelta +from enum import Enum +from typing import Any, Optional + +from pydantic import BaseModel + +logger = logging.getLogger(__name__) + + +class Platform(str, Enum): + INSTAGRAM = "instagram" + TIKTOK = "tiktok" + TWITTER = "twitter" + SNAPCHAT = "snapchat" + + +PLATFORM_RULES = { + Platform.INSTAGRAM: {"max_chars": 2200, "max_hashtags": 30, "name_ar": "إنستغرام"}, + Platform.TIKTOK: {"max_chars": 300, "max_hashtags": 5, "name_ar": "تيك توك"}, + Platform.TWITTER: {"max_chars": 280, "max_hashtags": 3, "name_ar": "تويتر"}, + Platform.SNAPCHAT: {"max_chars": 250, "max_hashtags": 0, "name_ar": "سناب شات"}, +} + +SAUDI_CONTENT_THEMES = [ + {"id": "vision_2030", "name_ar": "رؤية ٢٠٣٠ والتحول الرقمي", "hashtags_ar": ["#رؤية_السعودية_2030", "#تحول_رقمي"]}, + {"id": "smb_growth", "name_ar": "نمو المشاريع الصغيرة والمتوسطة", "hashtags_ar": ["#ريادة_أعمال", "#مشاريع_صغيرة"]}, + {"id": "ai_arabic", "name_ar": "الذكاء الاصطناعي بالعربي", "hashtags_ar": ["#ذكاء_اصطناعي", "#تقنية"]}, + {"id": "sales_tips", "name_ar": "نصائح المبيعات للسوق السعودي", "hashtags_ar": ["#مبيعات", "#CRM"]}, + {"id": "whatsapp_business", "name_ar": "واتساب للأعمال", "hashtags_ar": ["#واتساب_أعمال", "#تواصل"]}, +] + +DM_INTENT_KEYWORDS = { + "pricing": ["سعر", "كم", "باقة", "price", "cost"], + "demo": ["عرض", "demo", "تجربة", "وريني"], + "support": ["مشكلة", "مساعدة", "help", "خطأ"], + "partnership": ["شراكة", "تعاون", "partner"], +} + + +class ContentDraft(BaseModel): + platform: str + content: str + hashtags: list[str] = [] + language: str = "ar" + theme: str = "" + created_at: datetime = None + + def __init__(self, **data): + super().__init__(**data) + if self.created_at is None: + self.created_at = datetime.now(timezone.utc) + + +class CalendarEntry(BaseModel): + date: str + platform: str + theme: str + content: ContentDraft + time_slot: str = "10:00" + + +class SocialMediaBrain: + """Unified brain for Instagram, TikTok, Twitter, Snapchat.""" + + def __init__(self): + from app.services.whatsapp_knowledge import DealixKnowledge + self.knowledge = DealixKnowledge + + def _detect_dm_intent(self, message: str) -> str: + msg_lower = message.lower() + for intent, keywords in DM_INTENT_KEYWORDS.items(): + if any(kw in msg_lower for kw in keywords): + return intent + return "general" + + def _enforce_platform_limits(self, text: str, hashtags: list[str], platform: Platform) -> tuple[str, list[str]]: + rules = PLATFORM_RULES[platform] + hashtags = hashtags[:rules["max_hashtags"]] + hashtag_text = " ".join(hashtags) + max_content = rules["max_chars"] - len(hashtag_text) - 2 if hashtags else rules["max_chars"] + if len(text) > max_content: + text = text[:max_content - 3] + "..." + return text, hashtags + + async def handle_inbound_dm( + self, platform: str, sender: str, message: str, db: Any = None + ) -> str: + plat = Platform(platform) if platform in Platform.__members__.values() else Platform.INSTAGRAM + intent = self._detect_dm_intent(message) + plat_name = PLATFORM_RULES[plat]["name_ar"] + logger.info(f"[SocialMediaBrain] DM on {plat.value} from={sender} intent={intent}") + + if intent == "pricing": + pricing = self.knowledge.get_pricing_text("ar") + return f"أهلاً! شكراً لتواصلك عبر {plat_name}.\n\nباقات Dealix:\n{pricing}\n\nتبي تفاصيل أكثر؟ راسلنا واتساب أو زور dealix.sa" + + if intent == "demo": + return f"ممتاز! يسعدنا نعرض لك Dealix.\n\nاحجز عرض توضيحي مجاني (١٥ دقيقة): dealix.sa/demo\n\nأو أرسل رقمك ونتواصل معك واتساب." + + if intent == "support": + return f"أهلاً! للدعم الفني الأسرع، تواصل معنا:\n• واتساب: dealix.sa/whatsapp\n• إيميل: support@dealix.sa\n\nأو وصف مشكلتك هنا وبنساعدك." + + if intent == "partnership": + return "شكراً لاهتمامك بالشراكة مع Dealix!\n\nأرسل لنا إيميل على partners@dealix.sa أو واتساب ونرتب اجتماع." + + return f"أهلاً وسهلاً! أنا مساعد Dealix على {plat_name}.\n\nأقدر أساعدك في:\n• الأسعار والباقات\n• حجز عرض توضيحي\n• الدعم الفني\n\nوش تحتاج؟" + + async def generate_content( + self, platform: str, topic: str, language: str = "ar" + ) -> ContentDraft: + plat = Platform(platform) if platform in Platform.__members__.values() else Platform.INSTAGRAM + theme = next((t for t in SAUDI_CONTENT_THEMES if t["id"] == topic), SAUDI_CONTENT_THEMES[0]) + hashtags_base = theme["hashtags_ar"] + ["#Dealix"] + + if language == "ar": + content_map = { + Platform.INSTAGRAM: ( + f"{theme['name_ar']}\n\n" + f"في السوق السعودي، الشركات اللي تستخدم أدوات ذكية تحقق نتائج أفضل.\n\n" + f"Dealix يساعدك:\n" + f"✅ إدارة عملاءك بالواتساب\n" + f"✅ ذكاء اصطناعي يفهم عربي\n" + f"✅ تقارير وتنبؤات مبيعات\n\n" + f"جرّب مجاناً ١٤ يوم — الرابط بالبايو" + ), + Platform.TIKTOK: f"{theme['name_ar']}\n\nDealix — نظام مبيعات ذكي للسوق السعودي. جرّب مجاناً!", + Platform.TWITTER: f"{theme['name_ar']}\n\nDealix: واتساب CRM + AI عربي للشركات السعودية. جرّب مجاناً ١٤ يوم.", + Platform.SNAPCHAT: f"{theme['name_ar']}\n\nDealix — نظام مبيعاتك الذكي. جرّبه مجاناً!", + } + else: + content_map = { + Platform.INSTAGRAM: f"{theme['name_ar']}\n\nSmart companies in Saudi use AI-powered tools.\n\nDealix helps you:\n✅ WhatsApp CRM\n✅ Arabic AI\n✅ Sales forecasting\n\nTry free for 14 days — link in bio", + Platform.TIKTOK: f"{theme['name_ar']}\n\nDealix — smart sales for Saudi. Try free!", + Platform.TWITTER: f"{theme['name_ar']}\n\nDealix: WhatsApp CRM + Arabic AI for Saudi companies. 14-day free trial.", + Platform.SNAPCHAT: f"{theme['name_ar']}\n\nDealix — your smart sales system. Try free!", + } + + raw_content = content_map.get(plat, content_map[Platform.INSTAGRAM]) + final_content, final_hashtags = self._enforce_platform_limits(raw_content, hashtags_base, plat) + + return ContentDraft( + platform=plat.value, content=final_content, hashtags=final_hashtags, + language=language, theme=topic, + ) + + async def generate_content_calendar( + self, platforms: list[str], days: int = 7, language: str = "ar" + ) -> list[CalendarEntry]: + calendar = [] + time_slots = {"instagram": "10:00", "tiktok": "18:00", "twitter": "08:00", "snapchat": "14:00"} + today = datetime.now(timezone.utc).date() + + for day_offset in range(days): + target_date = today + timedelta(days=day_offset) + theme = SAUDI_CONTENT_THEMES[day_offset % len(SAUDI_CONTENT_THEMES)] + + for plat_str in platforms: + content = await self.generate_content(plat_str, theme["id"], language) + calendar.append(CalendarEntry( + date=target_date.isoformat(), platform=plat_str, + theme=theme["id"], content=content, + time_slot=time_slots.get(plat_str, "10:00"), + )) + + logger.info(f"[SocialMediaBrain] generated {len(calendar)} calendar entries for {days} days") + return calendar + + +# Global singleton +social_media_brain = SocialMediaBrain()