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.
Files changed (35) hide show
  1. fusesell-1.3.42.dist-info/METADATA +873 -0
  2. fusesell-1.3.42.dist-info/RECORD +35 -0
  3. fusesell-1.3.42.dist-info/WHEEL +5 -0
  4. fusesell-1.3.42.dist-info/entry_points.txt +2 -0
  5. fusesell-1.3.42.dist-info/licenses/LICENSE +21 -0
  6. fusesell-1.3.42.dist-info/top_level.txt +2 -0
  7. fusesell.py +20 -0
  8. fusesell_local/__init__.py +37 -0
  9. fusesell_local/api.py +343 -0
  10. fusesell_local/cli.py +1480 -0
  11. fusesell_local/config/__init__.py +11 -0
  12. fusesell_local/config/default_email_templates.json +34 -0
  13. fusesell_local/config/default_prompts.json +19 -0
  14. fusesell_local/config/default_scoring_criteria.json +154 -0
  15. fusesell_local/config/prompts.py +245 -0
  16. fusesell_local/config/settings.py +277 -0
  17. fusesell_local/pipeline.py +978 -0
  18. fusesell_local/stages/__init__.py +19 -0
  19. fusesell_local/stages/base_stage.py +603 -0
  20. fusesell_local/stages/data_acquisition.py +1820 -0
  21. fusesell_local/stages/data_preparation.py +1238 -0
  22. fusesell_local/stages/follow_up.py +1728 -0
  23. fusesell_local/stages/initial_outreach.py +2972 -0
  24. fusesell_local/stages/lead_scoring.py +1452 -0
  25. fusesell_local/utils/__init__.py +36 -0
  26. fusesell_local/utils/agent_context.py +552 -0
  27. fusesell_local/utils/auto_setup.py +361 -0
  28. fusesell_local/utils/birthday_email_manager.py +467 -0
  29. fusesell_local/utils/data_manager.py +4857 -0
  30. fusesell_local/utils/event_scheduler.py +959 -0
  31. fusesell_local/utils/llm_client.py +342 -0
  32. fusesell_local/utils/logger.py +203 -0
  33. fusesell_local/utils/output_helpers.py +2443 -0
  34. fusesell_local/utils/timezone_detector.py +914 -0
  35. 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
+ }