stackConsult commited on
Commit
0f64dc1
·
verified ·
1 Parent(s): 443db46

Create cadence_agent.py with core data structures and initialization

Browse files
Files changed (1) hide show
  1. cadence_agent.py +256 -0
cadence_agent.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cadence Agent - AI-driven multi-channel outreach orchestration
3
+
4
+ Handles intelligent sequencing, timing optimization, engagement-based branching,
5
+ personalization, compliance, rate limiting, and comprehensive error handling.
6
+
7
+ Author: StrategyStack Team
8
+ Date: November 2025
9
+ Version: 1.0.0
10
+ """
11
+
12
+ import logging
13
+ import json
14
+ import csv
15
+ from datetime import datetime, timedelta
16
+ from typing import Dict, List, Optional, Tuple, Any
17
+ from enum import Enum
18
+ import pytz
19
+ from dataclasses import dataclass, field, asdict
20
+ import yaml
21
+ import time
22
+ from collections import defaultdict
23
+ from pathlib import Path
24
+
25
+ # Configure logging
26
+ logging.basicConfig(
27
+ level=logging.INFO,
28
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
29
+ )
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class ChannelType(Enum):
34
+ """Enumeration of supported outreach channels"""
35
+ EMAIL = "email"
36
+ LINKEDIN = "linkedin"
37
+ SMS = "sms"
38
+ PHONE = "phone"
39
+ DIRECT_MAIL = "direct_mail"
40
+
41
+
42
+ class TouchpointStatus(Enum):
43
+ """Enumeration of touchpoint execution statuses"""
44
+ PENDING = "pending"
45
+ SCHEDULED = "scheduled"
46
+ SENT = "sent"
47
+ DELIVERED = "delivered"
48
+ OPENED = "opened"
49
+ CLICKED = "clicked"
50
+ REPLIED = "replied"
51
+ FAILED = "failed"
52
+ SKIPPED = "skipped"
53
+ CANCELLED = "cancelled"
54
+
55
+
56
+ class EngagementLevel(Enum):
57
+ """Enumeration of engagement levels for branching logic"""
58
+ HIGH = "high"
59
+ MEDIUM = "medium"
60
+ LOW = "low"
61
+ NEGATIVE = "negative"
62
+ UNKNOWN = "unknown"
63
+
64
+
65
+ class CadenceError(Exception):
66
+ """Base exception class for cadence-related errors"""
67
+ pass
68
+
69
+
70
+ class RateLimitError(CadenceError):
71
+ """Exception raised when rate limits are exceeded"""
72
+ pass
73
+
74
+
75
+ class ComplianceError(CadenceError):
76
+ """Exception raised for compliance violations"""
77
+ pass
78
+
79
+
80
+ class DataValidationError(CadenceError):
81
+ """Exception raised for data validation failures"""
82
+ pass
83
+
84
+
85
+ @dataclass
86
+ class Touchpoint:
87
+ """Represents a single touchpoint in a cadence sequence"""
88
+ step: int
89
+ day: int
90
+ channel: ChannelType
91
+ template: str
92
+ priority: str
93
+ conditions: Dict[str, Any] = field(default_factory=dict)
94
+ status: TouchpointStatus = TouchpointStatus.PENDING
95
+ scheduled_time: Optional[datetime] = None
96
+ sent_time: Optional[datetime] = None
97
+ engagement_data: Dict[str, Any] = field(default_factory=dict)
98
+ error_log: List[Dict[str, Any]] = field(default_factory=list)
99
+ retry_count: int = 0
100
+ max_retries: int = 3
101
+
102
+
103
+ @dataclass
104
+ class CadenceState:
105
+ """Represents the current state of a lead's cadence journey"""
106
+ lead_id: str
107
+ cadence_template: str
108
+ start_date: datetime
109
+ current_step: int = 0
110
+ touchpoints_completed: List[int] = field(default_factory=list)
111
+ touchpoints_pending: List[int] = field(default_factory=list)
112
+ engagement_level: EngagementLevel = EngagementLevel.UNKNOWN
113
+ engagement_score: float = 0.0
114
+ total_opens: int = 0
115
+ total_clicks: int = 0
116
+ total_replies: int = 0
117
+ opted_out: bool = False
118
+ suppressed: bool = False
119
+ paused: bool = False
120
+ completed: bool = False
121
+ last_touchpoint_date: Optional[datetime] = None
122
+ next_touchpoint_date: Optional[datetime] = None
123
+ metadata: Dict[str, Any] = field(default_factory=dict)
124
+ error_history: List[Dict[str, Any]] = field(default_factory=list)
125
+
126
+
127
+ @dataclass
128
+ class RateLimitTracker:
129
+ """Tracks rate limits per channel"""
130
+ channel: ChannelType
131
+ hourly_count: int = 0
132
+ daily_count: int = 0
133
+ hourly_reset_time: datetime = field(default_factory=datetime.now)
134
+ daily_reset_time: datetime = field(default_factory=datetime.now)
135
+ domain_counts: Dict[str, int] = field(default_factory=dict)
136
+ contact_counts: Dict[str, int] = field(default_factory=dict)
137
+
138
+
139
+ class CadenceAgent:
140
+ """
141
+ Main Cadence Agent class for orchestrating multi-channel outreach sequences.
142
+
143
+ Handles:
144
+ - Intelligent sequencing and timing optimization
145
+ - Engagement-based branching logic
146
+ - Personalization and context-aware messaging
147
+ - Compliance and consent management
148
+ - Rate limiting and throttling
149
+ - Comprehensive error handling and recovery
150
+ """
151
+
152
+ def __init__(self, config_path: str = "docs/configs/cadence_config.yaml"):
153
+ """
154
+ Initialize the Cadence Agent.
155
+
156
+ Args:
157
+ config_path: Path to cadence configuration YAML file
158
+
159
+ Raises:
160
+ FileNotFoundError: If config file not found
161
+ DataValidationError: If config validation fails
162
+ """
163
+ try:
164
+ self.config = self._load_config(config_path)
165
+ self.cadence_states: Dict[str, CadenceState] = {}
166
+ self.rate_limiters: Dict[ChannelType, RateLimitTracker] = {
167
+ channel: RateLimitTracker(channel=channel)
168
+ for channel in ChannelType
169
+ }
170
+ self.suppression_lists: Dict[str, set] = self._load_suppression_lists()
171
+
172
+ # State persistence files
173
+ self.state_file = Path("cadence_states.json")
174
+ self.activity_log = Path("cadence_activity_log.csv")
175
+ self.error_log = Path("cadence_error_log.csv")
176
+
177
+ # Load existing states if available
178
+ self._load_cadence_states()
179
+
180
+ # Initialize activity logging
181
+ self._initialize_activity_log()
182
+
183
+ logger.info("CadenceAgent initialized successfully")
184
+
185
+ except Exception as e:
186
+ logger.error(f"Failed to initialize CadenceAgent: {str(e)}")
187
+ raise DataValidationError(f"Initialization failed: {str(e)}")
188
+
189
+ def _load_config(self, config_path: str) -> Dict[str, Any]:
190
+ """
191
+ Load and validate cadence configuration.
192
+
193
+ Args:
194
+ config_path: Path to configuration file
195
+
196
+ Returns:
197
+ Validated configuration dictionary
198
+
199
+ Raises:
200
+ FileNotFoundError: If config file not found
201
+ DataValidationError: If config validation fails
202
+ """
203
+ try:
204
+ with open(config_path, 'r') as f:
205
+ config = yaml.safe_load(f)
206
+
207
+ # Validate required config sections
208
+ required_sections = [
209
+ 'cadence_templates',
210
+ 'touchpoint_sequence',
211
+ 'timing_optimization',
212
+ 'engagement_branching',
213
+ 'rate_limits',
214
+ 'compliance'
215
+ ]
216
+
217
+ for section in required_sections:
218
+ if section not in config:
219
+ raise DataValidationError(
220
+ f"Missing required config section: {section}"
221
+ )
222
+
223
+ logger.info(f"Configuration loaded from {config_path}")
224
+ return config
225
+
226
+ except FileNotFoundError:
227
+ logger.error(f"Configuration file not found: {config_path}")
228
+ raise
229
+ except yaml.YAMLError as e:
230
+ logger.error(f"YAML parsing error: {str(e)}")
231
+ raise DataValidationError(f"Invalid YAML configuration: {str(e)}")
232
+ except Exception as e:
233
+ logger.error(f"Unexpected error loading config: {str(e)}")
234
+ raise DataValidationError(f"Config load failed: {str(e)}")
235
+
236
+ def _load_suppression_lists(self) -> Dict[str, set]:
237
+ """
238
+ Load suppression lists from configuration.
239
+
240
+ Returns:
241
+ Dictionary of suppression lists
242
+
243
+ Note:
244
+ In production, these would be loaded from database or files.
245
+ This implementation uses placeholder data.
246
+ """
247
+ suppression_lists = {
248
+ 'global_unsubscribe': set(),
249
+ 'domain_blocklist': set(),
250
+ 'competitor_list': set(),
251
+ 'do_not_contact': set()
252
+ }
253
+
254
+ # TODO: Load from actual data sources
255
+ logger.info("Suppression lists loaded")
256
+ return suppression_lists