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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusesell
3
- Version: 1.3.0
3
+ Version: 1.3.2
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.3.0.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
3
- fusesell_local/__init__.py,sha256=7shpYv9KVMmtCmwRUUrJOIid2cvvUuLpZwnbNNk4TS8,967
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=hvt0tpJQGDX5-k3pb2SR54IOnhQv2XyYXYtiVVECmts,131850
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.0.dist-info/METADATA,sha256=cBJe2l9s_vsCApaTmAtH_tOjdq9berqTED9CGOtSoEM,35074
32
- fusesell-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
- fusesell-1.3.0.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
34
- fusesell-1.3.0.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
35
- fusesell-1.3.0.dist-info/RECORD,,
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,,
@@ -32,6 +32,6 @@ __all__ = [
32
32
  "validate_config",
33
33
  ]
34
34
 
35
- __version__ = "1.3.0"
35
+ __version__ = "1.3.2"
36
36
  __author__ = "FuseSell Team"
37
37
  __description__ = "Local implementation of FuseSell AI sales automation pipeline"
@@ -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
- try:
714
- input_data = context.get('input_data', {})
715
- rep_profile = rep_profile or {}
716
- recipient_identity = self._resolve_recipient_identity(customer_data, context)
717
- if recipient_identity.get('first_name') and not context.get('customer_first_name'):
718
- context['customer_first_name'] = recipient_identity['first_name']
719
- context.setdefault('_recipient_identity', recipient_identity)
720
- if rep_profile:
721
- primary_name = rep_profile.get('name')
722
- if primary_name:
723
- input_data['staff_name'] = primary_name
724
- self.config['staff_name'] = primary_name
725
- if rep_profile.get('email'):
726
- input_data.setdefault('staff_email', rep_profile.get('email'))
727
- if rep_profile.get('phone') or rep_profile.get('primary_phone'):
728
- input_data.setdefault('staff_phone', rep_profile.get('phone') or rep_profile.get('primary_phone'))
729
- if rep_profile.get('position'):
730
- input_data.setdefault('staff_title', rep_profile.get('position'))
731
- if rep_profile.get('website'):
732
- input_data.setdefault('staff_website', rep_profile.get('website'))
733
-
734
- company_info = customer_data.get('companyInfo', {})
735
- contact_info = customer_data.get('primaryContact', {})
736
- pain_points = customer_data.get('painPoints', [])
737
-
738
- prompt_drafts = self._generate_email_drafts_from_prompt(
739
- customer_data,
740
- recommended_product,
741
- scoring_data,
742
- context
743
- )
744
- if prompt_drafts:
745
- return prompt_drafts
746
-
747
- # Generate multiple draft variations with different approaches
748
- draft_approaches = [
749
- {
750
- 'name': 'professional_direct',
751
- 'tone': 'professional and direct',
752
- 'focus': 'business value and ROI',
753
- 'length': 'concise'
754
- },
755
- {
756
- 'name': 'consultative',
757
- 'tone': 'consultative and helpful',
758
- 'focus': 'solving specific pain points',
759
- 'length': 'medium'
760
- },
761
- {
762
- 'name': 'industry_expert',
763
- 'tone': 'industry expert and insightful',
764
- 'focus': 'industry trends and challenges',
765
- 'length': 'detailed'
766
- },
767
- {
768
- 'name': 'relationship_building',
769
- 'tone': 'warm and relationship-focused',
770
- 'focus': 'building connection and trust',
771
- 'length': 'personal'
772
- }
773
- ]
774
-
775
- generated_drafts = []
776
-
777
- for approach in draft_approaches:
778
- try:
779
- # Generate email content for this approach
780
- email_content = self._generate_single_email_draft(
781
- customer_data, recommended_product, scoring_data,
782
- approach, context
783
- )
784
-
785
- # Generate subject lines for this approach
786
- subject_lines = self._generate_subject_lines(
787
- customer_data, recommended_product, approach, context
788
- )
789
-
790
- draft_id = f"uuid:{str(uuid.uuid4())}"
791
- draft_approach = approach['name']
792
- draft_type = "initial"
793
-
794
- # Select the best subject line (first one, or most relevant)
795
- selected_subject = subject_lines[0] if subject_lines else f"Partnership opportunity for {company_info.get('name', 'your company')}"
796
-
797
- draft = {
798
- 'draft_id': draft_id,
799
- 'approach': approach['name'],
800
- 'tone': approach['tone'],
801
- 'focus': approach['focus'],
802
- 'subject': selected_subject, # Single subject instead of array
803
- 'subject_alternatives': subject_lines[1:4] if len(subject_lines) > 1 else [], # Store alternatives separately
804
- 'email_body': email_content,
805
- 'email_format': 'html',
806
- 'recipient_email': recipient_identity.get('email'),
807
- 'recipient_name': recipient_identity.get('full_name'),
808
- 'customer_first_name': recipient_identity.get('first_name'),
809
- 'call_to_action': self._extract_call_to_action(email_content),
810
- 'personalization_score': self._calculate_personalization_score(email_content, customer_data),
811
- 'generated_at': datetime.now().isoformat(),
812
- 'status': 'draft',
813
- 'metadata': {
814
- 'customer_company': company_info.get('name', 'Unknown'),
815
- 'contact_name': contact_info.get('name', 'Unknown'),
816
- 'recipient_email': recipient_identity.get('email'),
817
- 'recipient_name': recipient_identity.get('full_name'),
818
- 'email_format': 'html',
819
- 'recommended_product': recommended_product.get('product_name', 'Unknown'),
820
- 'pain_points_addressed': len([p for p in pain_points if p.get('severity') in ['high', 'medium']]),
821
- 'generation_method': 'llm_powered'
822
- }
823
- }
824
-
825
- generated_drafts.append(draft)
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(f"Failed to generate single email draft: {str(e)}")
1430
- return self._generate_template_email(customer_data, recommended_product, approach, context)
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', {})