fusesell 1.2.7__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.7 → fusesell-1.2.8}/CHANGELOG.md +9 -0
- {fusesell-1.2.7/fusesell.egg-info → fusesell-1.2.8}/PKG-INFO +1 -1
- {fusesell-1.2.7 → fusesell-1.2.8/fusesell.egg-info}/PKG-INFO +1 -1
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/__init__.py +1 -1
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/initial_outreach.py +102 -29
- {fusesell-1.2.7 → fusesell-1.2.8}/pyproject.toml +1 -1
- {fusesell-1.2.7 → fusesell-1.2.8}/LICENSE +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/MANIFEST.in +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/README.md +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell.egg-info/SOURCES.txt +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell.egg-info/dependency_links.txt +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell.egg-info/entry_points.txt +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell.egg-info/requires.txt +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell.egg-info/top_level.txt +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/api.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/cli.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/config/__init__.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/config/prompts.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/config/settings.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/pipeline.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/__init__.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/base_stage.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/data_acquisition.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/data_preparation.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/follow_up.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/lead_scoring.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/tests/conftest.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/tests/test_api.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/tests/test_cli.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/tests/test_data_manager_products.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/tests/test_data_manager_sales_process.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/tests/test_data_manager_teams.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/__init__.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/birthday_email_manager.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/data_manager.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/event_scheduler.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/llm_client.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/logger.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/timezone_detector.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/validators.py +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/requirements.txt +0 -0
- {fusesell-1.2.7 → fusesell-1.2.8}/setup.cfg +0 -0
|
@@ -2,6 +2,15 @@
|
|
|
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
|
+
|
|
5
14
|
# [1.2.7] - 2025-10-24
|
|
6
15
|
|
|
7
16
|
### Changed
|
|
@@ -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
|
|
|
@@ -624,16 +640,31 @@ class InitialOutreachStage(BaseStage):
|
|
|
624
640
|
self.logger.error(f"Failed to get auto interaction config for team {team_id}: {str(e)}")
|
|
625
641
|
return default_config
|
|
626
642
|
|
|
627
|
-
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]]:
|
|
628
|
-
"""Generate multiple personalized email drafts using LLM."""
|
|
629
|
-
if self.is_dry_run():
|
|
630
|
-
return self._get_mock_email_drafts(customer_data, recommended_product, context)
|
|
631
|
-
|
|
632
|
-
try:
|
|
633
|
-
input_data = context.get('input_data', {})
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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', [])
|
|
637
668
|
|
|
638
669
|
prompt_drafts = self._generate_email_drafts_from_prompt(
|
|
639
670
|
customer_data,
|
|
@@ -1000,12 +1031,51 @@ class InitialOutreachStage(BaseStage):
|
|
|
1000
1031
|
f'Start with "Hi {en_name}," or "Hello {en_name}," and do not use the surname or placeholders.'
|
|
1001
1032
|
)
|
|
1002
1033
|
return 'If the recipient name is unknown, use a neutral greeting like "Hi there," without placeholders.'
|
|
1003
|
-
|
|
1004
|
-
def
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
|
1009
1079
|
|
|
1010
1080
|
def _strip_code_fences(self, text: str) -> str:
|
|
1011
1081
|
if not text:
|
|
@@ -1102,9 +1172,12 @@ class InitialOutreachStage(BaseStage):
|
|
|
1102
1172
|
tags = [tags]
|
|
1103
1173
|
tags = [str(tag).strip() for tag in tags if str(tag).strip()]
|
|
1104
1174
|
|
|
1105
|
-
call_to_action = self._extract_call_to_action(email_body)
|
|
1106
|
-
personalization_score = self._calculate_personalization_score(email_body, customer_data)
|
|
1107
|
-
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)
|
|
1108
1181
|
|
|
1109
1182
|
metadata = {
|
|
1110
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
|
|
File without changes
|