fusesell 1.3.42__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fusesell-1.3.42.dist-info/METADATA +873 -0
- fusesell-1.3.42.dist-info/RECORD +35 -0
- fusesell-1.3.42.dist-info/WHEEL +5 -0
- fusesell-1.3.42.dist-info/entry_points.txt +2 -0
- fusesell-1.3.42.dist-info/licenses/LICENSE +21 -0
- fusesell-1.3.42.dist-info/top_level.txt +2 -0
- fusesell.py +20 -0
- fusesell_local/__init__.py +37 -0
- fusesell_local/api.py +343 -0
- fusesell_local/cli.py +1480 -0
- fusesell_local/config/__init__.py +11 -0
- fusesell_local/config/default_email_templates.json +34 -0
- fusesell_local/config/default_prompts.json +19 -0
- fusesell_local/config/default_scoring_criteria.json +154 -0
- fusesell_local/config/prompts.py +245 -0
- fusesell_local/config/settings.py +277 -0
- fusesell_local/pipeline.py +978 -0
- fusesell_local/stages/__init__.py +19 -0
- fusesell_local/stages/base_stage.py +603 -0
- fusesell_local/stages/data_acquisition.py +1820 -0
- fusesell_local/stages/data_preparation.py +1238 -0
- fusesell_local/stages/follow_up.py +1728 -0
- fusesell_local/stages/initial_outreach.py +2972 -0
- fusesell_local/stages/lead_scoring.py +1452 -0
- fusesell_local/utils/__init__.py +36 -0
- fusesell_local/utils/agent_context.py +552 -0
- fusesell_local/utils/auto_setup.py +361 -0
- fusesell_local/utils/birthday_email_manager.py +467 -0
- fusesell_local/utils/data_manager.py +4857 -0
- fusesell_local/utils/event_scheduler.py +959 -0
- fusesell_local/utils/llm_client.py +342 -0
- fusesell_local/utils/logger.py +203 -0
- fusesell_local/utils/output_helpers.py +2443 -0
- fusesell_local/utils/timezone_detector.py +914 -0
- fusesell_local/utils/validators.py +436 -0
|
@@ -0,0 +1,1728 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Follow-up Stage - Generate contextual follow-up emails based on interaction history
|
|
3
|
+
Converted from fusesell_follow_up.yaml with enhanced LLM-powered generation
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
import requests
|
|
9
|
+
from typing import Dict, Any, List, Optional
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
from .base_stage import BaseStage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FollowUpStage(BaseStage):
|
|
15
|
+
"""
|
|
16
|
+
Follow-up stage for managing ongoing customer engagement with intelligent context analysis.
|
|
17
|
+
Supports multiple follow-up strategies based on interaction history and customer behavior.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
21
|
+
"""
|
|
22
|
+
Execute follow-up stage with action-based routing and interaction history analysis.
|
|
23
|
+
|
|
24
|
+
Actions supported:
|
|
25
|
+
- draft_write: Generate new follow-up drafts based on interaction history
|
|
26
|
+
- draft_rewrite: Modify existing draft using selected_draft_id
|
|
27
|
+
- send: Send approved draft to recipient_address with follow-up scheduling
|
|
28
|
+
- close: Close follow-up sequence when customer responds or shows disinterest
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
context: Execution context
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Stage execution result
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
# Get action from input data (matching server schema)
|
|
38
|
+
input_data = context.get('input_data', {})
|
|
39
|
+
action = input_data.get('action', 'draft_write') # Default to draft_write
|
|
40
|
+
|
|
41
|
+
self.logger.info(f"Executing follow-up with action: {action}")
|
|
42
|
+
|
|
43
|
+
# Validate required fields based on action
|
|
44
|
+
self._validate_action_input(action, input_data)
|
|
45
|
+
|
|
46
|
+
# Route based on action type (matching server executor schema)
|
|
47
|
+
if action == 'draft_write':
|
|
48
|
+
return self._handle_draft_write(context)
|
|
49
|
+
elif action == 'draft_rewrite':
|
|
50
|
+
return self._handle_draft_rewrite(context)
|
|
51
|
+
elif action == 'send':
|
|
52
|
+
return self._handle_send(context)
|
|
53
|
+
elif action == 'close':
|
|
54
|
+
return self._handle_close(context)
|
|
55
|
+
else:
|
|
56
|
+
raise ValueError(f"Invalid action: {action}. Must be one of: draft_write, draft_rewrite, send, close")
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
self.log_stage_error(context, e)
|
|
60
|
+
return self.handle_stage_error(e, context)
|
|
61
|
+
|
|
62
|
+
def _validate_action_input(self, action: str, input_data: Dict[str, Any]) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Validate required fields based on action type.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
action: Action type
|
|
68
|
+
input_data: Input data
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If required fields are missing
|
|
72
|
+
"""
|
|
73
|
+
if action in ['draft_rewrite', 'send']:
|
|
74
|
+
if not input_data.get('selected_draft_id'):
|
|
75
|
+
raise ValueError(f"selected_draft_id is required for {action} action")
|
|
76
|
+
|
|
77
|
+
if action == 'send':
|
|
78
|
+
if not input_data.get('recipient_address'):
|
|
79
|
+
raise ValueError("recipient_address is required for send action")
|
|
80
|
+
|
|
81
|
+
def _handle_draft_write(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Handle draft_write action - Generate new follow-up drafts based on interaction history.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
context: Execution context
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Stage execution result with new follow-up drafts
|
|
90
|
+
"""
|
|
91
|
+
# Analyze interaction history and determine follow-up strategy
|
|
92
|
+
interaction_analysis = self._analyze_interaction_history(context)
|
|
93
|
+
|
|
94
|
+
# Determine if follow-up is needed and what type
|
|
95
|
+
follow_up_strategy = self._determine_follow_up_strategy(interaction_analysis, context)
|
|
96
|
+
|
|
97
|
+
if not follow_up_strategy['should_follow_up']:
|
|
98
|
+
return self.create_success_result({
|
|
99
|
+
'action': 'draft_write',
|
|
100
|
+
'status': 'follow_up_not_needed',
|
|
101
|
+
'reason': follow_up_strategy['reason'],
|
|
102
|
+
'interaction_analysis': interaction_analysis,
|
|
103
|
+
'recommendation': 'No follow-up required at this time',
|
|
104
|
+
'customer_id': context.get('execution_id')
|
|
105
|
+
}, context)
|
|
106
|
+
|
|
107
|
+
# Get data from previous stages or context
|
|
108
|
+
customer_data = self._get_customer_data(context)
|
|
109
|
+
scoring_data = self._get_scoring_data(context)
|
|
110
|
+
|
|
111
|
+
# Get the recommended product (same as initial outreach)
|
|
112
|
+
recommended_product = self._get_recommended_product(scoring_data)
|
|
113
|
+
|
|
114
|
+
if not recommended_product:
|
|
115
|
+
raise ValueError("No product recommendation available for follow-up email generation")
|
|
116
|
+
|
|
117
|
+
# Generate follow-up email drafts based on strategy
|
|
118
|
+
follow_up_drafts = self._generate_follow_up_drafts(
|
|
119
|
+
customer_data, recommended_product, scoring_data,
|
|
120
|
+
interaction_analysis, follow_up_strategy, context
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Save drafts to local files and database
|
|
124
|
+
saved_drafts = self._save_follow_up_drafts(context, follow_up_drafts)
|
|
125
|
+
|
|
126
|
+
# Prepare final output
|
|
127
|
+
follow_up_data = {
|
|
128
|
+
'action': 'draft_write',
|
|
129
|
+
'status': 'follow_up_drafts_generated',
|
|
130
|
+
'follow_up_drafts': saved_drafts,
|
|
131
|
+
'follow_up_strategy': follow_up_strategy,
|
|
132
|
+
'interaction_analysis': interaction_analysis,
|
|
133
|
+
'recommended_product': recommended_product,
|
|
134
|
+
'customer_summary': self._create_customer_summary(customer_data),
|
|
135
|
+
'total_drafts_generated': len(saved_drafts),
|
|
136
|
+
'generation_timestamp': datetime.now().isoformat(),
|
|
137
|
+
'customer_id': context.get('execution_id'),
|
|
138
|
+
'sequence_number': follow_up_strategy.get('sequence_number', 1)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Save to database
|
|
142
|
+
self.save_stage_result(context, follow_up_data)
|
|
143
|
+
|
|
144
|
+
result = self.create_success_result(follow_up_data, context)
|
|
145
|
+
# Logging handled by execute_with_timing wrapper
|
|
146
|
+
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
def _analyze_interaction_history(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
150
|
+
"""
|
|
151
|
+
Analyze interaction history to understand customer engagement and response patterns.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
context: Execution context
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Interaction analysis results
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
input_data = context.get('input_data', {})
|
|
161
|
+
execution_id = context.get('execution_id')
|
|
162
|
+
|
|
163
|
+
# Get data manager for database operations
|
|
164
|
+
data_manager = self.get_data_manager()
|
|
165
|
+
|
|
166
|
+
# Get previous email drafts and interactions for this customer
|
|
167
|
+
previous_drafts = data_manager.get_email_drafts_by_execution(execution_id)
|
|
168
|
+
|
|
169
|
+
# Analyze interaction patterns
|
|
170
|
+
interaction_analysis = {
|
|
171
|
+
'total_emails_sent': 0,
|
|
172
|
+
'total_follow_ups': 0,
|
|
173
|
+
'last_interaction_date': None,
|
|
174
|
+
'days_since_last_interaction': 0,
|
|
175
|
+
'response_received': False,
|
|
176
|
+
'engagement_level': 'unknown',
|
|
177
|
+
'interaction_timeline': [],
|
|
178
|
+
'follow_up_sequence': [],
|
|
179
|
+
'customer_sentiment': 'neutral',
|
|
180
|
+
'recommended_approach': 'gentle_reminder'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Count emails and follow-ups
|
|
184
|
+
initial_emails = [d for d in previous_drafts if d.get('draft_type') == 'initial']
|
|
185
|
+
follow_up_emails = [d for d in previous_drafts if d.get('draft_type') in ['follow_up', 'initial_rewrite']]
|
|
186
|
+
|
|
187
|
+
interaction_analysis['total_emails_sent'] = len(initial_emails)
|
|
188
|
+
interaction_analysis['total_follow_ups'] = len(follow_up_emails)
|
|
189
|
+
|
|
190
|
+
# Determine last interaction
|
|
191
|
+
all_emails = sorted(previous_drafts, key=lambda x: x.get('created_at', ''), reverse=True)
|
|
192
|
+
if all_emails:
|
|
193
|
+
last_email = all_emails[0]
|
|
194
|
+
interaction_analysis['last_interaction_date'] = last_email.get('created_at')
|
|
195
|
+
|
|
196
|
+
# Calculate days since last interaction
|
|
197
|
+
try:
|
|
198
|
+
last_date = datetime.fromisoformat(last_email.get('created_at', '').replace('Z', '+00:00'))
|
|
199
|
+
days_diff = (datetime.now() - last_date.replace(tzinfo=None)).days
|
|
200
|
+
interaction_analysis['days_since_last_interaction'] = days_diff
|
|
201
|
+
except:
|
|
202
|
+
interaction_analysis['days_since_last_interaction'] = 0
|
|
203
|
+
|
|
204
|
+
# Analyze engagement level based on interaction patterns
|
|
205
|
+
interaction_analysis['engagement_level'] = self._determine_engagement_level(interaction_analysis)
|
|
206
|
+
|
|
207
|
+
# Determine customer sentiment (simplified - in real implementation, this could analyze responses)
|
|
208
|
+
interaction_analysis['customer_sentiment'] = self._analyze_customer_sentiment(context, interaction_analysis)
|
|
209
|
+
|
|
210
|
+
# Recommend approach based on analysis
|
|
211
|
+
interaction_analysis['recommended_approach'] = self._recommend_follow_up_approach(interaction_analysis)
|
|
212
|
+
|
|
213
|
+
# Create interaction timeline
|
|
214
|
+
interaction_analysis['interaction_timeline'] = self._create_interaction_timeline(previous_drafts)
|
|
215
|
+
|
|
216
|
+
return interaction_analysis
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
self.logger.error(f"Failed to analyze interaction history: {str(e)}")
|
|
220
|
+
# Return default analysis
|
|
221
|
+
return {
|
|
222
|
+
'total_emails_sent': 0,
|
|
223
|
+
'total_follow_ups': 0,
|
|
224
|
+
'days_since_last_interaction': 0,
|
|
225
|
+
'engagement_level': 'unknown',
|
|
226
|
+
'customer_sentiment': 'neutral',
|
|
227
|
+
'recommended_approach': 'gentle_reminder',
|
|
228
|
+
'analysis_error': str(e)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
def _determine_follow_up_strategy(self, interaction_analysis: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
|
232
|
+
"""
|
|
233
|
+
Determine the appropriate follow-up strategy based on interaction analysis.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
interaction_analysis: Results from interaction history analysis
|
|
237
|
+
context: Execution context
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Follow-up strategy recommendations
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
total_follow_ups = interaction_analysis.get('total_follow_ups', 0)
|
|
244
|
+
days_since_last = interaction_analysis.get('days_since_last_interaction', 0)
|
|
245
|
+
engagement_level = interaction_analysis.get('engagement_level', 'unknown')
|
|
246
|
+
customer_sentiment = interaction_analysis.get('customer_sentiment', 'neutral')
|
|
247
|
+
|
|
248
|
+
strategy = {
|
|
249
|
+
'should_follow_up': True,
|
|
250
|
+
'sequence_number': total_follow_ups + 1,
|
|
251
|
+
'strategy_type': 'gentle_reminder',
|
|
252
|
+
'timing_recommendation': 'immediate',
|
|
253
|
+
'approach_tone': 'professional',
|
|
254
|
+
'content_focus': 'value_add',
|
|
255
|
+
'reason': 'Standard follow-up sequence'
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# Determine if follow-up should continue
|
|
259
|
+
if total_follow_ups >= 5:
|
|
260
|
+
strategy.update({
|
|
261
|
+
'should_follow_up': False,
|
|
262
|
+
'reason': 'Maximum follow-up attempts reached (5)',
|
|
263
|
+
'recommendation': 'Consider closing this lead or trying a different approach'
|
|
264
|
+
})
|
|
265
|
+
return strategy
|
|
266
|
+
|
|
267
|
+
if customer_sentiment == 'negative':
|
|
268
|
+
strategy.update({
|
|
269
|
+
'should_follow_up': False,
|
|
270
|
+
'reason': 'Customer sentiment is negative',
|
|
271
|
+
'recommendation': 'Respect customer preference and close follow-up sequence'
|
|
272
|
+
})
|
|
273
|
+
return strategy
|
|
274
|
+
|
|
275
|
+
if days_since_last < 3 and total_follow_ups > 0:
|
|
276
|
+
strategy.update({
|
|
277
|
+
'should_follow_up': False,
|
|
278
|
+
'reason': 'Too soon since last interaction (less than 3 days)',
|
|
279
|
+
'recommendation': 'Wait at least 3 days between follow-ups'
|
|
280
|
+
})
|
|
281
|
+
return strategy
|
|
282
|
+
|
|
283
|
+
# Determine strategy type based on sequence number
|
|
284
|
+
if total_follow_ups == 0:
|
|
285
|
+
strategy.update({
|
|
286
|
+
'strategy_type': 'gentle_reminder',
|
|
287
|
+
'content_focus': 'gentle_nudge',
|
|
288
|
+
'approach_tone': 'friendly',
|
|
289
|
+
'timing_recommendation': 'after_5_days'
|
|
290
|
+
})
|
|
291
|
+
elif total_follow_ups == 1:
|
|
292
|
+
strategy.update({
|
|
293
|
+
'strategy_type': 'value_add',
|
|
294
|
+
'content_focus': 'additional_insights',
|
|
295
|
+
'approach_tone': 'helpful',
|
|
296
|
+
'timing_recommendation': 'after_1_week'
|
|
297
|
+
})
|
|
298
|
+
elif total_follow_ups == 2:
|
|
299
|
+
strategy.update({
|
|
300
|
+
'strategy_type': 'alternative_approach',
|
|
301
|
+
'content_focus': 'different_angle',
|
|
302
|
+
'approach_tone': 'consultative',
|
|
303
|
+
'timing_recommendation': 'after_2_weeks'
|
|
304
|
+
})
|
|
305
|
+
elif total_follow_ups == 3:
|
|
306
|
+
strategy.update({
|
|
307
|
+
'strategy_type': 'final_attempt',
|
|
308
|
+
'content_focus': 'last_chance',
|
|
309
|
+
'approach_tone': 'respectful_closure',
|
|
310
|
+
'timing_recommendation': 'after_1_month'
|
|
311
|
+
})
|
|
312
|
+
else:
|
|
313
|
+
strategy.update({
|
|
314
|
+
'strategy_type': 'graceful_close',
|
|
315
|
+
'content_focus': 'relationship_maintenance',
|
|
316
|
+
'approach_tone': 'professional_farewell',
|
|
317
|
+
'timing_recommendation': 'final_attempt'
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
# Adjust based on engagement level
|
|
321
|
+
if engagement_level == 'high':
|
|
322
|
+
strategy['approach_tone'] = 'enthusiastic'
|
|
323
|
+
strategy['content_focus'] = 'detailed_proposal'
|
|
324
|
+
elif engagement_level == 'low':
|
|
325
|
+
strategy['approach_tone'] = 'gentle'
|
|
326
|
+
strategy['content_focus'] = 'simple_question'
|
|
327
|
+
|
|
328
|
+
return strategy
|
|
329
|
+
|
|
330
|
+
except Exception as e:
|
|
331
|
+
self.logger.error(f"Failed to determine follow-up strategy: {str(e)}")
|
|
332
|
+
return {
|
|
333
|
+
'should_follow_up': True,
|
|
334
|
+
'sequence_number': 1,
|
|
335
|
+
'strategy_type': 'gentle_reminder',
|
|
336
|
+
'approach_tone': 'professional',
|
|
337
|
+
'content_focus': 'value_add',
|
|
338
|
+
'reason': 'Default strategy due to analysis error'
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
def _generate_follow_up_drafts(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any],
|
|
342
|
+
scoring_data: Dict[str, Any], interaction_analysis: Dict[str, Any],
|
|
343
|
+
follow_up_strategy: Dict[str, Any], context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
344
|
+
"""
|
|
345
|
+
Generate multiple follow-up email drafts based on strategy and interaction history.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
customer_data: Customer information
|
|
349
|
+
recommended_product: Product recommendation
|
|
350
|
+
scoring_data: Lead scoring results
|
|
351
|
+
interaction_analysis: Interaction history analysis
|
|
352
|
+
follow_up_strategy: Follow-up strategy
|
|
353
|
+
context: Execution context
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
List of generated follow-up drafts
|
|
357
|
+
"""
|
|
358
|
+
if self.is_dry_run():
|
|
359
|
+
return self._get_mock_follow_up_drafts(customer_data, follow_up_strategy, context)
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
input_data = context.get('input_data', {})
|
|
363
|
+
company_info = customer_data.get('companyInfo', {})
|
|
364
|
+
contact_info = customer_data.get('primaryContact', {})
|
|
365
|
+
|
|
366
|
+
# Define follow-up approaches based on strategy
|
|
367
|
+
follow_up_approaches = self._get_follow_up_approaches(follow_up_strategy)
|
|
368
|
+
|
|
369
|
+
generated_drafts = []
|
|
370
|
+
|
|
371
|
+
for approach in follow_up_approaches:
|
|
372
|
+
try:
|
|
373
|
+
# Generate follow-up content for this approach
|
|
374
|
+
email_content = self._generate_single_follow_up_draft(
|
|
375
|
+
customer_data, recommended_product, scoring_data,
|
|
376
|
+
interaction_analysis, follow_up_strategy, approach, context
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Generate subject lines for this approach
|
|
380
|
+
subject_lines = self._generate_follow_up_subject_lines(
|
|
381
|
+
customer_data, recommended_product, interaction_analysis,
|
|
382
|
+
follow_up_strategy, approach, context
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
draft_id = f"uuid:{str(uuid.uuid4())}"
|
|
386
|
+
draft_approach = approach['name']
|
|
387
|
+
draft_type = "followup"
|
|
388
|
+
|
|
389
|
+
draft = {
|
|
390
|
+
'draft_id': draft_id,
|
|
391
|
+
'approach': approach['name'],
|
|
392
|
+
'strategy_type': follow_up_strategy.get('strategy_type', 'gentle_reminder'),
|
|
393
|
+
'sequence_number': follow_up_strategy.get('sequence_number', 1),
|
|
394
|
+
'tone': approach['tone'],
|
|
395
|
+
'focus': approach['focus'],
|
|
396
|
+
'subject_lines': subject_lines,
|
|
397
|
+
'email_body': email_content,
|
|
398
|
+
'call_to_action': self._extract_call_to_action(email_content),
|
|
399
|
+
'personalization_score': self._calculate_personalization_score(email_content, customer_data),
|
|
400
|
+
'follow_up_context': {
|
|
401
|
+
'days_since_last_interaction': interaction_analysis.get('days_since_last_interaction', 0),
|
|
402
|
+
'total_previous_attempts': interaction_analysis.get('total_follow_ups', 0),
|
|
403
|
+
'engagement_level': interaction_analysis.get('engagement_level', 'unknown'),
|
|
404
|
+
'recommended_timing': follow_up_strategy.get('timing_recommendation', 'immediate')
|
|
405
|
+
},
|
|
406
|
+
'generated_at': datetime.now().isoformat(),
|
|
407
|
+
'status': 'draft',
|
|
408
|
+
'metadata': {
|
|
409
|
+
'customer_company': company_info.get('name', 'Unknown'),
|
|
410
|
+
'contact_name': contact_info.get('name', 'Unknown'),
|
|
411
|
+
'recommended_product': recommended_product.get('product_name', 'Unknown'),
|
|
412
|
+
'follow_up_type': follow_up_strategy.get('strategy_type', 'gentle_reminder'),
|
|
413
|
+
'generation_method': 'llm_powered_followup'
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
priority_order = self._get_draft_priority_order(
|
|
418
|
+
draft,
|
|
419
|
+
position=len(generated_drafts) + 1
|
|
420
|
+
)
|
|
421
|
+
draft['priority_order'] = priority_order
|
|
422
|
+
draft['metadata']['priority_order'] = priority_order
|
|
423
|
+
|
|
424
|
+
generated_drafts.append(draft)
|
|
425
|
+
|
|
426
|
+
except Exception as e:
|
|
427
|
+
self.logger.warning(f"Failed to generate follow-up draft for approach {approach['name']}: {str(e)}")
|
|
428
|
+
continue
|
|
429
|
+
|
|
430
|
+
if not generated_drafts:
|
|
431
|
+
# Fallback to simple template if all LLM generations fail
|
|
432
|
+
self.logger.warning("All LLM follow-up draft generations failed, using fallback template")
|
|
433
|
+
return self._generate_fallback_follow_up_draft(customer_data, recommended_product, follow_up_strategy, context)
|
|
434
|
+
|
|
435
|
+
self.logger.info(f"Generated {len(generated_drafts)} follow-up drafts successfully")
|
|
436
|
+
return generated_drafts
|
|
437
|
+
|
|
438
|
+
except Exception as e:
|
|
439
|
+
self.logger.error(f"Follow-up draft generation failed: {str(e)}")
|
|
440
|
+
return self._generate_fallback_follow_up_draft(customer_data, recommended_product, follow_up_strategy, context)
|
|
441
|
+
|
|
442
|
+
def _get_follow_up_approaches(self, follow_up_strategy: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
443
|
+
"""
|
|
444
|
+
Get follow-up approaches based on strategy type.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
follow_up_strategy: Follow-up strategy
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
List of follow-up approaches
|
|
451
|
+
"""
|
|
452
|
+
strategy_type = follow_up_strategy.get('strategy_type', 'gentle_reminder')
|
|
453
|
+
sequence_number = follow_up_strategy.get('sequence_number', 1)
|
|
454
|
+
|
|
455
|
+
if strategy_type == 'gentle_reminder':
|
|
456
|
+
return [
|
|
457
|
+
{
|
|
458
|
+
'name': 'friendly_check_in',
|
|
459
|
+
'tone': 'friendly and casual',
|
|
460
|
+
'focus': 'gentle reminder with soft touch',
|
|
461
|
+
'length': 'short'
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
'name': 'professional_follow_up',
|
|
465
|
+
'tone': 'professional and respectful',
|
|
466
|
+
'focus': 'polite follow-up on previous conversation',
|
|
467
|
+
'length': 'medium'
|
|
468
|
+
}
|
|
469
|
+
]
|
|
470
|
+
elif strategy_type == 'value_add':
|
|
471
|
+
return [
|
|
472
|
+
{
|
|
473
|
+
'name': 'insights_sharing',
|
|
474
|
+
'tone': 'helpful and informative',
|
|
475
|
+
'focus': 'sharing valuable insights or resources',
|
|
476
|
+
'length': 'medium'
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
'name': 'industry_trends',
|
|
480
|
+
'tone': 'expert and consultative',
|
|
481
|
+
'focus': 'relevant industry trends and opportunities',
|
|
482
|
+
'length': 'detailed'
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
elif strategy_type == 'alternative_approach':
|
|
486
|
+
return [
|
|
487
|
+
{
|
|
488
|
+
'name': 'different_angle',
|
|
489
|
+
'tone': 'creative and engaging',
|
|
490
|
+
'focus': 'new perspective or different value proposition',
|
|
491
|
+
'length': 'medium'
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
'name': 'case_study_approach',
|
|
495
|
+
'tone': 'evidence-based and compelling',
|
|
496
|
+
'focus': 'success stories and social proof',
|
|
497
|
+
'length': 'detailed'
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
elif strategy_type == 'final_attempt':
|
|
501
|
+
return [
|
|
502
|
+
{
|
|
503
|
+
'name': 'respectful_final_reach',
|
|
504
|
+
'tone': 'respectful and understanding',
|
|
505
|
+
'focus': 'final attempt with graceful exit option',
|
|
506
|
+
'length': 'short'
|
|
507
|
+
}
|
|
508
|
+
]
|
|
509
|
+
else: # graceful_close
|
|
510
|
+
return [
|
|
511
|
+
{
|
|
512
|
+
'name': 'graceful_farewell',
|
|
513
|
+
'tone': 'professional and gracious',
|
|
514
|
+
'focus': 'maintaining relationship for future opportunities',
|
|
515
|
+
'length': 'short'
|
|
516
|
+
}
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
def _generate_single_follow_up_draft(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any],
|
|
520
|
+
scoring_data: Dict[str, Any], interaction_analysis: Dict[str, Any],
|
|
521
|
+
follow_up_strategy: Dict[str, Any], approach: Dict[str, Any],
|
|
522
|
+
context: Dict[str, Any]) -> str:
|
|
523
|
+
"""
|
|
524
|
+
Generate a single follow-up email draft using LLM with specific approach.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
customer_data: Customer information
|
|
528
|
+
recommended_product: Product recommendation
|
|
529
|
+
scoring_data: Lead scoring results
|
|
530
|
+
interaction_analysis: Interaction history analysis
|
|
531
|
+
follow_up_strategy: Follow-up strategy
|
|
532
|
+
approach: Specific approach for this draft
|
|
533
|
+
context: Execution context
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
Generated follow-up email content
|
|
537
|
+
"""
|
|
538
|
+
try:
|
|
539
|
+
input_data = context.get('input_data', {})
|
|
540
|
+
company_info = customer_data.get('companyInfo', {})
|
|
541
|
+
contact_info = customer_data.get('primaryContact', {})
|
|
542
|
+
pain_points = customer_data.get('painPoints', [])
|
|
543
|
+
|
|
544
|
+
# Prepare context for LLM
|
|
545
|
+
follow_up_context = {
|
|
546
|
+
'company_name': company_info.get('name', 'the company'),
|
|
547
|
+
'contact_name': contact_info.get('name', 'there'),
|
|
548
|
+
'contact_title': contact_info.get('title', ''),
|
|
549
|
+
'industry': company_info.get('industry', 'technology'),
|
|
550
|
+
'main_pain_points': [p.get('description', '') for p in pain_points[:3]],
|
|
551
|
+
'recommended_product': recommended_product.get('product_name', 'our solution'),
|
|
552
|
+
'sender_name': input_data.get('staff_name', 'Sales Team'),
|
|
553
|
+
'sender_company': input_data.get('org_name', 'Our Company'),
|
|
554
|
+
'sequence_number': follow_up_strategy.get('sequence_number', 1),
|
|
555
|
+
'days_since_last': interaction_analysis.get('days_since_last_interaction', 0),
|
|
556
|
+
'total_attempts': interaction_analysis.get('total_follow_ups', 0),
|
|
557
|
+
'strategy_type': follow_up_strategy.get('strategy_type', 'gentle_reminder'),
|
|
558
|
+
'approach_tone': approach.get('tone', 'professional'),
|
|
559
|
+
'approach_focus': approach.get('focus', 'follow-up'),
|
|
560
|
+
'approach_length': approach.get('length', 'medium')
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
# Create LLM prompt for follow-up generation
|
|
564
|
+
prompt = self._create_follow_up_generation_prompt(follow_up_context, approach)
|
|
565
|
+
|
|
566
|
+
# Generate follow-up using LLM
|
|
567
|
+
email_content = self.call_llm(
|
|
568
|
+
prompt=prompt,
|
|
569
|
+
temperature=0.7,
|
|
570
|
+
max_tokens=800
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# Clean and validate the generated content
|
|
574
|
+
cleaned_content = self._clean_email_content(email_content)
|
|
575
|
+
|
|
576
|
+
return cleaned_content
|
|
577
|
+
|
|
578
|
+
except Exception as e:
|
|
579
|
+
self.logger.error(f"Failed to generate single follow-up draft: {str(e)}")
|
|
580
|
+
return self._generate_template_follow_up_email(customer_data, recommended_product, follow_up_strategy, approach, context)
|
|
581
|
+
|
|
582
|
+
def _create_follow_up_generation_prompt(self, follow_up_context: Dict[str, Any], approach: Dict[str, Any]) -> str:
|
|
583
|
+
"""
|
|
584
|
+
Create LLM prompt for follow-up email generation.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
follow_up_context: Context for follow-up generation
|
|
588
|
+
approach: Specific approach details
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
LLM prompt for follow-up generation
|
|
592
|
+
"""
|
|
593
|
+
|
|
594
|
+
pain_points_text = ""
|
|
595
|
+
if follow_up_context['main_pain_points']:
|
|
596
|
+
pain_points_text = f"Their key challenges: {', '.join(follow_up_context['main_pain_points'])}"
|
|
597
|
+
|
|
598
|
+
# Create context about previous interactions
|
|
599
|
+
interaction_context = f"""
|
|
600
|
+
FOLLOW-UP CONTEXT:
|
|
601
|
+
- This is follow-up #{follow_up_context['sequence_number']}
|
|
602
|
+
- {follow_up_context['days_since_last']} days since last interaction
|
|
603
|
+
- Total previous attempts: {follow_up_context['total_attempts']}
|
|
604
|
+
- Follow-up strategy: {follow_up_context['strategy_type']}"""
|
|
605
|
+
|
|
606
|
+
prompt = f"""Generate a personalized follow-up email with the following specifications:
|
|
607
|
+
|
|
608
|
+
CUSTOMER INFORMATION:
|
|
609
|
+
- Company: {follow_up_context['company_name']}
|
|
610
|
+
- Contact: {follow_up_context['contact_name']} ({follow_up_context['contact_title']})
|
|
611
|
+
- Industry: {follow_up_context['industry']}
|
|
612
|
+
{pain_points_text}
|
|
613
|
+
|
|
614
|
+
OUR OFFERING:
|
|
615
|
+
- Product/Solution: {follow_up_context['recommended_product']}
|
|
616
|
+
|
|
617
|
+
SENDER INFORMATION:
|
|
618
|
+
- Sender: {follow_up_context['sender_name']}
|
|
619
|
+
- Company: {follow_up_context['sender_company']}
|
|
620
|
+
|
|
621
|
+
{interaction_context}
|
|
622
|
+
|
|
623
|
+
EMAIL APPROACH:
|
|
624
|
+
- Tone: {follow_up_context['approach_tone']}
|
|
625
|
+
- Focus: {follow_up_context['approach_focus']}
|
|
626
|
+
- Length: {follow_up_context['approach_length']}
|
|
627
|
+
|
|
628
|
+
FOLLOW-UP REQUIREMENTS:
|
|
629
|
+
1. Reference that this is a follow-up (don't repeat everything from initial email)
|
|
630
|
+
2. Acknowledge the time that has passed since last contact
|
|
631
|
+
3. Provide new value or perspective (don't just repeat the same message)
|
|
632
|
+
4. Be respectful of their time and attention
|
|
633
|
+
5. Include a clear, low-pressure call-to-action
|
|
634
|
+
6. Match the {follow_up_context['approach_tone']} tone
|
|
635
|
+
7. Focus on {follow_up_context['approach_focus']}
|
|
636
|
+
8. Keep it {follow_up_context['approach_length']} in length
|
|
637
|
+
|
|
638
|
+
STRATEGY-SPECIFIC GUIDELINES:
|
|
639
|
+
- If gentle_reminder: Be soft, friendly, and non-pushy
|
|
640
|
+
- If value_add: Provide genuine insights, resources, or industry information
|
|
641
|
+
- If alternative_approach: Try a different angle or value proposition
|
|
642
|
+
- If final_attempt: Be respectful and offer a graceful exit
|
|
643
|
+
- If graceful_close: Focus on maintaining the relationship for future
|
|
644
|
+
|
|
645
|
+
Generate only the email content, no additional commentary:"""
|
|
646
|
+
|
|
647
|
+
return prompt
|
|
648
|
+
|
|
649
|
+
# Import utility methods from initial outreach (they're the same)
|
|
650
|
+
def _extract_call_to_action(self, email_content: str) -> str:
|
|
651
|
+
"""Extract the main call-to-action from email content."""
|
|
652
|
+
# Look for common CTA patterns
|
|
653
|
+
cta_patterns = [
|
|
654
|
+
r"Would you be (?:interested in|available for|open to) ([^?]+\?)",
|
|
655
|
+
r"Can we schedule ([^?]+\?)",
|
|
656
|
+
r"I'd love to ([^.]+\.)",
|
|
657
|
+
r"Let's ([^.]+\.)",
|
|
658
|
+
r"Would you like to ([^?]+\?)"
|
|
659
|
+
]
|
|
660
|
+
|
|
661
|
+
import re
|
|
662
|
+
for pattern in cta_patterns:
|
|
663
|
+
match = re.search(pattern, email_content, re.IGNORECASE)
|
|
664
|
+
if match:
|
|
665
|
+
return match.group(0)
|
|
666
|
+
|
|
667
|
+
# Fallback: look for question marks
|
|
668
|
+
sentences = email_content.split('.')
|
|
669
|
+
for sentence in sentences:
|
|
670
|
+
if '?' in sentence:
|
|
671
|
+
return sentence.strip() + ('.' if not sentence.strip().endswith('?') else '')
|
|
672
|
+
|
|
673
|
+
return "Would you be interested in learning more?"
|
|
674
|
+
|
|
675
|
+
def _calculate_personalization_score(self, email_content: str, customer_data: Dict[str, Any]) -> int:
|
|
676
|
+
"""Calculate personalization score based on customer data usage."""
|
|
677
|
+
score = 0
|
|
678
|
+
company_info = customer_data.get('companyInfo', {})
|
|
679
|
+
contact_info = customer_data.get('primaryContact', {})
|
|
680
|
+
|
|
681
|
+
# Check for company name usage
|
|
682
|
+
if company_info.get('name', '').lower() in email_content.lower():
|
|
683
|
+
score += 25
|
|
684
|
+
|
|
685
|
+
# Check for contact name usage
|
|
686
|
+
if contact_info.get('name', '').lower() in email_content.lower():
|
|
687
|
+
score += 25
|
|
688
|
+
|
|
689
|
+
# Check for industry mention
|
|
690
|
+
if company_info.get('industry', '').lower() in email_content.lower():
|
|
691
|
+
score += 20
|
|
692
|
+
|
|
693
|
+
# Check for pain points reference
|
|
694
|
+
pain_points = customer_data.get('painPoints', [])
|
|
695
|
+
for pain_point in pain_points:
|
|
696
|
+
if any(keyword.lower() in email_content.lower() for keyword in pain_point.get('description', '').split()[:3]):
|
|
697
|
+
score += 15
|
|
698
|
+
break
|
|
699
|
+
|
|
700
|
+
# Check for specific details (company size, location, etc.)
|
|
701
|
+
if any(detail in email_content.lower() for detail in [
|
|
702
|
+
company_info.get('size', '').lower(),
|
|
703
|
+
company_info.get('location', '').lower()
|
|
704
|
+
] if detail):
|
|
705
|
+
score += 15
|
|
706
|
+
|
|
707
|
+
return min(score, 100)
|
|
708
|
+
|
|
709
|
+
def _get_draft_priority_order(self, draft: Dict[str, Any], position: int = 1) -> int:
|
|
710
|
+
"""Compute a priority order for follow-up drafts."""
|
|
711
|
+
try:
|
|
712
|
+
explicit = int(draft.get('priority_order'))
|
|
713
|
+
if explicit > 0:
|
|
714
|
+
return explicit
|
|
715
|
+
except (TypeError, ValueError):
|
|
716
|
+
pass
|
|
717
|
+
|
|
718
|
+
base_priority = max(1, position)
|
|
719
|
+
try:
|
|
720
|
+
personalization_score = float(draft.get('personalization_score', 0))
|
|
721
|
+
except (TypeError, ValueError):
|
|
722
|
+
personalization_score = 0
|
|
723
|
+
|
|
724
|
+
if personalization_score >= 80:
|
|
725
|
+
return base_priority
|
|
726
|
+
if personalization_score >= 60:
|
|
727
|
+
return base_priority + 1
|
|
728
|
+
return base_priority + 2
|
|
729
|
+
|
|
730
|
+
def _remove_tagline_block(self, content: str) -> str:
|
|
731
|
+
"""Remove tagline rows (e.g., 'Tagline: ...') that often follow the greeting."""
|
|
732
|
+
if not content:
|
|
733
|
+
return ''
|
|
734
|
+
|
|
735
|
+
import re
|
|
736
|
+
|
|
737
|
+
tagline_pattern = re.compile(r'^\s*(tag\s*line|tagline)\b[:\-]?', re.IGNORECASE)
|
|
738
|
+
lines = content.splitlines()
|
|
739
|
+
filtered = [line for line in lines if not tagline_pattern.match(line)]
|
|
740
|
+
return '\n'.join(filtered).strip() if len(filtered) != len(lines) else content
|
|
741
|
+
|
|
742
|
+
def _clean_email_content(self, raw_content: str) -> str:
|
|
743
|
+
"""Clean and validate generated email content."""
|
|
744
|
+
# Remove any unwanted prefixes or suffixes
|
|
745
|
+
content = raw_content.strip()
|
|
746
|
+
|
|
747
|
+
# Remove common LLM artifacts
|
|
748
|
+
artifacts_to_remove = [
|
|
749
|
+
"Here's the follow-up email:",
|
|
750
|
+
"Here is the follow-up email:",
|
|
751
|
+
"Follow-up email content:",
|
|
752
|
+
"Generated follow-up email:",
|
|
753
|
+
"Subject:",
|
|
754
|
+
"Email:"
|
|
755
|
+
]
|
|
756
|
+
|
|
757
|
+
for artifact in artifacts_to_remove:
|
|
758
|
+
if content.startswith(artifact):
|
|
759
|
+
content = content[len(artifact):].strip()
|
|
760
|
+
|
|
761
|
+
content = self._remove_tagline_block(content)
|
|
762
|
+
|
|
763
|
+
# Ensure proper email structure
|
|
764
|
+
if not content.startswith(('Dear', 'Hi', 'Hello', 'Greetings')):
|
|
765
|
+
# Add a greeting if missing
|
|
766
|
+
content = f"Hi there,\n\n{content}"
|
|
767
|
+
|
|
768
|
+
# Ensure proper closing
|
|
769
|
+
if not any(closing in content.lower() for closing in ['best regards', 'sincerely', 'best', 'thanks']):
|
|
770
|
+
content += "\n\nBest regards"
|
|
771
|
+
|
|
772
|
+
return content
|
|
773
|
+
|
|
774
|
+
def _handle_draft_rewrite(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
775
|
+
"""
|
|
776
|
+
Handle draft_rewrite action - Modify existing follow-up draft.
|
|
777
|
+
|
|
778
|
+
Args:
|
|
779
|
+
context: Execution context
|
|
780
|
+
|
|
781
|
+
Returns:
|
|
782
|
+
Stage execution result with rewritten draft
|
|
783
|
+
"""
|
|
784
|
+
input_data = context.get('input_data', {})
|
|
785
|
+
selected_draft_id = input_data.get('selected_draft_id')
|
|
786
|
+
reason = input_data.get('reason', 'No reason provided')
|
|
787
|
+
|
|
788
|
+
# Retrieve existing draft
|
|
789
|
+
existing_draft = self._get_draft_by_id(selected_draft_id)
|
|
790
|
+
if not existing_draft:
|
|
791
|
+
raise ValueError(f"Follow-up draft not found: {selected_draft_id}")
|
|
792
|
+
|
|
793
|
+
# Get customer data for context
|
|
794
|
+
customer_data = self._get_customer_data(context)
|
|
795
|
+
scoring_data = self._get_scoring_data(context)
|
|
796
|
+
interaction_analysis = self._analyze_interaction_history(context)
|
|
797
|
+
|
|
798
|
+
# Rewrite the draft based on reason
|
|
799
|
+
rewritten_draft = self._rewrite_follow_up_draft(existing_draft, reason, customer_data, interaction_analysis, context)
|
|
800
|
+
|
|
801
|
+
# Save the rewritten draft
|
|
802
|
+
saved_draft = self._save_rewritten_follow_up_draft(context, rewritten_draft, selected_draft_id)
|
|
803
|
+
|
|
804
|
+
# Prepare output
|
|
805
|
+
follow_up_data = {
|
|
806
|
+
'action': 'draft_rewrite',
|
|
807
|
+
'status': 'follow_up_draft_rewritten',
|
|
808
|
+
'original_draft_id': selected_draft_id,
|
|
809
|
+
'rewritten_draft': saved_draft,
|
|
810
|
+
'rewrite_reason': reason,
|
|
811
|
+
'generation_timestamp': datetime.now().isoformat(),
|
|
812
|
+
'customer_id': context.get('execution_id')
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
# Save to database
|
|
816
|
+
self.save_stage_result(context, follow_up_data)
|
|
817
|
+
|
|
818
|
+
result = self.create_success_result(follow_up_data, context)
|
|
819
|
+
# Logging handled by execute_with_timing wrapper
|
|
820
|
+
|
|
821
|
+
return result
|
|
822
|
+
|
|
823
|
+
def _handle_send(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
824
|
+
"""
|
|
825
|
+
Handle send action - Send approved follow-up draft to recipient.
|
|
826
|
+
|
|
827
|
+
Args:
|
|
828
|
+
context: Execution context
|
|
829
|
+
|
|
830
|
+
Returns:
|
|
831
|
+
Stage execution result with send status
|
|
832
|
+
"""
|
|
833
|
+
input_data = context.get('input_data', {})
|
|
834
|
+
selected_draft_id = input_data.get('selected_draft_id')
|
|
835
|
+
recipient_address = input_data.get('recipient_address')
|
|
836
|
+
recipient_name = input_data.get('recipient_name', 'Dear Customer')
|
|
837
|
+
send_immediately = input_data.get('send_immediately', False)
|
|
838
|
+
|
|
839
|
+
# Retrieve the draft to send
|
|
840
|
+
draft_to_send = self._get_draft_by_id(selected_draft_id)
|
|
841
|
+
if not draft_to_send:
|
|
842
|
+
raise ValueError(f"Follow-up draft not found: {selected_draft_id}")
|
|
843
|
+
|
|
844
|
+
# Check if we should send immediately or schedule
|
|
845
|
+
if send_immediately:
|
|
846
|
+
# Send immediately
|
|
847
|
+
send_result = self._send_follow_up_email(draft_to_send, recipient_address, recipient_name, context)
|
|
848
|
+
else:
|
|
849
|
+
# Schedule for optimal timing
|
|
850
|
+
send_result = self._schedule_follow_up_email(draft_to_send, recipient_address, recipient_name, context)
|
|
851
|
+
|
|
852
|
+
# Prepare output
|
|
853
|
+
follow_up_data = {
|
|
854
|
+
'action': 'send',
|
|
855
|
+
'status': 'follow_up_sent' if send_immediately else 'follow_up_scheduled',
|
|
856
|
+
'draft_id': selected_draft_id,
|
|
857
|
+
'recipient_address': recipient_address,
|
|
858
|
+
'recipient_name': recipient_name,
|
|
859
|
+
'send_result': send_result,
|
|
860
|
+
'sent_immediately': send_immediately,
|
|
861
|
+
'send_timestamp': datetime.now().isoformat(),
|
|
862
|
+
'customer_id': context.get('execution_id')
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
# Save to database
|
|
866
|
+
self.save_stage_result(context, follow_up_data)
|
|
867
|
+
|
|
868
|
+
result = self.create_success_result(follow_up_data, context)
|
|
869
|
+
# Logging handled by execute_with_timing wrapper
|
|
870
|
+
|
|
871
|
+
return result
|
|
872
|
+
|
|
873
|
+
def _handle_close(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
874
|
+
"""
|
|
875
|
+
Handle close action - Close follow-up sequence.
|
|
876
|
+
|
|
877
|
+
Args:
|
|
878
|
+
context: Execution context
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
Stage execution result with close status
|
|
882
|
+
"""
|
|
883
|
+
input_data = context.get('input_data', {})
|
|
884
|
+
reason = input_data.get('reason', 'Follow-up sequence closed')
|
|
885
|
+
|
|
886
|
+
# Prepare output
|
|
887
|
+
follow_up_data = {
|
|
888
|
+
'action': 'close',
|
|
889
|
+
'status': 'follow_up_closed',
|
|
890
|
+
'close_reason': reason,
|
|
891
|
+
'closed_timestamp': datetime.now().isoformat(),
|
|
892
|
+
'customer_id': context.get('execution_id')
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
# Save to database
|
|
896
|
+
self.save_stage_result(context, follow_up_data)
|
|
897
|
+
|
|
898
|
+
result = self.create_success_result(follow_up_data, context)
|
|
899
|
+
# Logging handled by execute_with_timing wrapper
|
|
900
|
+
|
|
901
|
+
return result
|
|
902
|
+
|
|
903
|
+
def _schedule_follow_up_email(self, draft: Dict[str, Any], recipient_address: str, recipient_name: str, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
904
|
+
"""
|
|
905
|
+
Schedule follow-up email event in database for external app to handle.
|
|
906
|
+
|
|
907
|
+
Args:
|
|
908
|
+
draft: Email draft to send
|
|
909
|
+
recipient_address: Email address of recipient
|
|
910
|
+
recipient_name: Name of recipient
|
|
911
|
+
context: Execution context
|
|
912
|
+
|
|
913
|
+
Returns:
|
|
914
|
+
Scheduling result
|
|
915
|
+
"""
|
|
916
|
+
try:
|
|
917
|
+
from ..utils.event_scheduler import EventScheduler
|
|
918
|
+
|
|
919
|
+
input_data = context.get('input_data', {})
|
|
920
|
+
|
|
921
|
+
# Initialize event scheduler
|
|
922
|
+
scheduler = EventScheduler(self.config.get('data_dir', './fusesell_data'))
|
|
923
|
+
|
|
924
|
+
# Check if immediate sending is requested
|
|
925
|
+
send_immediately = input_data.get('send_immediately', False)
|
|
926
|
+
reminder_context = self._build_follow_up_reminder_context(
|
|
927
|
+
draft,
|
|
928
|
+
recipient_address,
|
|
929
|
+
recipient_name,
|
|
930
|
+
context
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
# Schedule the follow-up email event
|
|
934
|
+
schedule_result = scheduler.schedule_email_event(
|
|
935
|
+
draft_id=draft.get('draft_id'),
|
|
936
|
+
recipient_address=recipient_address,
|
|
937
|
+
recipient_name=recipient_name,
|
|
938
|
+
org_id=input_data.get('org_id', 'default'),
|
|
939
|
+
team_id=input_data.get('team_id'),
|
|
940
|
+
customer_timezone=input_data.get('customer_timezone'),
|
|
941
|
+
email_type='follow_up',
|
|
942
|
+
send_immediately=send_immediately,
|
|
943
|
+
reminder_context=reminder_context
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
if schedule_result['success']:
|
|
947
|
+
self.logger.info(f"Follow-up email event scheduled successfully: {schedule_result['event_id']} for {schedule_result['scheduled_time']}")
|
|
948
|
+
return {
|
|
949
|
+
'success': True,
|
|
950
|
+
'message': f'Follow-up email event scheduled for {schedule_result["scheduled_time"]}',
|
|
951
|
+
'event_id': schedule_result['event_id'],
|
|
952
|
+
'scheduled_time': schedule_result['scheduled_time'],
|
|
953
|
+
'service': 'Database Event Scheduler'
|
|
954
|
+
}
|
|
955
|
+
else:
|
|
956
|
+
self.logger.error(f"Follow-up email event scheduling failed: {schedule_result.get('error', 'Unknown error')}")
|
|
957
|
+
return {
|
|
958
|
+
'success': False,
|
|
959
|
+
'message': f'Follow-up email event scheduling failed: {schedule_result.get("error", "Unknown error")}',
|
|
960
|
+
'service': 'Database Event Scheduler'
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
except Exception as e:
|
|
964
|
+
self.logger.error(f"Follow-up email scheduling failed: {str(e)}")
|
|
965
|
+
return {
|
|
966
|
+
'success': False,
|
|
967
|
+
'message': f'Follow-up email scheduling failed: {str(e)}',
|
|
968
|
+
'error': str(e)
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
def _build_follow_up_reminder_context(
|
|
972
|
+
self,
|
|
973
|
+
draft: Dict[str, Any],
|
|
974
|
+
recipient_address: str,
|
|
975
|
+
recipient_name: str,
|
|
976
|
+
context: Dict[str, Any]
|
|
977
|
+
) -> Dict[str, Any]:
|
|
978
|
+
"""
|
|
979
|
+
Build reminder_task metadata for scheduled follow-up emails.
|
|
980
|
+
"""
|
|
981
|
+
input_data = context.get('input_data', {})
|
|
982
|
+
org_id = input_data.get('org_id', 'default') or 'default'
|
|
983
|
+
customer_id = input_data.get('customer_id') or context.get('execution_id') or 'unknown'
|
|
984
|
+
task_id = context.get('execution_id') or input_data.get('task_id') or 'unknown_task'
|
|
985
|
+
team_id = input_data.get('team_id')
|
|
986
|
+
team_name = input_data.get('team_name')
|
|
987
|
+
language = input_data.get('language')
|
|
988
|
+
customer_name = input_data.get('customer_name')
|
|
989
|
+
staff_name = input_data.get('staff_name')
|
|
990
|
+
interaction_type = input_data.get('interaction_type', 'follow_up')
|
|
991
|
+
follow_up_iteration = input_data.get('current_follow_up_time') or 1
|
|
992
|
+
reminder_room = self.config.get('reminder_room_id') or input_data.get('reminder_room_id')
|
|
993
|
+
draft_id = draft.get('draft_id') or 'unknown_draft'
|
|
994
|
+
product_name = draft.get('product_name') or input_data.get('product_name')
|
|
995
|
+
|
|
996
|
+
customextra = {
|
|
997
|
+
'reminder_content': 'follow_up',
|
|
998
|
+
'org_id': org_id,
|
|
999
|
+
'customer_id': customer_id,
|
|
1000
|
+
'task_id': task_id,
|
|
1001
|
+
'customer_name': customer_name,
|
|
1002
|
+
'language': language,
|
|
1003
|
+
'recipient_address': recipient_address,
|
|
1004
|
+
'recipient_name': recipient_name,
|
|
1005
|
+
'staff_name': staff_name,
|
|
1006
|
+
'team_id': team_id,
|
|
1007
|
+
'team_name': team_name,
|
|
1008
|
+
'interaction_type': interaction_type,
|
|
1009
|
+
'action_status': 'scheduled',
|
|
1010
|
+
'current_follow_up_time': follow_up_iteration,
|
|
1011
|
+
'draft_id': draft_id,
|
|
1012
|
+
'import_uuid': f"{org_id}_{customer_id}_{task_id}_{draft_id}"
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if product_name:
|
|
1016
|
+
customextra['product_name'] = product_name
|
|
1017
|
+
if draft.get('approach'):
|
|
1018
|
+
customextra['approach'] = draft.get('approach')
|
|
1019
|
+
if draft.get('mail_tone'):
|
|
1020
|
+
customextra['mail_tone'] = draft.get('mail_tone')
|
|
1021
|
+
if draft.get('message_type'):
|
|
1022
|
+
customextra['message_type'] = draft.get('message_type')
|
|
1023
|
+
|
|
1024
|
+
return {
|
|
1025
|
+
'status': 'published',
|
|
1026
|
+
'task': f"FuseSell follow-up {org_id}_{customer_id} - {task_id}",
|
|
1027
|
+
'tags': ['fusesell', 'follow-up'],
|
|
1028
|
+
'room_id': reminder_room,
|
|
1029
|
+
'org_id': org_id,
|
|
1030
|
+
'customer_id': customer_id,
|
|
1031
|
+
'task_id': task_id,
|
|
1032
|
+
'team_id': team_id,
|
|
1033
|
+
'team_name': team_name,
|
|
1034
|
+
'language': language,
|
|
1035
|
+
'customer_name': customer_name,
|
|
1036
|
+
'staff_name': staff_name,
|
|
1037
|
+
'customextra': customextra
|
|
1038
|
+
}
|
|
1039
|
+
# Data access methods (similar to initial outreach)
|
|
1040
|
+
def _get_customer_data(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
1041
|
+
"""Get customer data from previous stages or input."""
|
|
1042
|
+
stage_results = context.get('stage_results', {})
|
|
1043
|
+
|
|
1044
|
+
# Try to get from data preparation stage first
|
|
1045
|
+
if 'data_preparation' in stage_results:
|
|
1046
|
+
return stage_results['data_preparation']
|
|
1047
|
+
|
|
1048
|
+
# Fallback to input data
|
|
1049
|
+
input_data = context.get('input_data', {})
|
|
1050
|
+
return {
|
|
1051
|
+
'companyInfo': input_data.get('companyInfo', {}),
|
|
1052
|
+
'primaryContact': input_data.get('primaryContact', {}),
|
|
1053
|
+
'painPoints': input_data.get('painPoints', [])
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
def _get_scoring_data(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
1057
|
+
"""Get scoring data from previous stages."""
|
|
1058
|
+
stage_results = context.get('stage_results', {})
|
|
1059
|
+
return stage_results.get('lead_scoring', {})
|
|
1060
|
+
|
|
1061
|
+
def _get_recommended_product(self, scoring_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
1062
|
+
"""Get the recommended product from scoring data."""
|
|
1063
|
+
product_scores = scoring_data.get('product_scores', [])
|
|
1064
|
+
if product_scores:
|
|
1065
|
+
# Return the highest scoring product
|
|
1066
|
+
best_product = max(product_scores, key=lambda x: x.get('overall_score', 0))
|
|
1067
|
+
return best_product
|
|
1068
|
+
return None
|
|
1069
|
+
|
|
1070
|
+
def _get_auto_interaction_config(self, team_id: str = None) -> Dict[str, Any]:
|
|
1071
|
+
"""
|
|
1072
|
+
Get auto interaction configuration from team settings.
|
|
1073
|
+
|
|
1074
|
+
Args:
|
|
1075
|
+
team_id: Team ID to get settings for
|
|
1076
|
+
|
|
1077
|
+
Returns:
|
|
1078
|
+
Auto interaction configuration dictionary with from_email, from_name, etc.
|
|
1079
|
+
If multiple configs exist, returns the first Email type config.
|
|
1080
|
+
"""
|
|
1081
|
+
default_config = {
|
|
1082
|
+
'from_email': '',
|
|
1083
|
+
'from_name': '',
|
|
1084
|
+
'from_number': '',
|
|
1085
|
+
'tool_type': 'Email',
|
|
1086
|
+
'email_cc': '',
|
|
1087
|
+
'email_bcc': ''
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if not team_id:
|
|
1091
|
+
return default_config
|
|
1092
|
+
|
|
1093
|
+
try:
|
|
1094
|
+
# Get team settings
|
|
1095
|
+
auto_interaction_settings = self.get_team_setting('gs_team_auto_interaction', team_id, [])
|
|
1096
|
+
|
|
1097
|
+
if not auto_interaction_settings or not isinstance(auto_interaction_settings, list):
|
|
1098
|
+
self.logger.debug(f"No auto interaction settings found for team {team_id}, using defaults")
|
|
1099
|
+
return default_config
|
|
1100
|
+
|
|
1101
|
+
# Find Email type configuration (preferred for email sending)
|
|
1102
|
+
email_config = None
|
|
1103
|
+
for config in auto_interaction_settings:
|
|
1104
|
+
if config.get('tool_type') == 'Email':
|
|
1105
|
+
email_config = config
|
|
1106
|
+
break
|
|
1107
|
+
|
|
1108
|
+
# If no Email config found, use the first one available
|
|
1109
|
+
if not email_config and len(auto_interaction_settings) > 0:
|
|
1110
|
+
email_config = auto_interaction_settings[0]
|
|
1111
|
+
self.logger.warning(f"No Email tool_type found in auto interaction settings, using first config with tool_type: {email_config.get('tool_type')}")
|
|
1112
|
+
|
|
1113
|
+
if email_config:
|
|
1114
|
+
self.logger.debug(f"Using auto interaction config for team {team_id}: from_name={email_config.get('from_name')}, tool_type={email_config.get('tool_type')}")
|
|
1115
|
+
return email_config
|
|
1116
|
+
else:
|
|
1117
|
+
return default_config
|
|
1118
|
+
|
|
1119
|
+
except Exception as e:
|
|
1120
|
+
self.logger.error(f"Failed to get auto interaction config for team {team_id}: {str(e)}")
|
|
1121
|
+
return default_config
|
|
1122
|
+
|
|
1123
|
+
def _get_draft_by_id(self, draft_id: str) -> Optional[Dict[str, Any]]:
|
|
1124
|
+
"""Retrieve follow-up draft by ID from database."""
|
|
1125
|
+
if self.is_dry_run():
|
|
1126
|
+
return {
|
|
1127
|
+
'draft_id': draft_id,
|
|
1128
|
+
'subject_lines': ['Mock Follow-up Subject Line'],
|
|
1129
|
+
'email_body': 'Mock follow-up email content for testing purposes.',
|
|
1130
|
+
'approach': 'mock',
|
|
1131
|
+
'tone': 'professional',
|
|
1132
|
+
'status': 'mock',
|
|
1133
|
+
'call_to_action': 'Mock follow-up call to action',
|
|
1134
|
+
'personalization_score': 75,
|
|
1135
|
+
'sequence_number': 1
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
try:
|
|
1139
|
+
# Get data manager for database operations
|
|
1140
|
+
data_manager = self.get_data_manager()
|
|
1141
|
+
|
|
1142
|
+
# Query database for draft
|
|
1143
|
+
draft_record = data_manager.get_email_draft_by_id(draft_id)
|
|
1144
|
+
|
|
1145
|
+
if not draft_record:
|
|
1146
|
+
self.logger.warning(f"Follow-up draft not found in database: {draft_id}")
|
|
1147
|
+
return None
|
|
1148
|
+
|
|
1149
|
+
# Parse metadata
|
|
1150
|
+
metadata = {}
|
|
1151
|
+
if draft_record.get('metadata'):
|
|
1152
|
+
try:
|
|
1153
|
+
metadata = json.loads(draft_record['metadata'])
|
|
1154
|
+
except json.JSONDecodeError:
|
|
1155
|
+
self.logger.warning(f"Failed to parse metadata for follow-up draft {draft_id}")
|
|
1156
|
+
|
|
1157
|
+
# Reconstruct draft object
|
|
1158
|
+
draft = {
|
|
1159
|
+
'draft_id': draft_record['draft_id'],
|
|
1160
|
+
'execution_id': draft_record['execution_id'],
|
|
1161
|
+
'customer_id': draft_record['customer_id'],
|
|
1162
|
+
'subject_lines': metadata.get('all_subject_lines', [draft_record['subject']]),
|
|
1163
|
+
'email_body': draft_record['content'],
|
|
1164
|
+
'approach': metadata.get('approach', 'unknown'),
|
|
1165
|
+
'tone': metadata.get('tone', 'professional'),
|
|
1166
|
+
'focus': metadata.get('focus', 'general'),
|
|
1167
|
+
'call_to_action': metadata.get('call_to_action', ''),
|
|
1168
|
+
'personalization_score': metadata.get('personalization_score', 0),
|
|
1169
|
+
'status': draft_record['status'],
|
|
1170
|
+
'version': draft_record['version'],
|
|
1171
|
+
'draft_type': draft_record['draft_type'],
|
|
1172
|
+
'sequence_number': metadata.get('sequence_number', 1),
|
|
1173
|
+
'created_at': draft_record['created_at'],
|
|
1174
|
+
'updated_at': draft_record['updated_at'],
|
|
1175
|
+
'metadata': metadata
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
self.logger.info(f"Retrieved follow-up draft {draft_id} from database")
|
|
1179
|
+
return draft
|
|
1180
|
+
|
|
1181
|
+
except Exception as e:
|
|
1182
|
+
self.logger.error(f"Failed to retrieve follow-up draft {draft_id}: {str(e)}")
|
|
1183
|
+
return None
|
|
1184
|
+
|
|
1185
|
+
# Helper methods for analysis
|
|
1186
|
+
def _determine_engagement_level(self, interaction_analysis: Dict[str, Any]) -> str:
|
|
1187
|
+
"""Determine customer engagement level based on interaction patterns."""
|
|
1188
|
+
total_emails = interaction_analysis.get('total_emails_sent', 0)
|
|
1189
|
+
total_follow_ups = interaction_analysis.get('total_follow_ups', 0)
|
|
1190
|
+
days_since_last = interaction_analysis.get('days_since_last_interaction', 0)
|
|
1191
|
+
|
|
1192
|
+
# Simple engagement scoring
|
|
1193
|
+
if total_emails == 0:
|
|
1194
|
+
return 'unknown'
|
|
1195
|
+
elif days_since_last <= 3 and total_follow_ups < 2:
|
|
1196
|
+
return 'high'
|
|
1197
|
+
elif days_since_last <= 7 and total_follow_ups < 3:
|
|
1198
|
+
return 'medium'
|
|
1199
|
+
else:
|
|
1200
|
+
return 'low'
|
|
1201
|
+
|
|
1202
|
+
def _analyze_customer_sentiment(self, context: Dict[str, Any], interaction_analysis: Dict[str, Any]) -> str:
|
|
1203
|
+
"""Analyze customer sentiment (simplified - could be enhanced with response analysis)."""
|
|
1204
|
+
# In a real implementation, this would analyze actual customer responses
|
|
1205
|
+
# For now, we'll use simple heuristics
|
|
1206
|
+
total_follow_ups = interaction_analysis.get('total_follow_ups', 0)
|
|
1207
|
+
|
|
1208
|
+
if total_follow_ups >= 4:
|
|
1209
|
+
return 'negative' # Too many follow-ups without response
|
|
1210
|
+
elif total_follow_ups >= 2:
|
|
1211
|
+
return 'neutral' # Some follow-ups, neutral response
|
|
1212
|
+
else:
|
|
1213
|
+
return 'positive' # Early in sequence, assume positive
|
|
1214
|
+
|
|
1215
|
+
def _recommend_follow_up_approach(self, interaction_analysis: Dict[str, Any]) -> str:
|
|
1216
|
+
"""Recommend follow-up approach based on interaction analysis."""
|
|
1217
|
+
engagement_level = interaction_analysis.get('engagement_level', 'unknown')
|
|
1218
|
+
total_follow_ups = interaction_analysis.get('total_follow_ups', 0)
|
|
1219
|
+
|
|
1220
|
+
if total_follow_ups == 0:
|
|
1221
|
+
return 'gentle_reminder'
|
|
1222
|
+
elif total_follow_ups == 1:
|
|
1223
|
+
return 'value_add'
|
|
1224
|
+
elif total_follow_ups == 2:
|
|
1225
|
+
return 'alternative_approach'
|
|
1226
|
+
elif total_follow_ups >= 3:
|
|
1227
|
+
return 'final_attempt'
|
|
1228
|
+
else:
|
|
1229
|
+
return 'gentle_reminder'
|
|
1230
|
+
|
|
1231
|
+
def _create_interaction_timeline(self, previous_drafts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
1232
|
+
"""Create interaction timeline from previous drafts."""
|
|
1233
|
+
timeline = []
|
|
1234
|
+
|
|
1235
|
+
for draft in sorted(previous_drafts, key=lambda x: x.get('created_at', '')):
|
|
1236
|
+
timeline.append({
|
|
1237
|
+
'date': draft.get('created_at'),
|
|
1238
|
+
'type': draft.get('draft_type', 'unknown'),
|
|
1239
|
+
'draft_id': draft.get('draft_id'),
|
|
1240
|
+
'status': draft.get('status', 'unknown')
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1243
|
+
return timeline
|
|
1244
|
+
|
|
1245
|
+
def _create_customer_summary(self, customer_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
1246
|
+
"""Create customer summary for follow-up context."""
|
|
1247
|
+
company_info = customer_data.get('companyInfo', {})
|
|
1248
|
+
contact_info = customer_data.get('primaryContact', {})
|
|
1249
|
+
|
|
1250
|
+
return {
|
|
1251
|
+
'company_name': company_info.get('name', 'Unknown'),
|
|
1252
|
+
'industry': company_info.get('industry', 'Unknown'),
|
|
1253
|
+
'contact_name': contact_info.get('name', 'Unknown'),
|
|
1254
|
+
'contact_email': contact_info.get('email', 'Unknown'),
|
|
1255
|
+
'follow_up_context': 'Generated for follow-up sequence'
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
# Mock and fallback methods
|
|
1259
|
+
def _get_mock_follow_up_drafts(self, customer_data: Dict[str, Any], follow_up_strategy: Dict[str, Any], context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
1260
|
+
"""Get mock follow-up drafts for dry run."""
|
|
1261
|
+
company_info = customer_data.get('companyInfo', {})
|
|
1262
|
+
|
|
1263
|
+
mock_draft = {
|
|
1264
|
+
'draft_id': 'mock_followup_001',
|
|
1265
|
+
'approach': 'gentle_reminder',
|
|
1266
|
+
'strategy_type': follow_up_strategy.get('strategy_type', 'gentle_reminder'),
|
|
1267
|
+
'sequence_number': follow_up_strategy.get('sequence_number', 1),
|
|
1268
|
+
'tone': 'friendly and professional',
|
|
1269
|
+
'focus': 'gentle follow-up',
|
|
1270
|
+
'subject_lines': [
|
|
1271
|
+
f"Following up on our conversation - {company_info.get('name', 'Test Company')}",
|
|
1272
|
+
f"Quick check-in with {company_info.get('name', 'Test Company')}",
|
|
1273
|
+
f"Thoughts on our discussion?"
|
|
1274
|
+
],
|
|
1275
|
+
'email_body': f"""[DRY RUN] Mock follow-up email content for {company_info.get('name', 'Test Company')}
|
|
1276
|
+
|
|
1277
|
+
This is a mock follow-up email that would be generated for testing purposes. In a real execution, this would contain contextual follow-up content based on the interaction history and follow-up strategy.""",
|
|
1278
|
+
'call_to_action': 'Mock follow-up call to action',
|
|
1279
|
+
'personalization_score': 80,
|
|
1280
|
+
'generated_at': datetime.now().isoformat(),
|
|
1281
|
+
'status': 'mock',
|
|
1282
|
+
'metadata': {
|
|
1283
|
+
'generation_method': 'mock_data',
|
|
1284
|
+
'note': 'This is mock data for dry run testing'
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
mock_draft['priority_order'] = self._get_draft_priority_order(mock_draft, position=1)
|
|
1289
|
+
mock_draft['metadata']['priority_order'] = mock_draft['priority_order']
|
|
1290
|
+
|
|
1291
|
+
return [mock_draft]
|
|
1292
|
+
|
|
1293
|
+
def validate_input(self, context: Dict[str, Any]) -> bool:
|
|
1294
|
+
"""
|
|
1295
|
+
Validate input data for follow-up stage.
|
|
1296
|
+
|
|
1297
|
+
Args:
|
|
1298
|
+
context: Execution context
|
|
1299
|
+
|
|
1300
|
+
Returns:
|
|
1301
|
+
True if input is valid
|
|
1302
|
+
"""
|
|
1303
|
+
# Follow-up stage can work with minimal input since it analyzes history
|
|
1304
|
+
return True
|
|
1305
|
+
|
|
1306
|
+
def _save_follow_up_drafts(self, context: Dict[str, Any], follow_up_drafts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
1307
|
+
"""Save follow-up drafts to database and files."""
|
|
1308
|
+
try:
|
|
1309
|
+
execution_id = context.get('execution_id')
|
|
1310
|
+
saved_drafts = []
|
|
1311
|
+
|
|
1312
|
+
# Get data manager for database operations
|
|
1313
|
+
data_manager = self.get_data_manager()
|
|
1314
|
+
|
|
1315
|
+
for draft in follow_up_drafts:
|
|
1316
|
+
try:
|
|
1317
|
+
priority_order = draft.get('priority_order')
|
|
1318
|
+
if not isinstance(priority_order, int) or priority_order < 1:
|
|
1319
|
+
priority_order = self._get_draft_priority_order(
|
|
1320
|
+
draft,
|
|
1321
|
+
position=len(saved_drafts) + 1
|
|
1322
|
+
)
|
|
1323
|
+
draft['priority_order'] = priority_order
|
|
1324
|
+
draft.setdefault('metadata', {})['priority_order'] = priority_order
|
|
1325
|
+
|
|
1326
|
+
# Prepare draft data for database
|
|
1327
|
+
draft_data = {
|
|
1328
|
+
'draft_id': draft['draft_id'],
|
|
1329
|
+
'execution_id': execution_id,
|
|
1330
|
+
'customer_id': execution_id, # Using execution_id as customer_id for now
|
|
1331
|
+
'subject': draft['subject_lines'][0] if draft['subject_lines'] else 'Follow-up',
|
|
1332
|
+
'content': draft['email_body'],
|
|
1333
|
+
'draft_type': 'follow_up',
|
|
1334
|
+
'version': 1,
|
|
1335
|
+
'status': 'draft',
|
|
1336
|
+
'metadata': json.dumps({
|
|
1337
|
+
'approach': draft.get('approach', 'unknown'),
|
|
1338
|
+
'strategy_type': draft.get('strategy_type', 'gentle_reminder'),
|
|
1339
|
+
'sequence_number': draft.get('sequence_number', 1),
|
|
1340
|
+
'tone': draft.get('tone', 'professional'),
|
|
1341
|
+
'focus': draft.get('focus', 'general'),
|
|
1342
|
+
'all_subject_lines': draft.get('subject_lines', []),
|
|
1343
|
+
'call_to_action': draft.get('call_to_action', ''),
|
|
1344
|
+
'personalization_score': draft.get('personalization_score', 0),
|
|
1345
|
+
'follow_up_context': draft.get('follow_up_context', {}),
|
|
1346
|
+
'generation_method': 'llm_powered_followup',
|
|
1347
|
+
'priority_order': priority_order,
|
|
1348
|
+
'generated_at': draft.get('generated_at', datetime.now().isoformat())
|
|
1349
|
+
})
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
# Save to database
|
|
1353
|
+
if not self.is_dry_run():
|
|
1354
|
+
data_manager.save_email_draft(draft_data)
|
|
1355
|
+
self.logger.info(f"Saved follow-up draft {draft['draft_id']} to database")
|
|
1356
|
+
|
|
1357
|
+
# Save to file system for backup
|
|
1358
|
+
draft_file_path = self._save_draft_to_file(execution_id, draft)
|
|
1359
|
+
|
|
1360
|
+
# Add context to draft
|
|
1361
|
+
draft_with_context = draft.copy()
|
|
1362
|
+
draft_with_context['execution_id'] = execution_id
|
|
1363
|
+
draft_with_context['file_path'] = draft_file_path
|
|
1364
|
+
draft_with_context['database_saved'] = not self.is_dry_run()
|
|
1365
|
+
draft_with_context['saved_at'] = datetime.now().isoformat()
|
|
1366
|
+
|
|
1367
|
+
saved_drafts.append(draft_with_context)
|
|
1368
|
+
|
|
1369
|
+
except Exception as e:
|
|
1370
|
+
self.logger.error(f"Failed to save individual follow-up draft {draft.get('draft_id', 'unknown')}: {str(e)}")
|
|
1371
|
+
# Still add to saved_drafts but mark as failed
|
|
1372
|
+
draft_with_context = draft.copy()
|
|
1373
|
+
draft_with_context['execution_id'] = execution_id
|
|
1374
|
+
draft_with_context['save_error'] = str(e)
|
|
1375
|
+
draft_with_context['database_saved'] = False
|
|
1376
|
+
saved_drafts.append(draft_with_context)
|
|
1377
|
+
|
|
1378
|
+
self.logger.info(f"Successfully saved {len([d for d in saved_drafts if d.get('database_saved', False)])} follow-up drafts to database")
|
|
1379
|
+
return saved_drafts
|
|
1380
|
+
|
|
1381
|
+
except Exception as e:
|
|
1382
|
+
self.logger.error(f"Failed to save follow-up drafts: {str(e)}")
|
|
1383
|
+
# Return drafts with error information
|
|
1384
|
+
for draft in follow_up_drafts:
|
|
1385
|
+
draft['save_error'] = str(e)
|
|
1386
|
+
draft['database_saved'] = False
|
|
1387
|
+
return follow_up_drafts
|
|
1388
|
+
|
|
1389
|
+
def _save_draft_to_file(self, execution_id: str, draft: Dict[str, Any]) -> str:
|
|
1390
|
+
"""Save follow-up draft to file system as backup."""
|
|
1391
|
+
try:
|
|
1392
|
+
import os
|
|
1393
|
+
|
|
1394
|
+
# Create drafts directory if it doesn't exist
|
|
1395
|
+
drafts_dir = os.path.join(self.config.get('data_dir', './fusesell_data'), 'drafts')
|
|
1396
|
+
os.makedirs(drafts_dir, exist_ok=True)
|
|
1397
|
+
|
|
1398
|
+
# Create file path
|
|
1399
|
+
file_name = f"{execution_id}_{draft['draft_id']}_followup.json"
|
|
1400
|
+
file_path = os.path.join(drafts_dir, file_name)
|
|
1401
|
+
|
|
1402
|
+
# Save draft to file
|
|
1403
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
1404
|
+
json.dump(draft, f, indent=2, ensure_ascii=False)
|
|
1405
|
+
|
|
1406
|
+
return f"drafts/{file_name}"
|
|
1407
|
+
|
|
1408
|
+
except Exception as e:
|
|
1409
|
+
self.logger.warning(f"Failed to save follow-up draft to file: {str(e)}")
|
|
1410
|
+
return f"drafts/{execution_id}_{draft['draft_id']}_followup.json"
|
|
1411
|
+
|
|
1412
|
+
def _generate_follow_up_subject_lines(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any],
|
|
1413
|
+
interaction_analysis: Dict[str, Any], follow_up_strategy: Dict[str, Any],
|
|
1414
|
+
approach: Dict[str, Any], context: Dict[str, Any]) -> List[str]:
|
|
1415
|
+
"""Generate follow-up subject lines using LLM."""
|
|
1416
|
+
try:
|
|
1417
|
+
input_data = context.get('input_data', {})
|
|
1418
|
+
company_info = customer_data.get('companyInfo', {})
|
|
1419
|
+
|
|
1420
|
+
prompt = f"""Generate 3 compelling follow-up email subject lines for {company_info.get('name', 'a company')} in the {company_info.get('industry', 'technology')} industry.
|
|
1421
|
+
|
|
1422
|
+
CONTEXT:
|
|
1423
|
+
- This is follow-up #{follow_up_strategy.get('sequence_number', 1)}
|
|
1424
|
+
- Days since last contact: {interaction_analysis.get('days_since_last_interaction', 0)}
|
|
1425
|
+
- Follow-up strategy: {follow_up_strategy.get('strategy_type', 'gentle_reminder')}
|
|
1426
|
+
- Target Company: {company_info.get('name', 'the company')}
|
|
1427
|
+
- Our Solution: {recommended_product.get('product_name', 'our solution')}
|
|
1428
|
+
- Approach Tone: {approach.get('tone', 'professional')}
|
|
1429
|
+
|
|
1430
|
+
REQUIREMENTS:
|
|
1431
|
+
1. Keep subject lines under 50 characters
|
|
1432
|
+
2. Make them appropriate for a follow-up (not initial contact)
|
|
1433
|
+
3. Reference the passage of time appropriately
|
|
1434
|
+
4. Match the {approach.get('tone', 'professional')} tone
|
|
1435
|
+
5. Avoid being pushy or aggressive
|
|
1436
|
+
|
|
1437
|
+
Generate 3 subject lines, one per line, no numbering or bullets:"""
|
|
1438
|
+
|
|
1439
|
+
response = self.call_llm(
|
|
1440
|
+
prompt=prompt,
|
|
1441
|
+
temperature=0.8,
|
|
1442
|
+
max_tokens=150
|
|
1443
|
+
)
|
|
1444
|
+
|
|
1445
|
+
# Parse subject lines from response
|
|
1446
|
+
subject_lines = [line.strip() for line in response.split('\n') if line.strip()]
|
|
1447
|
+
|
|
1448
|
+
# Ensure we have at least 3 subject lines
|
|
1449
|
+
if len(subject_lines) < 3:
|
|
1450
|
+
subject_lines.extend(self._generate_fallback_follow_up_subject_lines(customer_data, follow_up_strategy))
|
|
1451
|
+
|
|
1452
|
+
return subject_lines[:3] # Return max 3 subject lines
|
|
1453
|
+
|
|
1454
|
+
except Exception as e:
|
|
1455
|
+
self.logger.warning(f"Failed to generate follow-up subject lines: {str(e)}")
|
|
1456
|
+
return self._generate_fallback_follow_up_subject_lines(customer_data, follow_up_strategy)
|
|
1457
|
+
|
|
1458
|
+
def _generate_fallback_follow_up_subject_lines(self, customer_data: Dict[str, Any], follow_up_strategy: Dict[str, Any]) -> List[str]:
|
|
1459
|
+
"""Generate fallback follow-up subject lines using templates."""
|
|
1460
|
+
company_info = customer_data.get('companyInfo', {})
|
|
1461
|
+
company_name = company_info.get('name', 'Your Company')
|
|
1462
|
+
sequence_number = follow_up_strategy.get('sequence_number', 1)
|
|
1463
|
+
|
|
1464
|
+
if sequence_number == 1:
|
|
1465
|
+
return [
|
|
1466
|
+
f"Following up on {company_name}",
|
|
1467
|
+
f"Quick check-in",
|
|
1468
|
+
f"Thoughts on our discussion?"
|
|
1469
|
+
]
|
|
1470
|
+
elif sequence_number == 2:
|
|
1471
|
+
return [
|
|
1472
|
+
f"Additional insights for {company_name}",
|
|
1473
|
+
f"One more thought",
|
|
1474
|
+
f"Quick update for you"
|
|
1475
|
+
]
|
|
1476
|
+
else:
|
|
1477
|
+
return [
|
|
1478
|
+
f"Final follow-up for {company_name}",
|
|
1479
|
+
f"Last check-in",
|
|
1480
|
+
f"Closing the loop"
|
|
1481
|
+
]
|
|
1482
|
+
|
|
1483
|
+
def _generate_fallback_follow_up_draft(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any],
|
|
1484
|
+
follow_up_strategy: Dict[str, Any], context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
1485
|
+
"""Generate fallback follow-up draft when LLM generation fails."""
|
|
1486
|
+
draft_id = f"uuid:{str(uuid.uuid4())}"
|
|
1487
|
+
draft_approach = "fallback"
|
|
1488
|
+
draft_type = "followup"
|
|
1489
|
+
|
|
1490
|
+
fallback_draft = {
|
|
1491
|
+
'draft_id': draft_id,
|
|
1492
|
+
'approach': 'fallback_template',
|
|
1493
|
+
'strategy_type': follow_up_strategy.get('strategy_type', 'gentle_reminder'),
|
|
1494
|
+
'sequence_number': follow_up_strategy.get('sequence_number', 1),
|
|
1495
|
+
'tone': 'professional',
|
|
1496
|
+
'focus': 'general follow-up',
|
|
1497
|
+
'subject_lines': self._generate_fallback_follow_up_subject_lines(customer_data, follow_up_strategy),
|
|
1498
|
+
'email_body': self._generate_template_follow_up_email(customer_data, recommended_product, follow_up_strategy, {'tone': 'professional'}, context),
|
|
1499
|
+
'call_to_action': 'Would you be interested in continuing our conversation?',
|
|
1500
|
+
'personalization_score': 50,
|
|
1501
|
+
'generated_at': datetime.now().isoformat(),
|
|
1502
|
+
'status': 'draft',
|
|
1503
|
+
'metadata': {
|
|
1504
|
+
'generation_method': 'template_fallback',
|
|
1505
|
+
'note': 'Generated using template due to LLM failure'
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
fallback_draft['priority_order'] = self._get_draft_priority_order(fallback_draft, position=1)
|
|
1510
|
+
fallback_draft['metadata']['priority_order'] = fallback_draft['priority_order']
|
|
1511
|
+
|
|
1512
|
+
return [fallback_draft]
|
|
1513
|
+
|
|
1514
|
+
def _generate_template_follow_up_email(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any],
|
|
1515
|
+
follow_up_strategy: Dict[str, Any], approach: Dict[str, Any], context: Dict[str, Any]) -> str:
|
|
1516
|
+
"""Generate follow-up email using template as fallback."""
|
|
1517
|
+
input_data = context.get('input_data', {})
|
|
1518
|
+
company_info = customer_data.get('companyInfo', {})
|
|
1519
|
+
contact_info = customer_data.get('primaryContact', {})
|
|
1520
|
+
sequence_number = follow_up_strategy.get('sequence_number', 1)
|
|
1521
|
+
|
|
1522
|
+
if sequence_number == 1:
|
|
1523
|
+
return f"""Hi {contact_info.get('name', 'there')},
|
|
1524
|
+
|
|
1525
|
+
I wanted to follow up on my previous message regarding {company_info.get('name', 'your company')} and how our {recommended_product.get('product_name', 'solution')} might be able to help.
|
|
1526
|
+
|
|
1527
|
+
I understand you're probably busy, but I'd love to hear your thoughts when you have a moment.
|
|
1528
|
+
|
|
1529
|
+
Would you be available for a brief 15-minute call this week?
|
|
1530
|
+
|
|
1531
|
+
Best regards,
|
|
1532
|
+
{input_data.get('staff_name', 'Sales Team')}
|
|
1533
|
+
{input_data.get('org_name', 'Our Company')}"""
|
|
1534
|
+
else:
|
|
1535
|
+
return f"""Hi {contact_info.get('name', 'there')},
|
|
1536
|
+
|
|
1537
|
+
I hope this message finds you well. I wanted to reach out one more time regarding the opportunity to help {company_info.get('name', 'your company')} with {recommended_product.get('product_name', 'our solution')}.
|
|
1538
|
+
|
|
1539
|
+
If now isn't the right time, I completely understand. Please feel free to reach out whenever it makes sense for your team.
|
|
1540
|
+
|
|
1541
|
+
Best regards,
|
|
1542
|
+
{input_data.get('staff_name', 'Sales Team')}
|
|
1543
|
+
{input_data.get('org_name', 'Our Company')}"""
|
|
1544
|
+
|
|
1545
|
+
def _rewrite_follow_up_draft(self, existing_draft: Dict[str, Any], reason: str, customer_data: Dict[str, Any],
|
|
1546
|
+
interaction_analysis: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
|
1547
|
+
"""Rewrite existing follow-up draft based on reason using LLM."""
|
|
1548
|
+
try:
|
|
1549
|
+
if self.is_dry_run():
|
|
1550
|
+
rewritten = existing_draft.copy()
|
|
1551
|
+
rewritten['draft_id'] = f"uuid:{str(uuid.uuid4())}"
|
|
1552
|
+
rewritten['draft_approach'] = "rewrite"
|
|
1553
|
+
rewritten['draft_type'] = "followup_rewrite"
|
|
1554
|
+
rewritten['email_body'] = f"[DRY RUN - REWRITTEN: {reason}] " + existing_draft.get('email_body', '')
|
|
1555
|
+
rewritten['rewrite_reason'] = reason
|
|
1556
|
+
rewritten['rewritten_at'] = datetime.now().isoformat()
|
|
1557
|
+
return rewritten
|
|
1558
|
+
|
|
1559
|
+
# Use similar logic to initial outreach rewrite but with follow-up context
|
|
1560
|
+
input_data = context.get('input_data', {})
|
|
1561
|
+
company_info = customer_data.get('companyInfo', {})
|
|
1562
|
+
contact_info = customer_data.get('primaryContact', {})
|
|
1563
|
+
|
|
1564
|
+
# Create rewrite prompt with follow-up context
|
|
1565
|
+
rewrite_prompt = f"""Rewrite the following follow-up email based on the feedback provided. Keep the follow-up nature and context but address the specific concerns mentioned.
|
|
1566
|
+
|
|
1567
|
+
ORIGINAL FOLLOW-UP EMAIL:
|
|
1568
|
+
{existing_draft.get('email_body', '')}
|
|
1569
|
+
|
|
1570
|
+
FEEDBACK/REASON FOR REWRITE:
|
|
1571
|
+
{reason}
|
|
1572
|
+
|
|
1573
|
+
FOLLOW-UP CONTEXT:
|
|
1574
|
+
- This is follow-up #{existing_draft.get('sequence_number', 1)}
|
|
1575
|
+
- Days since last interaction: {interaction_analysis.get('days_since_last_interaction', 0)}
|
|
1576
|
+
- Company: {company_info.get('name', 'the company')}
|
|
1577
|
+
- Contact: {contact_info.get('name', 'the contact')}
|
|
1578
|
+
|
|
1579
|
+
REQUIREMENTS:
|
|
1580
|
+
1. Address the feedback/reason provided
|
|
1581
|
+
2. Maintain the follow-up context and tone
|
|
1582
|
+
3. Keep it respectful and professional
|
|
1583
|
+
4. Don't be pushy or aggressive
|
|
1584
|
+
5. Include a clear but gentle call-to-action
|
|
1585
|
+
|
|
1586
|
+
Generate only the rewritten follow-up email content:"""
|
|
1587
|
+
|
|
1588
|
+
# Generate rewritten content using LLM
|
|
1589
|
+
rewritten_content = self.call_llm(
|
|
1590
|
+
prompt=rewrite_prompt,
|
|
1591
|
+
temperature=0.6,
|
|
1592
|
+
max_tokens=800
|
|
1593
|
+
)
|
|
1594
|
+
|
|
1595
|
+
# Clean the rewritten content
|
|
1596
|
+
cleaned_content = self._clean_email_content(rewritten_content)
|
|
1597
|
+
|
|
1598
|
+
# Create rewritten draft object
|
|
1599
|
+
rewritten = existing_draft.copy()
|
|
1600
|
+
rewritten['draft_id'] = f"uuid:{str(uuid.uuid4())}"
|
|
1601
|
+
rewritten['draft_approach'] = "rewrite"
|
|
1602
|
+
rewritten['draft_type'] = "followup_rewrite"
|
|
1603
|
+
rewritten['email_body'] = cleaned_content
|
|
1604
|
+
rewritten['rewrite_reason'] = reason
|
|
1605
|
+
rewritten['rewritten_at'] = datetime.now().isoformat()
|
|
1606
|
+
rewritten['version'] = existing_draft.get('version', 1) + 1
|
|
1607
|
+
rewritten['call_to_action'] = self._extract_call_to_action(cleaned_content)
|
|
1608
|
+
rewritten['personalization_score'] = self._calculate_personalization_score(cleaned_content, customer_data)
|
|
1609
|
+
|
|
1610
|
+
return rewritten
|
|
1611
|
+
|
|
1612
|
+
except Exception as e:
|
|
1613
|
+
self.logger.error(f"Failed to rewrite follow-up draft: {str(e)}")
|
|
1614
|
+
# Fallback to simple modification
|
|
1615
|
+
rewritten = existing_draft.copy()
|
|
1616
|
+
rewritten['draft_id'] = f"uuid:{str(uuid.uuid4())}"
|
|
1617
|
+
rewritten['draft_approach'] = "rewrite"
|
|
1618
|
+
rewritten['draft_type'] = "followup_rewrite"
|
|
1619
|
+
rewritten['email_body'] = f"[REWRITTEN: {reason}]\n\n" + existing_draft.get('email_body', '')
|
|
1620
|
+
rewritten['rewrite_reason'] = reason
|
|
1621
|
+
rewritten['rewritten_at'] = datetime.now().isoformat()
|
|
1622
|
+
return rewritten
|
|
1623
|
+
|
|
1624
|
+
def _save_rewritten_follow_up_draft(self, context: Dict[str, Any], rewritten_draft: Dict[str, Any], original_draft_id: str) -> Dict[str, Any]:
|
|
1625
|
+
"""Save rewritten follow-up draft to database and file system."""
|
|
1626
|
+
# Use similar logic to initial outreach but mark as follow-up rewrite
|
|
1627
|
+
rewritten_draft['original_draft_id'] = original_draft_id
|
|
1628
|
+
rewritten_draft['execution_id'] = context.get('execution_id')
|
|
1629
|
+
rewritten_draft['draft_type'] = 'follow_up_rewrite'
|
|
1630
|
+
|
|
1631
|
+
# Save using the same method as regular drafts
|
|
1632
|
+
saved_drafts = self._save_follow_up_drafts(context, [rewritten_draft])
|
|
1633
|
+
return saved_drafts[0] if saved_drafts else rewritten_draft
|
|
1634
|
+
|
|
1635
|
+
def _send_follow_up_email(self, draft: Dict[str, Any], recipient_address: str, recipient_name: str, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
1636
|
+
"""Send follow-up email immediately (similar to initial outreach but for follow-up)."""
|
|
1637
|
+
if self.is_dry_run():
|
|
1638
|
+
return {
|
|
1639
|
+
'success': True,
|
|
1640
|
+
'message': f'[DRY RUN] Would send follow-up email to {recipient_address}',
|
|
1641
|
+
'email_id': f'mock_followup_email_{uuid.uuid4().hex[:8]}'
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
try:
|
|
1645
|
+
# Use similar email sending logic as initial outreach
|
|
1646
|
+
# This would integrate with the RTA email service
|
|
1647
|
+
input_data = context.get('input_data', {})
|
|
1648
|
+
|
|
1649
|
+
# Get auto interaction settings from team settings
|
|
1650
|
+
auto_interaction_config = self._get_auto_interaction_config(input_data.get('team_id'))
|
|
1651
|
+
|
|
1652
|
+
# Prepare email payload for RTA email service (matching trigger_auto_interaction)
|
|
1653
|
+
email_payload = {
|
|
1654
|
+
"project_code": input_data.get('project_code', ''),
|
|
1655
|
+
"event_type": "custom",
|
|
1656
|
+
"event_id": input_data.get('customer_id', context.get('execution_id')),
|
|
1657
|
+
"type": "interaction",
|
|
1658
|
+
"family": "GLOBALSELL_INTERACT_EVENT_ADHOC",
|
|
1659
|
+
"language": input_data.get('language', 'english'),
|
|
1660
|
+
"submission_date": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
|
1661
|
+
"instanceName": f"Send follow-up email to {recipient_name} ({recipient_address}) from {auto_interaction_config.get('from_name', input_data.get('org_name', 'Unknown'))}",
|
|
1662
|
+
"instanceID": f"{context.get('execution_id')}_{input_data.get('customer_id', 'unknown')}",
|
|
1663
|
+
"uuid": f"{context.get('execution_id')}_{input_data.get('customer_id', 'unknown')}",
|
|
1664
|
+
"action_type": auto_interaction_config.get('tool_type', 'email').lower(),
|
|
1665
|
+
"email": recipient_address,
|
|
1666
|
+
"number": auto_interaction_config.get('from_number', input_data.get('customer_phone', '')),
|
|
1667
|
+
"subject": draft['subject_lines'][0] if draft.get('subject_lines') else 'Follow-up',
|
|
1668
|
+
"content": draft.get('email_body', ''),
|
|
1669
|
+
"team_id": input_data.get('team_id', ''),
|
|
1670
|
+
"from_email": auto_interaction_config.get('from_email', ''),
|
|
1671
|
+
"from_name": auto_interaction_config.get('from_name', ''),
|
|
1672
|
+
"email_cc": auto_interaction_config.get('email_cc', ''),
|
|
1673
|
+
"email_bcc": auto_interaction_config.get('email_bcc', ''),
|
|
1674
|
+
"extraData": {
|
|
1675
|
+
"org_id": input_data.get('org_id'),
|
|
1676
|
+
"human_action_id": input_data.get('human_action_id', ''),
|
|
1677
|
+
"email_tags": "gs_162_follow_up",
|
|
1678
|
+
"task_id": input_data.get('customer_id', context.get('execution_id'))
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
# Send to RTA email service
|
|
1683
|
+
headers = {
|
|
1684
|
+
'Content-Type': 'application/json'
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
response = requests.post(
|
|
1688
|
+
'https://automation.rta.vn/webhook/autoemail-trigger-by-inst-check',
|
|
1689
|
+
json=email_payload,
|
|
1690
|
+
headers=headers,
|
|
1691
|
+
timeout=30
|
|
1692
|
+
)
|
|
1693
|
+
|
|
1694
|
+
if response.status_code == 200:
|
|
1695
|
+
result = response.json()
|
|
1696
|
+
execution_id = result.get('executionID', '')
|
|
1697
|
+
|
|
1698
|
+
if execution_id:
|
|
1699
|
+
self.logger.info(f"Follow-up email sent successfully via RTA service: {execution_id}")
|
|
1700
|
+
return {
|
|
1701
|
+
'success': True,
|
|
1702
|
+
'message': f'Follow-up email sent to {recipient_address}',
|
|
1703
|
+
'email_id': execution_id,
|
|
1704
|
+
'service': 'RTA_email_service',
|
|
1705
|
+
'response': result
|
|
1706
|
+
}
|
|
1707
|
+
else:
|
|
1708
|
+
self.logger.warning("Email service returned success but no execution ID")
|
|
1709
|
+
return {
|
|
1710
|
+
'success': False,
|
|
1711
|
+
'message': 'Email service returned success but no execution ID',
|
|
1712
|
+
'response': result
|
|
1713
|
+
}
|
|
1714
|
+
else:
|
|
1715
|
+
self.logger.error(f"Email service returned error: {response.status_code} - {response.text}")
|
|
1716
|
+
return {
|
|
1717
|
+
'success': False,
|
|
1718
|
+
'message': f'Email service error: {response.status_code}',
|
|
1719
|
+
'error': response.text
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
except Exception as e:
|
|
1723
|
+
self.logger.error(f"Follow-up email sending failed: {str(e)}")
|
|
1724
|
+
return {
|
|
1725
|
+
'success': False,
|
|
1726
|
+
'message': f'Follow-up email sending failed: {str(e)}',
|
|
1727
|
+
'error': str(e)
|
|
1728
|
+
}
|