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.

Files changed (43) hide show
  1. {fusesell-1.2.7 → fusesell-1.2.8}/CHANGELOG.md +9 -0
  2. {fusesell-1.2.7/fusesell.egg-info → fusesell-1.2.8}/PKG-INFO +1 -1
  3. {fusesell-1.2.7 → fusesell-1.2.8/fusesell.egg-info}/PKG-INFO +1 -1
  4. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/__init__.py +1 -1
  5. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/initial_outreach.py +102 -29
  6. {fusesell-1.2.7 → fusesell-1.2.8}/pyproject.toml +1 -1
  7. {fusesell-1.2.7 → fusesell-1.2.8}/LICENSE +0 -0
  8. {fusesell-1.2.7 → fusesell-1.2.8}/MANIFEST.in +0 -0
  9. {fusesell-1.2.7 → fusesell-1.2.8}/README.md +0 -0
  10. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell.egg-info/SOURCES.txt +0 -0
  11. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell.egg-info/dependency_links.txt +0 -0
  12. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell.egg-info/entry_points.txt +0 -0
  13. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell.egg-info/requires.txt +0 -0
  14. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell.egg-info/top_level.txt +0 -0
  15. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell.py +0 -0
  16. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/api.py +0 -0
  17. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/cli.py +0 -0
  18. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/config/__init__.py +0 -0
  19. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/config/prompts.py +0 -0
  20. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/config/settings.py +0 -0
  21. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/pipeline.py +0 -0
  22. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/__init__.py +0 -0
  23. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/base_stage.py +0 -0
  24. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/data_acquisition.py +0 -0
  25. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/data_preparation.py +0 -0
  26. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/follow_up.py +0 -0
  27. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/stages/lead_scoring.py +0 -0
  28. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/tests/conftest.py +0 -0
  29. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/tests/test_api.py +0 -0
  30. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/tests/test_cli.py +0 -0
  31. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/tests/test_data_manager_products.py +0 -0
  32. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/tests/test_data_manager_sales_process.py +0 -0
  33. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/tests/test_data_manager_teams.py +0 -0
  34. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/__init__.py +0 -0
  35. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/birthday_email_manager.py +0 -0
  36. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/data_manager.py +0 -0
  37. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/event_scheduler.py +0 -0
  38. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/llm_client.py +0 -0
  39. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/logger.py +0 -0
  40. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/timezone_detector.py +0 -0
  41. {fusesell-1.2.7 → fusesell-1.2.8}/fusesell_local/utils/validators.py +0 -0
  42. {fusesell-1.2.7 → fusesell-1.2.8}/requirements.txt +0 -0
  43. {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
@@ -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
  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
@@ -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'),
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fusesell"
7
- version = "1.2.7"
7
+ version = "1.2.8"
8
8
  description = "Local implementation of FuseSell AI sales automation pipeline"
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes