fusesell 1.2.6__tar.gz → 1.2.8__tar.gz
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.
Potentially problematic release.
This version of fusesell might be problematic. Click here for more details.
- {fusesell-1.2.6 → fusesell-1.2.8}/CHANGELOG.md +19 -0
- {fusesell-1.2.6/fusesell.egg-info → fusesell-1.2.8}/PKG-INFO +1 -1
- {fusesell-1.2.6 → fusesell-1.2.8/fusesell.egg-info}/PKG-INFO +1 -1
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/__init__.py +1 -1
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/pipeline.py +23 -19
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/stages/initial_outreach.py +210 -83
- {fusesell-1.2.6 → fusesell-1.2.8}/pyproject.toml +1 -1
- {fusesell-1.2.6 → fusesell-1.2.8}/LICENSE +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/MANIFEST.in +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/README.md +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell.egg-info/SOURCES.txt +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell.egg-info/dependency_links.txt +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell.egg-info/entry_points.txt +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell.egg-info/requires.txt +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell.egg-info/top_level.txt +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/api.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/cli.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/config/__init__.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/config/prompts.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/config/settings.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/stages/__init__.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/stages/base_stage.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/stages/data_acquisition.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/stages/data_preparation.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/stages/follow_up.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/stages/lead_scoring.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/tests/conftest.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/tests/test_api.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/tests/test_cli.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/tests/test_data_manager_products.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/tests/test_data_manager_sales_process.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/tests/test_data_manager_teams.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/utils/__init__.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/utils/birthday_email_manager.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/utils/data_manager.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/utils/event_scheduler.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/utils/llm_client.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/utils/logger.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/utils/timezone_detector.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/fusesell_local/utils/validators.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/requirements.txt +0 -0
- {fusesell-1.2.6 → fusesell-1.2.8}/setup.cfg +0 -0
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to FuseSell Local will be documented in this file.
|
|
4
4
|
|
|
5
|
+
# [1.2.8] - 2025-10-24
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Initial outreach resolves the primary sales rep from `gs_team_rep` and injects their identity into prompts, reminders, and draft metadata so outreach reflects real team settings.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Sanitizes generated email bodies to replace or remove `[Your …]` placeholders, ensuring signatures contain actual values even when optional rep fields are missing.
|
|
12
|
+
- Reminder scheduling now preserves merged contact emails so follow-up records always carry `customer_email` for downstream automations.
|
|
13
|
+
|
|
14
|
+
# [1.2.7] - 2025-10-24
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- RealTimeX sales-process normalization now forwards `recipient_address`, `recipient_name`, and `customer_email` into the FuseSell pipeline so prompt generation and scheduling have complete context.
|
|
18
|
+
- Default outreach prompt replacements enrich customer metadata with toolkit-derived contact details and enforce first-name greetings to match server quality.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Initial outreach stage now records generated drafts in the pipeline summary, seeds reminder_task rows with toolkit credentials, and schedules follow-up events when `send_immediately` is false.
|
|
22
|
+
- Prompt-based draft generation no longer skips scheduling due to missing email fields and guarantees outputs without unresolved placeholders.
|
|
23
|
+
|
|
5
24
|
# [1.2.6] - 2025-10-24
|
|
6
25
|
|
|
7
26
|
### Added
|
|
@@ -377,15 +377,16 @@ class FuseSellPipeline:
|
|
|
377
377
|
return {
|
|
378
378
|
'org_id': self.config['org_id'],
|
|
379
379
|
'org_name': self.config['org_name'],
|
|
380
|
-
'team_id': self.config.get('team_id'),
|
|
381
|
-
'team_name': self.config.get('team_name'),
|
|
382
|
-
'project_code': self.config.get('project_code'),
|
|
383
|
-
'staff_name': self.config.get('staff_name', 'Sales Team'),
|
|
384
|
-
'
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
'
|
|
388
|
-
'
|
|
380
|
+
'team_id': self.config.get('team_id'),
|
|
381
|
+
'team_name': self.config.get('team_name'),
|
|
382
|
+
'project_code': self.config.get('project_code'),
|
|
383
|
+
'staff_name': self.config.get('staff_name', 'Sales Team'),
|
|
384
|
+
'customer_name': self.config.get('customer_name', ''),
|
|
385
|
+
'language': self.config.get('language', 'english'),
|
|
386
|
+
# Data sources (matching executor schema)
|
|
387
|
+
'input_website': self.config.get('input_website', ''),
|
|
388
|
+
'input_description': self.config.get('input_description', ''),
|
|
389
|
+
'input_business_card': self.config.get('input_business_card', ''),
|
|
389
390
|
'input_linkedin_url': self.config.get('input_linkedin_url', ''),
|
|
390
391
|
'input_facebook_url': self.config.get('input_facebook_url', ''),
|
|
391
392
|
'input_freetext': self.config.get('input_freetext', ''),
|
|
@@ -396,10 +397,11 @@ class FuseSellPipeline:
|
|
|
396
397
|
|
|
397
398
|
# Action and continuation fields (for server executor compatibility)
|
|
398
399
|
'action': self.config.get('action', 'draft_write'),
|
|
399
|
-
'selected_draft_id': self.config.get('selected_draft_id', ''),
|
|
400
|
-
'reason': self.config.get('reason', ''),
|
|
400
|
+
'selected_draft_id': self.config.get('selected_draft_id', ''),
|
|
401
|
+
'reason': self.config.get('reason', ''),
|
|
401
402
|
'recipient_address': self.config.get('recipient_address', ''),
|
|
402
403
|
'recipient_name': self.config.get('recipient_name', ''),
|
|
404
|
+
'customer_email': self.config.get('customer_email', ''),
|
|
403
405
|
'interaction_type': self.config.get('interaction_type', 'email'),
|
|
404
406
|
'human_action_id': self.config.get('human_action_id', ''),
|
|
405
407
|
|
|
@@ -600,14 +602,16 @@ class FuseSellPipeline:
|
|
|
600
602
|
if result.get('status') == 'success':
|
|
601
603
|
data = result.get('data', {})
|
|
602
604
|
|
|
603
|
-
if stage_name == 'data_preparation':
|
|
604
|
-
customer_data = data
|
|
605
|
-
elif stage_name == 'lead_scoring':
|
|
606
|
-
lead_scores = data.get('scores', [])
|
|
607
|
-
elif stage_name == 'initial_outreach':
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
605
|
+
if stage_name == 'data_preparation':
|
|
606
|
+
customer_data = data
|
|
607
|
+
elif stage_name == 'lead_scoring':
|
|
608
|
+
lead_scores = data.get('scores', [])
|
|
609
|
+
elif stage_name == 'initial_outreach':
|
|
610
|
+
drafts = data.get('drafts') or data.get('email_drafts') or []
|
|
611
|
+
email_drafts.extend(drafts)
|
|
612
|
+
elif stage_name == 'follow_up':
|
|
613
|
+
drafts = data.get('drafts') or data.get('email_drafts') or []
|
|
614
|
+
email_drafts.extend(drafts)
|
|
611
615
|
|
|
612
616
|
# Generate performance analytics
|
|
613
617
|
performance_analytics = self._generate_performance_analytics(duration)
|
|
@@ -12,13 +12,17 @@ from datetime import datetime
|
|
|
12
12
|
from .base_stage import BaseStage
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class InitialOutreachStage(BaseStage):
|
|
16
|
-
"""
|
|
17
|
-
Initial Outreach stage with full server executor schema compliance.
|
|
18
|
-
Supports: draft_write, draft_rewrite, send, close actions.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
def
|
|
15
|
+
class InitialOutreachStage(BaseStage):
|
|
16
|
+
"""
|
|
17
|
+
Initial Outreach stage with full server executor schema compliance.
|
|
18
|
+
Supports: draft_write, draft_rewrite, send, close actions.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *args, **kwargs):
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
self._active_rep_profile: Dict[str, Any] = {}
|
|
24
|
+
|
|
25
|
+
def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
22
26
|
"""
|
|
23
27
|
Execute initial outreach stage with action-based routing (matching server executor).
|
|
24
28
|
|
|
@@ -99,9 +103,21 @@ class InitialOutreachStage(BaseStage):
|
|
|
99
103
|
if not recommended_product:
|
|
100
104
|
raise ValueError("No product recommendation available for email generation")
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
rep_profile = self._resolve_primary_sales_rep(context)
|
|
107
|
+
self._active_rep_profile = rep_profile or {}
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
# Generate multiple email drafts
|
|
111
|
+
email_drafts = self._generate_email_drafts(
|
|
112
|
+
customer_data,
|
|
113
|
+
recommended_product,
|
|
114
|
+
scoring_data,
|
|
115
|
+
context,
|
|
116
|
+
rep_profile=self._active_rep_profile
|
|
117
|
+
)
|
|
118
|
+
finally:
|
|
119
|
+
self._active_rep_profile = {}
|
|
120
|
+
|
|
105
121
|
# Save drafts to local files and database
|
|
106
122
|
saved_drafts = self._save_email_drafts(context, email_drafts)
|
|
107
123
|
|
|
@@ -115,10 +131,11 @@ class InitialOutreachStage(BaseStage):
|
|
|
115
131
|
outreach_data = {
|
|
116
132
|
'action': 'draft_write',
|
|
117
133
|
'status': 'drafts_generated',
|
|
118
|
-
'email_drafts': saved_drafts,
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
134
|
+
'email_drafts': saved_drafts,
|
|
135
|
+
'drafts': saved_drafts,
|
|
136
|
+
'recommended_product': recommended_product,
|
|
137
|
+
'customer_summary': self._create_customer_summary(customer_data),
|
|
138
|
+
'total_drafts_generated': len(saved_drafts),
|
|
122
139
|
'generation_timestamp': datetime.now().isoformat(),
|
|
123
140
|
'customer_id': context.get('execution_id')
|
|
124
141
|
}
|
|
@@ -327,8 +344,8 @@ class InitialOutreachStage(BaseStage):
|
|
|
327
344
|
team_id = input_data.get('team_id')
|
|
328
345
|
team_name = input_data.get('team_name')
|
|
329
346
|
language = input_data.get('language')
|
|
330
|
-
customer_name = input_data.get('customer_name')
|
|
331
|
-
staff_name = input_data.get('staff_name')
|
|
347
|
+
customer_name = input_data.get('customer_name') or input_data.get('recipient_name')
|
|
348
|
+
staff_name = input_data.get('staff_name') or self.config.get('staff_name') or 'Sales Team'
|
|
332
349
|
reminder_room = self.config.get('reminder_room_id') or input_data.get('reminder_room_id')
|
|
333
350
|
draft_id = draft.get('draft_id') or 'unknown_draft'
|
|
334
351
|
|
|
@@ -355,6 +372,8 @@ class InitialOutreachStage(BaseStage):
|
|
|
355
372
|
customextra['approach'] = draft.get('approach')
|
|
356
373
|
if draft.get('mail_tone'):
|
|
357
374
|
customextra['mail_tone'] = draft.get('mail_tone')
|
|
375
|
+
if recipient_address and 'customer_email' not in customextra:
|
|
376
|
+
customextra['customer_email'] = recipient_address
|
|
358
377
|
|
|
359
378
|
return {
|
|
360
379
|
'status': 'published',
|
|
@@ -368,6 +387,7 @@ class InitialOutreachStage(BaseStage):
|
|
|
368
387
|
'team_name': team_name,
|
|
369
388
|
'language': language,
|
|
370
389
|
'customer_name': customer_name,
|
|
390
|
+
'customer_email': recipient_address,
|
|
371
391
|
'staff_name': staff_name,
|
|
372
392
|
'customextra': customextra
|
|
373
393
|
}
|
|
@@ -394,11 +414,18 @@ class InitialOutreachStage(BaseStage):
|
|
|
394
414
|
return None
|
|
395
415
|
|
|
396
416
|
contact_info = customer_data.get('primaryContact', {}) or {}
|
|
417
|
+
stage_results = context.get('stage_results', {}) or {}
|
|
418
|
+
data_acquisition = {}
|
|
419
|
+
if isinstance(stage_results, dict):
|
|
420
|
+
data_acquisition = stage_results.get('data_acquisition', {}).get('data', {}) or {}
|
|
397
421
|
|
|
398
422
|
recipient_address = (
|
|
399
423
|
input_data.get('recipient_address')
|
|
400
424
|
or contact_info.get('email')
|
|
401
425
|
or contact_info.get('emailAddress')
|
|
426
|
+
or data_acquisition.get('customer_email')
|
|
427
|
+
or data_acquisition.get('contact_email')
|
|
428
|
+
or input_data.get('customer_email')
|
|
402
429
|
)
|
|
403
430
|
if not recipient_address:
|
|
404
431
|
self.logger.info("Skipping reminder scheduling: recipient email not available")
|
|
@@ -408,6 +435,8 @@ class InitialOutreachStage(BaseStage):
|
|
|
408
435
|
input_data.get('recipient_name')
|
|
409
436
|
or contact_info.get('name')
|
|
410
437
|
or contact_info.get('fullName')
|
|
438
|
+
or data_acquisition.get('contact_name')
|
|
439
|
+
or data_acquisition.get('customer_name')
|
|
411
440
|
or ''
|
|
412
441
|
)
|
|
413
442
|
|
|
@@ -611,16 +640,31 @@ class InitialOutreachStage(BaseStage):
|
|
|
611
640
|
self.logger.error(f"Failed to get auto interaction config for team {team_id}: {str(e)}")
|
|
612
641
|
return default_config
|
|
613
642
|
|
|
614
|
-
def _generate_email_drafts(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any], scoring_data: Dict[str, Any], context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
615
|
-
"""Generate multiple personalized email drafts using LLM."""
|
|
616
|
-
if self.is_dry_run():
|
|
617
|
-
return self._get_mock_email_drafts(customer_data, recommended_product, context)
|
|
618
|
-
|
|
619
|
-
try:
|
|
620
|
-
input_data = context.get('input_data', {})
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
643
|
+
def _generate_email_drafts(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any], scoring_data: Dict[str, Any], context: Dict[str, Any], rep_profile: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
|
644
|
+
"""Generate multiple personalized email drafts using LLM."""
|
|
645
|
+
if self.is_dry_run():
|
|
646
|
+
return self._get_mock_email_drafts(customer_data, recommended_product, context)
|
|
647
|
+
|
|
648
|
+
try:
|
|
649
|
+
input_data = context.get('input_data', {})
|
|
650
|
+
rep_profile = rep_profile or {}
|
|
651
|
+
if rep_profile:
|
|
652
|
+
primary_name = rep_profile.get('name')
|
|
653
|
+
if primary_name:
|
|
654
|
+
input_data['staff_name'] = primary_name
|
|
655
|
+
self.config['staff_name'] = primary_name
|
|
656
|
+
if rep_profile.get('email'):
|
|
657
|
+
input_data.setdefault('staff_email', rep_profile.get('email'))
|
|
658
|
+
if rep_profile.get('phone') or rep_profile.get('primary_phone'):
|
|
659
|
+
input_data.setdefault('staff_phone', rep_profile.get('phone') or rep_profile.get('primary_phone'))
|
|
660
|
+
if rep_profile.get('position'):
|
|
661
|
+
input_data.setdefault('staff_title', rep_profile.get('position'))
|
|
662
|
+
if rep_profile.get('website'):
|
|
663
|
+
input_data.setdefault('staff_website', rep_profile.get('website'))
|
|
664
|
+
|
|
665
|
+
company_info = customer_data.get('companyInfo', {})
|
|
666
|
+
contact_info = customer_data.get('primaryContact', {})
|
|
667
|
+
pain_points = customer_data.get('painPoints', [])
|
|
624
668
|
|
|
625
669
|
prompt_drafts = self._generate_email_drafts_from_prompt(
|
|
626
670
|
customer_data,
|
|
@@ -819,46 +863,80 @@ class InitialOutreachStage(BaseStage):
|
|
|
819
863
|
|
|
820
864
|
def _build_prompt_replacements(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any], scoring_data: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, str]:
|
|
821
865
|
input_data = context.get('input_data', {})
|
|
822
|
-
company_info = customer_data.get('companyInfo', {}) or {}
|
|
823
|
-
contact_info = customer_data.get('primaryContact', {}) or {}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
866
|
+
company_info = customer_data.get('companyInfo', {}) or {}
|
|
867
|
+
contact_info = dict(customer_data.get('primaryContact', {}) or {})
|
|
868
|
+
stage_results = context.get('stage_results', {}) or {}
|
|
869
|
+
data_acquisition = {}
|
|
870
|
+
if isinstance(stage_results, dict):
|
|
871
|
+
data_acquisition = stage_results.get('data_acquisition', {}).get('data', {}) or {}
|
|
872
|
+
if not contact_info.get('name'):
|
|
873
|
+
fallback_contact_name = (
|
|
874
|
+
data_acquisition.get('contact_name')
|
|
875
|
+
or data_acquisition.get('customer_contact')
|
|
876
|
+
or input_data.get('recipient_name')
|
|
877
|
+
or input_data.get('customer_name')
|
|
878
|
+
)
|
|
879
|
+
if fallback_contact_name:
|
|
880
|
+
contact_info['name'] = fallback_contact_name
|
|
881
|
+
if not contact_info.get('email'):
|
|
882
|
+
fallback_email = (
|
|
883
|
+
data_acquisition.get('customer_email')
|
|
884
|
+
or data_acquisition.get('contact_email')
|
|
885
|
+
or input_data.get('recipient_address')
|
|
886
|
+
or input_data.get('customer_email')
|
|
887
|
+
)
|
|
888
|
+
if fallback_email:
|
|
889
|
+
contact_info['email'] = fallback_email
|
|
890
|
+
customer_data = dict(customer_data)
|
|
891
|
+
customer_data['primaryContact'] = contact_info
|
|
892
|
+
language = input_data.get('language') or company_info.get('language') or 'English'
|
|
893
|
+
contact_name = contact_info.get('name') or input_data.get('customer_name') or input_data.get('recipient_name') or 'there'
|
|
894
|
+
company_name = company_info.get('name') or input_data.get('company_name') or 'the company'
|
|
895
|
+
staff_name = input_data.get('staff_name') or input_data.get('sender_name') or 'Sales Team'
|
|
896
|
+
org_name = input_data.get('org_name') or 'Our Company'
|
|
897
|
+
selected_product_name = recommended_product.get('product_name') if recommended_product else None
|
|
898
|
+
language_lower = language.lower() if isinstance(language, str) else ''
|
|
899
|
+
name_parts = contact_name.split() if isinstance(contact_name, str) else []
|
|
900
|
+
if name_parts:
|
|
901
|
+
if language_lower in ('vietnamese', 'vi'):
|
|
902
|
+
first_name = name_parts[-1]
|
|
903
|
+
else:
|
|
904
|
+
first_name = name_parts[0]
|
|
905
|
+
else:
|
|
906
|
+
first_name = contact_name or ''
|
|
907
|
+
|
|
908
|
+
action = input_data.get('action', 'draft_write')
|
|
909
|
+
action_labels = {
|
|
910
|
+
'draft_write': 'email drafts',
|
|
911
|
+
'draft_rewrite': 'email rewrites',
|
|
835
912
|
'send': 'email sends',
|
|
836
913
|
'close': 'email workflow'
|
|
837
914
|
}
|
|
838
915
|
action_type = action_labels.get(action, action.replace('_', ' '))
|
|
839
916
|
|
|
840
|
-
company_summary = self._build_company_info_summary(
|
|
841
|
-
company_info,
|
|
842
|
-
contact_info,
|
|
843
|
-
customer_data.get('painPoints', []),
|
|
844
|
-
scoring_data
|
|
845
|
-
)
|
|
846
|
-
product_summary = self._build_product_info_summary(recommended_product)
|
|
847
|
-
first_name_guide = self._build_first_name_guide(language, contact_name)
|
|
848
|
-
|
|
849
|
-
replacements = {
|
|
850
|
-
'##action_type##': action_type,
|
|
851
|
-
'##language##': language.title() if isinstance(language, str) else 'English',
|
|
852
|
-
'##customer_name##': contact_name,
|
|
917
|
+
company_summary = self._build_company_info_summary(
|
|
918
|
+
company_info,
|
|
919
|
+
contact_info,
|
|
920
|
+
customer_data.get('painPoints', []),
|
|
921
|
+
scoring_data
|
|
922
|
+
)
|
|
923
|
+
product_summary = self._build_product_info_summary(recommended_product)
|
|
924
|
+
first_name_guide = self._build_first_name_guide(language, contact_name)
|
|
925
|
+
|
|
926
|
+
replacements = {
|
|
927
|
+
'##action_type##': action_type,
|
|
928
|
+
'##language##': language.title() if isinstance(language, str) else 'English',
|
|
929
|
+
'##customer_name##': contact_name,
|
|
853
930
|
'##company_name##': company_name,
|
|
854
|
-
'##staff_name##': staff_name,
|
|
855
|
-
'##org_name##': org_name,
|
|
856
|
-
'##first_name_guide##': first_name_guide,
|
|
857
|
-
'##
|
|
858
|
-
'##
|
|
859
|
-
'##
|
|
860
|
-
|
|
861
|
-
|
|
931
|
+
'##staff_name##': staff_name,
|
|
932
|
+
'##org_name##': org_name,
|
|
933
|
+
'##first_name_guide##': first_name_guide,
|
|
934
|
+
'##customer_first_name##': first_name or contact_name,
|
|
935
|
+
'##selected_product##': selected_product_name or 'our solution',
|
|
936
|
+
'##company_info##': company_summary,
|
|
937
|
+
'##selected_product_info##': product_summary
|
|
938
|
+
}
|
|
939
|
+
|
|
862
940
|
return {key: (value if value is not None else '') for key, value in replacements.items()}
|
|
863
941
|
|
|
864
942
|
def _build_company_info_summary(self, company_info: Dict[str, Any], contact_info: Dict[str, Any], pain_points: List[Dict[str, Any]], scoring_data: Dict[str, Any]) -> str:
|
|
@@ -932,26 +1010,72 @@ class InitialOutreachStage(BaseStage):
|
|
|
932
1010
|
summary = "\n".join(lines).strip()
|
|
933
1011
|
return summary or 'Product details unavailable.'
|
|
934
1012
|
|
|
935
|
-
def _build_first_name_guide(self, language: str, contact_name: str) -> str:
|
|
936
|
-
if not language:
|
|
937
|
-
return ''
|
|
938
|
-
|
|
939
|
-
language_lower = language.lower()
|
|
940
|
-
if
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
if
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1013
|
+
def _build_first_name_guide(self, language: str, contact_name: str) -> str:
|
|
1014
|
+
if not language:
|
|
1015
|
+
return ''
|
|
1016
|
+
|
|
1017
|
+
language_lower = language.lower()
|
|
1018
|
+
name_parts = contact_name.split() if contact_name else []
|
|
1019
|
+
if language_lower in ('vietnamese', 'vi'):
|
|
1020
|
+
if not contact_name or contact_name.lower() == 'a person':
|
|
1021
|
+
return "If the recipient's name is unknown, use `anh/chi` in the greeting."
|
|
1022
|
+
vn_name = name_parts[-1] if name_parts else contact_name
|
|
1023
|
+
if vn_name:
|
|
1024
|
+
return f"For Vietnamese recipients, use `anh/chi {vn_name}` in the greeting to keep it respectful. Do not use placeholders or omit the honorific."
|
|
1025
|
+
return "For Vietnamese recipients, use `anh/chi` followed by the recipient's first name in the greeting."
|
|
1026
|
+
|
|
1027
|
+
if name_parts:
|
|
1028
|
+
en_name = name_parts[0]
|
|
1029
|
+
return (
|
|
1030
|
+
f'Use only the recipient\'s first name "{en_name}" in the greeting. '
|
|
1031
|
+
f'Start with "Hi {en_name}," or "Hello {en_name}," and do not use the surname or placeholders.'
|
|
1032
|
+
)
|
|
1033
|
+
return 'If the recipient name is unknown, use a neutral greeting like "Hi there," without placeholders.'
|
|
1034
|
+
|
|
1035
|
+
def _resolve_primary_sales_rep(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
1036
|
+
team_id = context.get('input_data', {}).get('team_id') or self.config.get('team_id')
|
|
1037
|
+
if not team_id:
|
|
1038
|
+
return {}
|
|
1039
|
+
reps = self.get_team_setting('gs_team_rep', team_id, [])
|
|
1040
|
+
if not isinstance(reps, list):
|
|
1041
|
+
return {}
|
|
1042
|
+
for rep in reps:
|
|
1043
|
+
if rep and rep.get('is_primary'):
|
|
1044
|
+
return rep
|
|
1045
|
+
return reps[0] if reps else {}
|
|
1046
|
+
|
|
1047
|
+
def _sanitize_email_body(self, html: str, staff_name: str, rep_profile: Dict[str, Any]) -> str:
|
|
1048
|
+
if not html:
|
|
1049
|
+
return ''
|
|
1050
|
+
|
|
1051
|
+
replacements = {
|
|
1052
|
+
'[Your Name]': rep_profile.get('name') or staff_name,
|
|
1053
|
+
'[Your Email]': rep_profile.get('email'),
|
|
1054
|
+
'[Your Phone Number]': rep_profile.get('phone') or rep_profile.get('primary_phone'),
|
|
1055
|
+
'[Your Phone]': rep_profile.get('phone') or rep_profile.get('primary_phone'),
|
|
1056
|
+
'[Your Title]': rep_profile.get('position'),
|
|
1057
|
+
'[Your LinkedIn Profile]': rep_profile.get('linkedin') or rep_profile.get('linkedin_profile'),
|
|
1058
|
+
'[Your LinkedIn Profile URL]': rep_profile.get('linkedin') or rep_profile.get('linkedin_profile'),
|
|
1059
|
+
'[Your Website]': rep_profile.get('website'),
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
for placeholder, value in replacements.items():
|
|
1063
|
+
if value:
|
|
1064
|
+
html = html.replace(placeholder, str(value))
|
|
1065
|
+
else:
|
|
1066
|
+
html = html.replace(placeholder, '')
|
|
1067
|
+
|
|
1068
|
+
# Remove any lingering placeholder fragments such as "[Your LinkedIn Profile"
|
|
1069
|
+
html = re.sub(r'\[Your[^<\]]+\]?', '', html, flags=re.IGNORECASE)
|
|
1070
|
+
# Collapse empty paragraphs created by placeholder removal
|
|
1071
|
+
html = re.sub(r'(<p>\s*</p>)+', '', html, flags=re.IGNORECASE)
|
|
1072
|
+
return html
|
|
1073
|
+
|
|
1074
|
+
def _extract_first_name(self, full_name: str) -> str:
|
|
1075
|
+
if not full_name:
|
|
1076
|
+
return ''
|
|
1077
|
+
parts = full_name.strip().split()
|
|
1078
|
+
return parts[-1] if parts else full_name
|
|
955
1079
|
|
|
956
1080
|
def _strip_code_fences(self, text: str) -> str:
|
|
957
1081
|
if not text:
|
|
@@ -1048,9 +1172,12 @@ class InitialOutreachStage(BaseStage):
|
|
|
1048
1172
|
tags = [tags]
|
|
1049
1173
|
tags = [str(tag).strip() for tag in tags if str(tag).strip()]
|
|
1050
1174
|
|
|
1051
|
-
call_to_action = self._extract_call_to_action(email_body)
|
|
1052
|
-
personalization_score = self._calculate_personalization_score(email_body, customer_data)
|
|
1053
|
-
message_type = entry.get('message_type') or 'Email'
|
|
1175
|
+
call_to_action = self._extract_call_to_action(email_body)
|
|
1176
|
+
personalization_score = self._calculate_personalization_score(email_body, customer_data)
|
|
1177
|
+
message_type = entry.get('message_type') or 'Email'
|
|
1178
|
+
rep_profile = getattr(self, '_active_rep_profile', {}) or {}
|
|
1179
|
+
staff_name = context.get('input_data', {}).get('staff_name') or self.config.get('staff_name', 'Sales Team')
|
|
1180
|
+
email_body = self._sanitize_email_body(email_body, staff_name, rep_profile)
|
|
1054
1181
|
|
|
1055
1182
|
metadata = {
|
|
1056
1183
|
'customer_company': customer_data.get('companyInfo', {}).get('name', 'Unknown'),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|