fusesell 1.3.0__py3-none-any.whl → 1.3.2__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.
- {fusesell-1.3.0.dist-info → fusesell-1.3.2.dist-info}/METADATA +1 -1
- {fusesell-1.3.0.dist-info → fusesell-1.3.2.dist-info}/RECORD +8 -8
- fusesell_local/__init__.py +1 -1
- fusesell_local/stages/initial_outreach.py +129 -194
- {fusesell-1.3.0.dist-info → fusesell-1.3.2.dist-info}/WHEEL +0 -0
- {fusesell-1.3.0.dist-info → fusesell-1.3.2.dist-info}/entry_points.txt +0 -0
- {fusesell-1.3.0.dist-info → fusesell-1.3.2.dist-info}/licenses/LICENSE +0 -0
- {fusesell-1.3.0.dist-info → fusesell-1.3.2.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
fusesell.py,sha256=t5PjkhWEJGINp4k517u0EX0ge7lzuHOUHHro-BE1mGk,596
|
|
2
|
-
fusesell-1.3.
|
|
3
|
-
fusesell_local/__init__.py,sha256=
|
|
2
|
+
fusesell-1.3.2.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
|
|
3
|
+
fusesell_local/__init__.py,sha256=tXr5H0DMQau3o2A4S5BUBYFUA9AGfHAmeZdHNIF78xA,967
|
|
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=
|
|
15
|
+
fusesell_local/stages/initial_outreach.py,sha256=98KMaGP_aFkCV4K8j8HgURmNEgbVTYZSvXfLOlXX3Mc,127216
|
|
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.3.
|
|
32
|
-
fusesell-1.3.
|
|
33
|
-
fusesell-1.3.
|
|
34
|
-
fusesell-1.3.
|
|
35
|
-
fusesell-1.3.
|
|
31
|
+
fusesell-1.3.2.dist-info/METADATA,sha256=GbUAPyqmrD5-9N3jaNnlAVQUNVG0AiRbNDvWOZDAaKo,35074
|
|
32
|
+
fusesell-1.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
fusesell-1.3.2.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
|
|
34
|
+
fusesell-1.3.2.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
|
|
35
|
+
fusesell-1.3.2.dist-info/RECORD,,
|
fusesell_local/__init__.py
CHANGED
|
@@ -710,135 +710,119 @@ class InitialOutreachStage(BaseStage):
|
|
|
710
710
|
if self.is_dry_run():
|
|
711
711
|
return self._get_mock_email_drafts(customer_data, recommended_product, context)
|
|
712
712
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
except Exception as e:
|
|
828
|
-
self.logger.warning(f"Failed to generate draft for approach {approach['name']}: {str(e)}")
|
|
829
|
-
continue
|
|
830
|
-
|
|
831
|
-
if not generated_drafts:
|
|
832
|
-
# Fallback to simple template if all LLM generations fail
|
|
833
|
-
self.logger.warning("All LLM draft generations failed, using fallback template")
|
|
834
|
-
return self._generate_fallback_draft(customer_data, recommended_product, context)
|
|
835
|
-
|
|
836
|
-
self.logger.info(f"Generated {len(generated_drafts)} email drafts successfully")
|
|
837
|
-
return generated_drafts
|
|
838
|
-
|
|
839
|
-
except Exception as e:
|
|
840
|
-
self.logger.error(f"Email draft generation failed: {str(e)}")
|
|
841
|
-
return self._generate_fallback_draft(customer_data, recommended_product, context)
|
|
713
|
+
input_data = context.get('input_data', {}) or {}
|
|
714
|
+
rep_profile = rep_profile or {}
|
|
715
|
+
recipient_identity = self._resolve_recipient_identity(customer_data, context)
|
|
716
|
+
if recipient_identity.get('first_name') and not context.get('customer_first_name'):
|
|
717
|
+
context['customer_first_name'] = recipient_identity['first_name']
|
|
718
|
+
context.setdefault('_recipient_identity', recipient_identity)
|
|
719
|
+
if rep_profile:
|
|
720
|
+
primary_name = rep_profile.get('name')
|
|
721
|
+
if primary_name:
|
|
722
|
+
input_data['staff_name'] = primary_name
|
|
723
|
+
self.config['staff_name'] = primary_name
|
|
724
|
+
if rep_profile.get('email'):
|
|
725
|
+
input_data.setdefault('staff_email', rep_profile.get('email'))
|
|
726
|
+
if rep_profile.get('phone') or rep_profile.get('primary_phone'):
|
|
727
|
+
input_data.setdefault('staff_phone', rep_profile.get('phone') or rep_profile.get('primary_phone'))
|
|
728
|
+
if rep_profile.get('position'):
|
|
729
|
+
input_data.setdefault('staff_title', rep_profile.get('position'))
|
|
730
|
+
if rep_profile.get('website'):
|
|
731
|
+
input_data.setdefault('staff_website', rep_profile.get('website'))
|
|
732
|
+
|
|
733
|
+
company_info = customer_data.get('companyInfo', {}) or {}
|
|
734
|
+
contact_info = customer_data.get('primaryContact', {}) or {}
|
|
735
|
+
pain_points = customer_data.get('painPoints', [])
|
|
736
|
+
|
|
737
|
+
prompt_drafts = self._generate_email_drafts_from_prompt(
|
|
738
|
+
customer_data,
|
|
739
|
+
recommended_product,
|
|
740
|
+
scoring_data,
|
|
741
|
+
context
|
|
742
|
+
)
|
|
743
|
+
if prompt_drafts:
|
|
744
|
+
return prompt_drafts
|
|
745
|
+
|
|
746
|
+
draft_approaches = [
|
|
747
|
+
{
|
|
748
|
+
'name': 'professional_direct',
|
|
749
|
+
'tone': 'professional and direct',
|
|
750
|
+
'focus': 'business value and ROI',
|
|
751
|
+
'length': 'concise'
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
'name': 'consultative',
|
|
755
|
+
'tone': 'consultative and helpful',
|
|
756
|
+
'focus': 'solving specific pain points',
|
|
757
|
+
'length': 'medium'
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
'name': 'industry_expert',
|
|
761
|
+
'tone': 'industry expert and insightful',
|
|
762
|
+
'focus': 'industry trends and challenges',
|
|
763
|
+
'length': 'detailed'
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
'name': 'relationship_building',
|
|
767
|
+
'tone': 'warm and relationship-focused',
|
|
768
|
+
'focus': 'building connection and trust',
|
|
769
|
+
'length': 'personal'
|
|
770
|
+
}
|
|
771
|
+
]
|
|
772
|
+
|
|
773
|
+
generated_drafts: List[Dict[str, Any]] = []
|
|
774
|
+
|
|
775
|
+
for approach in draft_approaches:
|
|
776
|
+
email_body = self._generate_single_email_draft(
|
|
777
|
+
customer_data,
|
|
778
|
+
recommended_product,
|
|
779
|
+
scoring_data,
|
|
780
|
+
approach,
|
|
781
|
+
context
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
subject_lines = self._generate_subject_lines(
|
|
785
|
+
customer_data, recommended_product, approach, context
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
draft_id = f"uuid:{str(uuid.uuid4())}"
|
|
789
|
+
selected_subject = subject_lines[0] if subject_lines else f"Partnership opportunity for {company_info.get('name', 'your company')}"
|
|
790
|
+
|
|
791
|
+
draft = {
|
|
792
|
+
'draft_id': draft_id,
|
|
793
|
+
'approach': approach['name'],
|
|
794
|
+
'tone': approach['tone'],
|
|
795
|
+
'focus': approach['focus'],
|
|
796
|
+
'subject': selected_subject,
|
|
797
|
+
'subject_alternatives': subject_lines[1:4] if len(subject_lines) > 1 else [],
|
|
798
|
+
'email_body': email_body,
|
|
799
|
+
'email_format': 'html',
|
|
800
|
+
'recipient_email': recipient_identity.get('email'),
|
|
801
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
802
|
+
'customer_first_name': recipient_identity.get('first_name'),
|
|
803
|
+
'call_to_action': self._extract_call_to_action(email_body),
|
|
804
|
+
'personalization_score': self._calculate_personalization_score(email_body, customer_data),
|
|
805
|
+
'generated_at': datetime.now().isoformat(),
|
|
806
|
+
'status': 'draft',
|
|
807
|
+
'metadata': {
|
|
808
|
+
'customer_company': company_info.get('name', 'Unknown'),
|
|
809
|
+
'contact_name': contact_info.get('name', 'Unknown'),
|
|
810
|
+
'recipient_email': recipient_identity.get('email'),
|
|
811
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
812
|
+
'email_format': 'html',
|
|
813
|
+
'recommended_product': recommended_product.get('product_name', 'Unknown') if recommended_product else 'Unknown',
|
|
814
|
+
'pain_points_addressed': len([p for p in pain_points if p.get('severity') in ['high', 'medium']]),
|
|
815
|
+
'generation_method': 'llm_powered'
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
generated_drafts.append(draft)
|
|
820
|
+
|
|
821
|
+
if not generated_drafts:
|
|
822
|
+
raise RuntimeError("LLM returned no outreach drafts; initial outreach cannot proceed.")
|
|
823
|
+
|
|
824
|
+
self.logger.info("Generated %s email drafts successfully", len(generated_drafts))
|
|
825
|
+
return generated_drafts
|
|
842
826
|
|
|
843
827
|
def _generate_email_drafts_from_prompt(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any], scoring_data: Dict[str, Any], context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
844
828
|
"""Attempt to generate drafts using configured prompt template."""
|
|
@@ -1425,9 +1409,9 @@ class InitialOutreachStage(BaseStage):
|
|
|
1425
1409
|
|
|
1426
1410
|
return cleaned_content
|
|
1427
1411
|
|
|
1428
|
-
except Exception as e:
|
|
1429
|
-
self.logger.error(
|
|
1430
|
-
|
|
1412
|
+
except Exception as e:
|
|
1413
|
+
self.logger.error("LLM single draft generation failed for approach %s: %s", approach.get('name'), e)
|
|
1414
|
+
raise RuntimeError(f"Failed to generate draft for approach {approach.get('name')}") from e
|
|
1431
1415
|
|
|
1432
1416
|
def _create_email_generation_prompt(self, customer_context: Dict[str, Any], approach: Dict[str, Any]) -> str:
|
|
1433
1417
|
"""Create LLM prompt for email generation."""
|
|
@@ -1587,6 +1571,19 @@ Generate 4 subject lines, one per line, no numbering or bullets:"""
|
|
|
1587
1571
|
sanitized = f"<html><body>{sanitized}</body></html>"
|
|
1588
1572
|
|
|
1589
1573
|
return sanitized
|
|
1574
|
+
|
|
1575
|
+
def _ensure_html_email(self, raw_content: Any, context: Dict[str, Any]) -> str:
|
|
1576
|
+
"""
|
|
1577
|
+
Normalize potentially plain-text content into HTML output.
|
|
1578
|
+
"""
|
|
1579
|
+
if raw_content is None:
|
|
1580
|
+
return "<html><body></body></html>"
|
|
1581
|
+
|
|
1582
|
+
text = str(raw_content)
|
|
1583
|
+
if '<html' in text.lower():
|
|
1584
|
+
return text
|
|
1585
|
+
|
|
1586
|
+
return self._clean_email_content(text, context)
|
|
1590
1587
|
|
|
1591
1588
|
def _extract_call_to_action(self, email_content: str) -> str:
|
|
1592
1589
|
"""Extract the main call-to-action from email content."""
|
|
@@ -1657,68 +1654,6 @@ Generate 4 subject lines, one per line, no numbering or bullets:"""
|
|
|
1657
1654
|
|
|
1658
1655
|
return min(score, 100)
|
|
1659
1656
|
|
|
1660
|
-
def _generate_template_email(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any],
|
|
1661
|
-
approach: Dict[str, Any], context: Dict[str, Any]) -> str:
|
|
1662
|
-
"""Generate email using template as fallback."""
|
|
1663
|
-
input_data = context.get('input_data', {})
|
|
1664
|
-
company_info = customer_data.get('companyInfo', {})
|
|
1665
|
-
contact_info = customer_data.get('primaryContact', {})
|
|
1666
|
-
|
|
1667
|
-
return f"""Dear {contact_info.get('name', 'there')},
|
|
1668
|
-
|
|
1669
|
-
I hope this email finds you well. I'm reaching out from {input_data.get('org_name', 'our company')} regarding a potential opportunity for {company_info.get('name', 'your company')}.
|
|
1670
|
-
|
|
1671
|
-
Based on our research of companies in the {company_info.get('industry', 'technology')} sector, I believe {company_info.get('name', 'your company')} could benefit from our {recommended_product.get('product_name', 'solution')}.
|
|
1672
|
-
|
|
1673
|
-
We've helped similar organizations achieve significant improvements in their operations. Would you be interested in a brief 15-minute call to discuss how we might be able to help {company_info.get('name', 'your company')} achieve its goals?
|
|
1674
|
-
|
|
1675
|
-
Best regards,
|
|
1676
|
-
{input_data.get('staff_name', 'Sales Team')}
|
|
1677
|
-
{input_data.get('org_name', 'Our Company')}"""
|
|
1678
|
-
|
|
1679
|
-
def _generate_fallback_draft(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any], context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
1680
|
-
"""Generate fallback draft when LLM generation fails."""
|
|
1681
|
-
draft_id = f"uuid:{str(uuid.uuid4())}"
|
|
1682
|
-
recipient_identity = self._resolve_recipient_identity(customer_data, context)
|
|
1683
|
-
context.setdefault('_recipient_identity', recipient_identity)
|
|
1684
|
-
if recipient_identity.get('first_name') and not context.get('customer_first_name'):
|
|
1685
|
-
context['customer_first_name'] = recipient_identity['first_name']
|
|
1686
|
-
|
|
1687
|
-
template_body = self._generate_template_email(
|
|
1688
|
-
customer_data,
|
|
1689
|
-
recommended_product,
|
|
1690
|
-
{'tone': 'professional'},
|
|
1691
|
-
context
|
|
1692
|
-
)
|
|
1693
|
-
email_body = self._clean_email_content(template_body, context)
|
|
1694
|
-
|
|
1695
|
-
fallback_subjects = self._generate_fallback_subject_lines(customer_data, recommended_product)
|
|
1696
|
-
|
|
1697
|
-
return [{
|
|
1698
|
-
'draft_id': draft_id,
|
|
1699
|
-
'approach': 'fallback_template',
|
|
1700
|
-
'tone': 'professional',
|
|
1701
|
-
'focus': 'general outreach',
|
|
1702
|
-
'subject': fallback_subjects[0],
|
|
1703
|
-
'subject_alternatives': fallback_subjects[1:],
|
|
1704
|
-
'email_body': email_body,
|
|
1705
|
-
'email_format': 'html',
|
|
1706
|
-
'recipient_email': recipient_identity.get('email'),
|
|
1707
|
-
'recipient_name': recipient_identity.get('full_name'),
|
|
1708
|
-
'customer_first_name': recipient_identity.get('first_name'),
|
|
1709
|
-
'call_to_action': 'Would you be interested in a brief call?',
|
|
1710
|
-
'personalization_score': 50,
|
|
1711
|
-
'generated_at': datetime.now().isoformat(),
|
|
1712
|
-
'status': 'draft',
|
|
1713
|
-
'metadata': {
|
|
1714
|
-
'generation_method': 'template_fallback',
|
|
1715
|
-
'note': 'Generated using template due to LLM failure',
|
|
1716
|
-
'recipient_email': recipient_identity.get('email'),
|
|
1717
|
-
'recipient_name': recipient_identity.get('full_name'),
|
|
1718
|
-
'email_format': 'html'
|
|
1719
|
-
}
|
|
1720
|
-
}]
|
|
1721
|
-
|
|
1722
1657
|
def _get_mock_email_drafts(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any], context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
1723
1658
|
"""Get mock email drafts for dry run."""
|
|
1724
1659
|
input_data = context.get('input_data', {})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|