fusesell 1.2.7__py3-none-any.whl → 1.2.8__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.

Potentially problematic release.


This version of fusesell might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusesell
3
- Version: 1.2.7
3
+ Version: 1.2.8
4
4
  Summary: Local implementation of FuseSell AI sales automation pipeline
5
5
  Author-email: FuseSell Team <team@fusesell.ai>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  fusesell.py,sha256=t5PjkhWEJGINp4k517u0EX0ge7lzuHOUHHro-BE1mGk,596
2
- fusesell-1.2.7.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
3
- fusesell_local/__init__.py,sha256=8U1jX2uHDz_7rJeLg7NGKJGJum7I8VK-RwZCtXxRVrs,966
2
+ fusesell-1.2.8.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
3
+ fusesell_local/__init__.py,sha256=ORlqpW5kTRov-Xw9f3Bk0L7wy1Q5bxTdaZAT9fIhLcQ,966
4
4
  fusesell_local/api.py,sha256=AcPune5YJdgi7nsMeusCUqc49z5UiycsQb6n3yiV_No,10839
5
5
  fusesell_local/cli.py,sha256=MYnVxuEf5KTR4VcO3sc-VtP9NkWlSixJsYfOWST2Ds0,65859
6
6
  fusesell_local/pipeline.py,sha256=RMF_kgwNEc1ka8-CDJyzIOTSo8PGtR_zPKAgRevhlNo,39913
@@ -12,7 +12,7 @@ fusesell_local/stages/base_stage.py,sha256=ldo5xuHZto7ceEg3i_3rxAx0xPccK4n2jaxEJ
12
12
  fusesell_local/stages/data_acquisition.py,sha256=Td3mwakJRoEYbi3od4v2ZzKOHLgLSgccZVxH3ezs1_4,71081
13
13
  fusesell_local/stages/data_preparation.py,sha256=XWLg9b1w2NrMxLcrWDqB95mRmLQmVIMXpKNaBNr98TQ,52751
14
14
  fusesell_local/stages/follow_up.py,sha256=H9Xek6EoIbHrerQvGTRswXDNFH6zq71DcRxxj0zpo2g,77747
15
- fusesell_local/stages/initial_outreach.py,sha256=U6KHZIpr7d-W3x65z9XVm0gKL88SaVR47-_cQq3f-dc,117170
15
+ fusesell_local/stages/initial_outreach.py,sha256=0JJMRtfvy1F83Z5anLm8b0634q4JXoFi__IxginPURw,120643
16
16
  fusesell_local/stages/lead_scoring.py,sha256=ir3l849eMGrGLf0OYUcmA1F3FwyYhAplS4niU3R2GRY,60658
17
17
  fusesell_local/tests/conftest.py,sha256=TWUtlP6cNPVOYkTPz-j9BzS_KnXdPWy8D-ObPLHvXYs,366
18
18
  fusesell_local/tests/test_api.py,sha256=763rUVb5pAuAQOovug6Ka0T9eGK8-WVOC_J08M7TETo,1827
@@ -28,8 +28,8 @@ fusesell_local/utils/llm_client.py,sha256=FVc25UlGt6hro7h5Iw7PHSXY3E3_67Xc-SUbHu
28
28
  fusesell_local/utils/logger.py,sha256=sWlV8Tjyz_Z8J4zXKOnNalh8_iD6ytfrwPZpD-wcEOs,6259
29
29
  fusesell_local/utils/timezone_detector.py,sha256=0cAE4c8ZXqCA8AvxRKm6PrFKmAmsbq3HOHR6w-mW3KQ,39997
30
30
  fusesell_local/utils/validators.py,sha256=Z1VzeoxFsnuzlIA_ZaMWoy-0Cgyqupd47kIdljlMDbs,15438
31
- fusesell-1.2.7.dist-info/METADATA,sha256=uHVe8upgxtCHgruhMvflhXpBfUpi5DD2eeDR4g9JPU8,35074
32
- fusesell-1.2.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
- fusesell-1.2.7.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
34
- fusesell-1.2.7.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
35
- fusesell-1.2.7.dist-info/RECORD,,
31
+ fusesell-1.2.8.dist-info/METADATA,sha256=9r91NKwHYUIJ-FiYGr7JoNsWe5dDLMd0JO6Nf8MlXW0,35074
32
+ fusesell-1.2.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
+ fusesell-1.2.8.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
34
+ fusesell-1.2.8.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
35
+ fusesell-1.2.8.dist-info/RECORD,,
@@ -32,6 +32,6 @@ __all__ = [
32
32
  "validate_config",
33
33
  ]
34
34
 
35
- __version__ = "1.2.7"
35
+ __version__ = "1.2.8"
36
36
  __author__ = "FuseSell Team"
37
37
  __description__ = "Local implementation of FuseSell AI sales automation pipeline"
@@ -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 execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
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
- # Generate multiple email drafts
103
- email_drafts = self._generate_email_drafts(customer_data, recommended_product, scoring_data, context)
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
- company_info = customer_data.get('companyInfo', {})
635
- contact_info = customer_data.get('primaryContact', {})
636
- pain_points = customer_data.get('painPoints', [])
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 _extract_first_name(self, full_name: str) -> str:
1005
- if not full_name:
1006
- return ''
1007
- parts = full_name.strip().split()
1008
- return parts[-1] if parts else full_name
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'),