Spaces:
Sleeping
Sleeping
Create outreach_agent.py
Browse files- outreach_agent.py +136 -0
outreach_agent.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Dict, Any, Optional, List, Callable
|
| 3 |
+
|
| 4 |
+
class OutreachAgent:
|
| 5 |
+
def __init__(self, channel_handlers: Optional[Dict[str, Callable]]=None):
|
| 6 |
+
self.logger = logging.getLogger("OutreachAgent")
|
| 7 |
+
self.lead_assets = {}
|
| 8 |
+
self.analytics = []
|
| 9 |
+
self.channel_handlers = channel_handlers or {}
|
| 10 |
+
|
| 11 |
+
def intake_lead(self, lead_profile: Dict[str, Any]) -> Dict[str, Any]:
|
| 12 |
+
enriched_profile = self.validate_and_enrich_profile(lead_profile)
|
| 13 |
+
self.logger.info(f"Intake for {enriched_profile.get('name', 'Unknown Lead')}")
|
| 14 |
+
asset_id = enriched_profile.get('id')
|
| 15 |
+
if asset_id:
|
| 16 |
+
self.lead_assets[asset_id] = enriched_profile
|
| 17 |
+
return enriched_profile
|
| 18 |
+
|
| 19 |
+
def validate_and_enrich_profile(self, profile: Dict[str, Any]) -> Dict[str, Any]:
|
| 20 |
+
# Enrich with historical, persona, engagements, etc.
|
| 21 |
+
required_fields = ['persona', 'channel_fit']
|
| 22 |
+
for field in required_fields:
|
| 23 |
+
if field not in profile:
|
| 24 |
+
profile[field] = 'unspecified'
|
| 25 |
+
# Simulate enrichment logic
|
| 26 |
+
profile['validation'] = 'success'
|
| 27 |
+
if 'recent_posts' not in profile:
|
| 28 |
+
profile['recent_posts'] = []
|
| 29 |
+
return profile
|
| 30 |
+
|
| 31 |
+
def select_template(self, profile: Dict[str, Any]) -> str:
|
| 32 |
+
persona = profile.get('persona', 'Generic')
|
| 33 |
+
pain = profile.get('pain_opportunity', 'N/A')
|
| 34 |
+
buyer_signals = profile.get('buyer_signals', [])
|
| 35 |
+
channel = profile.get('channel_fit', 'email')
|
| 36 |
+
# Template selection logic (expandable)
|
| 37 |
+
template = f"Hi {{name}}, as a {{persona}}, we noticed: {{pain}}. {{buyer_signals}}. Ready to discuss via {{channel}}?"
|
| 38 |
+
return template
|
| 39 |
+
|
| 40 |
+
def tailor_message(self, profile: Dict[str, Any], template: str) -> str:
|
| 41 |
+
variables = {
|
| 42 |
+
"name": profile.get("name", "there"),
|
| 43 |
+
"persona": profile.get("persona", "Professional"),
|
| 44 |
+
"pain": profile.get("pain_opportunity", "key considerations"),
|
| 45 |
+
"buyer_signals": ', '.join(profile.get("buyer_signals", [])),
|
| 46 |
+
"channel": profile.get("channel_fit", "email"),
|
| 47 |
+
"context": profile.get("context_snippets", ""),
|
| 48 |
+
"recent_posts": '\n'.join(profile.get("recent_posts", [])),
|
| 49 |
+
"complaints": ', '.join(profile.get("customer_complaint_tags", [])),
|
| 50 |
+
"hooks": ', '.join(profile.get("competitor_industry_hooks", [])),
|
| 51 |
+
"urgency": profile.get("urgency_triggers", "")
|
| 52 |
+
}
|
| 53 |
+
message = template.format(**variables)
|
| 54 |
+
# Attach recent posts, urgency, hooks etc with nuance
|
| 55 |
+
if variables["context"]:
|
| 56 |
+
message += f"\nContext: {variables['context']}"
|
| 57 |
+
if variables["recent_posts"]:
|
| 58 |
+
message += f"\nRecent posts: {variables['recent_posts']}"
|
| 59 |
+
if variables["complaints"]:
|
| 60 |
+
message += f"\nCustomer complaints: {variables['complaints']}"
|
| 61 |
+
if variables["hooks"]:
|
| 62 |
+
message += f"\nIndustry/competitor hooks: {variables['hooks']}"
|
| 63 |
+
if variables["urgency"]:
|
| 64 |
+
message += f"\nUrgency triggers: {variables['urgency']}"
|
| 65 |
+
return message
|
| 66 |
+
|
| 67 |
+
def deliver(self, profile: Dict[str, Any], message: str, channel: str) -> Dict[str, Any]:
|
| 68 |
+
# Channel handler pattern for extensibility
|
| 69 |
+
handler = self.channel_handlers.get(channel, self.default_handler)
|
| 70 |
+
result = handler(profile, message)
|
| 71 |
+
self.logger.info(f"Delivered to {profile.get('name')} via {channel}")
|
| 72 |
+
self.update_analytics(profile, channel, result)
|
| 73 |
+
return result
|
| 74 |
+
|
| 75 |
+
def default_handler(self, profile: Dict[str, Any], message: str) -> Dict[str, Any]:
|
| 76 |
+
# Simulate sending logic
|
| 77 |
+
return {"status": "sent", "channel": profile.get('channel_fit', 'email'), "message": message}
|
| 78 |
+
|
| 79 |
+
def browser_social_actions(self, profile: Dict[str, Any], action: str, extra: Optional[Dict[str, Any]]=None) -> Dict[str, Any]:
|
| 80 |
+
# Simulate browser agent: connect, DM, comment, like, milestone outreach
|
| 81 |
+
valid_actions = ["connect", "dm", "comment", "like", "milestone"]
|
| 82 |
+
if action not in valid_actions:
|
| 83 |
+
raise ValueError(f"Unsupported social action: {action}")
|
| 84 |
+
self.logger.info(f"Performed {action} for {profile.get('name')}")
|
| 85 |
+
result = {"action": action, "status": "success", "lead": profile.get('id')}
|
| 86 |
+
self.update_analytics(profile, "social_action", result)
|
| 87 |
+
return result
|
| 88 |
+
|
| 89 |
+
def update_analytics(self, profile: Dict[str, Any], channel: str, result: Dict[str, Any]):
|
| 90 |
+
entry = {
|
| 91 |
+
"lead_id": profile.get("id"),
|
| 92 |
+
"channel": channel,
|
| 93 |
+
"result": result
|
| 94 |
+
}
|
| 95 |
+
self.analytics.append(entry)
|
| 96 |
+
if profile.get("id") in self.lead_assets:
|
| 97 |
+
self.lead_assets[profile.get("id")]["last_engagement"] = entry
|
| 98 |
+
|
| 99 |
+
def feedback_loop(self, profile: Dict[str, Any], feedback: str):
|
| 100 |
+
# Store feedback and auto-update asset
|
| 101 |
+
pid = profile.get("id")
|
| 102 |
+
if pid and pid in self.lead_assets:
|
| 103 |
+
self.lead_assets[pid]["feedback"] = feedback
|
| 104 |
+
self.logger.info(f"Feedback for {profile.get('name')}: {feedback}")
|
| 105 |
+
|
| 106 |
+
def get_transcript(self, profile_id: str) -> Dict[str, Any]:
|
| 107 |
+
asset = self.lead_assets.get(profile_id)
|
| 108 |
+
return {
|
| 109 |
+
"profile": asset,
|
| 110 |
+
"last_message": asset.get("last_engagement", {}).get("result", {}).get("message", "") if asset else "",
|
| 111 |
+
"engagement_stats": [a for a in self.analytics if a.get("lead_id") == profile_id]
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
# Example usage handler for email
|
| 115 |
+
|
| 116 |
+
def email_handler(profile, message):
|
| 117 |
+
# Integrate real email API here
|
| 118 |
+
return {"status": "sent_via_email", "recipient": profile.get("email", "n/a"), "message": message}
|
| 119 |
+
|
| 120 |
+
# Example handlers for expansion for LinkedIn, SMS, browser agent, etc.
|
| 121 |
+
|
| 122 |
+
def linkedin_handler(profile, message):
|
| 123 |
+
# Integrate real LinkedIn API calls here
|
| 124 |
+
return {"status": "sent_via_linkedin", "recipient": profile.get("linkedin", "n/a"), "message": message}
|
| 125 |
+
|
| 126 |
+
def sms_handler(profile, message):
|
| 127 |
+
# Integrate real SMS API calls here
|
| 128 |
+
return {"status": "sent_via_sms", "recipient": profile.get("phone", "n/a"), "message": message}
|
| 129 |
+
|
| 130 |
+
# Usage:
|
| 131 |
+
# agent = OutreachAgent(channel_handlers={"email": email_handler, "linkedin": linkedin_handler, "sms": sms_handler})
|
| 132 |
+
# lead = agent.intake_lead({...})
|
| 133 |
+
# template = agent.select_template(lead)
|
| 134 |
+
# personalized_msg = agent.tailor_message(lead, template)
|
| 135 |
+
# agent.deliver(lead, personalized_msg, channel="email")
|
| 136 |
+
# agent.browser_social_actions(lead, "connect")
|