fusesell 1.3.0__tar.gz → 1.3.1__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.3.0 → fusesell-1.3.1}/CHANGELOG.md +9 -0
- {fusesell-1.3.0/fusesell.egg-info → fusesell-1.3.1}/PKG-INFO +1 -1
- {fusesell-1.3.0 → fusesell-1.3.1/fusesell.egg-info}/PKG-INFO +1 -1
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/__init__.py +1 -1
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/stages/initial_outreach.py +88 -24
- {fusesell-1.3.0 → fusesell-1.3.1}/pyproject.toml +1 -1
- {fusesell-1.3.0 → fusesell-1.3.1}/LICENSE +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/MANIFEST.in +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/README.md +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell.egg-info/SOURCES.txt +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell.egg-info/dependency_links.txt +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell.egg-info/entry_points.txt +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell.egg-info/requires.txt +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell.egg-info/top_level.txt +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/api.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/cli.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/config/__init__.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/config/prompts.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/config/settings.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/pipeline.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/stages/__init__.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/stages/base_stage.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/stages/data_acquisition.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/stages/data_preparation.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/stages/follow_up.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/stages/lead_scoring.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/tests/conftest.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/tests/test_api.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/tests/test_cli.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/tests/test_data_manager_products.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/tests/test_data_manager_sales_process.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/tests/test_data_manager_teams.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/utils/__init__.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/utils/birthday_email_manager.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/utils/data_manager.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/utils/event_scheduler.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/utils/llm_client.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/utils/logger.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/utils/timezone_detector.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/fusesell_local/utils/validators.py +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/requirements.txt +0 -0
- {fusesell-1.3.0 → fusesell-1.3.1}/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.3.1] - 2025-10-24
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Fallback draft generation now produces approach-specific HTML emails, ensuring usable output even when the LLM call fails.
|
|
9
|
+
- Deterministic template emails and mock drafts reuse the resolved recipient data and signatures so every record stays HTML-compliant.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Normalized draft creation to wrap plain-text results in <html><body> and removed duplicate drafts that previously occurred when the template path was used.
|
|
13
|
+
|
|
5
14
|
# [1.3.0] - 2025-10-24
|
|
6
15
|
|
|
7
16
|
### Added
|
|
@@ -776,12 +776,13 @@ class InitialOutreachStage(BaseStage):
|
|
|
776
776
|
|
|
777
777
|
for approach in draft_approaches:
|
|
778
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
|
-
|
|
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
|
+
email_content = self._ensure_html_email(email_content, context)
|
|
785
|
+
|
|
785
786
|
# Generate subject lines for this approach
|
|
786
787
|
subject_lines = self._generate_subject_lines(
|
|
787
788
|
customer_data, recommended_product, approach, context
|
|
@@ -1587,6 +1588,19 @@ Generate 4 subject lines, one per line, no numbering or bullets:"""
|
|
|
1587
1588
|
sanitized = f"<html><body>{sanitized}</body></html>"
|
|
1588
1589
|
|
|
1589
1590
|
return sanitized
|
|
1591
|
+
|
|
1592
|
+
def _ensure_html_email(self, raw_content: Any, context: Dict[str, Any]) -> str:
|
|
1593
|
+
"""
|
|
1594
|
+
Normalize potentially plain-text content into HTML output.
|
|
1595
|
+
"""
|
|
1596
|
+
if raw_content is None:
|
|
1597
|
+
return "<html><body></body></html>"
|
|
1598
|
+
|
|
1599
|
+
text = str(raw_content)
|
|
1600
|
+
if '<html' in text.lower():
|
|
1601
|
+
return text
|
|
1602
|
+
|
|
1603
|
+
return self._clean_email_content(text, context)
|
|
1590
1604
|
|
|
1591
1605
|
def _extract_call_to_action(self, email_content: str) -> str:
|
|
1592
1606
|
"""Extract the main call-to-action from email content."""
|
|
@@ -1657,24 +1671,74 @@ Generate 4 subject lines, one per line, no numbering or bullets:"""
|
|
|
1657
1671
|
|
|
1658
1672
|
return min(score, 100)
|
|
1659
1673
|
|
|
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
|
|
1663
|
-
input_data = context.get('input_data', {})
|
|
1664
|
-
company_info = customer_data.get('companyInfo', {})
|
|
1665
|
-
contact_info = customer_data.get('primaryContact', {})
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1674
|
+
def _generate_template_email(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any],
|
|
1675
|
+
approach: Dict[str, Any], context: Dict[str, Any]) -> str:
|
|
1676
|
+
"""Generate a deterministic HTML email when LLM generation is unavailable."""
|
|
1677
|
+
input_data = context.get('input_data', {}) or {}
|
|
1678
|
+
company_info = customer_data.get('companyInfo', {}) or {}
|
|
1679
|
+
contact_info = customer_data.get('primaryContact', {}) or {}
|
|
1680
|
+
identity = context.get('_recipient_identity') or self._resolve_recipient_identity(customer_data, context)
|
|
1681
|
+
|
|
1682
|
+
first_name = identity.get('first_name') or contact_info.get('name') or input_data.get('customer_name') or input_data.get('recipient_name') or 'there'
|
|
1683
|
+
first_name = self._extract_first_name(first_name) if isinstance(first_name, str) else 'there'
|
|
1684
|
+
|
|
1685
|
+
staff_name = input_data.get('staff_name') or self.config.get('staff_name', 'Sales Team')
|
|
1686
|
+
org_name = input_data.get('org_name') or self.config.get('org_name', 'FuseSell')
|
|
1687
|
+
company_name = company_info.get('name', 'your company')
|
|
1688
|
+
industry = company_info.get('industry', 'your industry')
|
|
1689
|
+
approach_name = approach.get('name', 'professional_direct')
|
|
1690
|
+
approach_focus = approach.get('focus', 'business value')
|
|
1691
|
+
approach_tone = approach.get('tone', 'professional')
|
|
1692
|
+
|
|
1693
|
+
benefits: List[str] = []
|
|
1694
|
+
if recommended_product:
|
|
1695
|
+
product_name = recommended_product.get('product_name')
|
|
1696
|
+
benefits = [b for b in (recommended_product.get('key_benefits') or []) if b]
|
|
1697
|
+
if not benefits and product_name:
|
|
1698
|
+
benefits = [
|
|
1699
|
+
f"{product_name} accelerates {company_name}'s {approach_focus} goals",
|
|
1700
|
+
f"Designed specifically for {industry} operators",
|
|
1701
|
+
"Rapid onboarding with dedicated local support"
|
|
1702
|
+
]
|
|
1703
|
+
if not benefits:
|
|
1704
|
+
benefits = [
|
|
1705
|
+
f"Measurable improvements in {approach_focus}",
|
|
1706
|
+
f"Playbooks tailored for {industry} teams",
|
|
1707
|
+
"Guided adoption with FuseSell specialists"
|
|
1708
|
+
]
|
|
1709
|
+
|
|
1710
|
+
bullet_html = ''.join(f"<li>{benefit}</li>" for benefit in benefits)
|
|
1711
|
+
|
|
1712
|
+
cta_map = {
|
|
1713
|
+
'professional_direct': f"Would you have 20 minutes this week to explore how FuseSell can lighten {company_name}'s {approach_focus} workload?",
|
|
1714
|
+
'consultative': f"Could we schedule a short working session to dig into your current {approach_focus} priorities?",
|
|
1715
|
+
'industry_expert': f"Shall we review the latest {industry} benchmarks together and map them to your roadmap?",
|
|
1716
|
+
'relationship_building': f"I'd love to hear how your team is approaching {approach_focus}; is a quick virtual coffee an option?"
|
|
1717
|
+
}
|
|
1718
|
+
cta_text = cta_map.get(approach_name, f"Would you be open to a brief call to discuss {approach_focus} priorities at {company_name}?")
|
|
1719
|
+
|
|
1720
|
+
product_sentence = ""
|
|
1721
|
+
if recommended_product and recommended_product.get('product_name'):
|
|
1722
|
+
product_sentence = f"<p>We engineered <strong>{recommended_product['product_name']}</strong> specifically for teams tackling {approach_focus} in {industry}. It's a natural fit for {company_name}'s next phase.</p>"
|
|
1723
|
+
|
|
1724
|
+
news = company_info.get('recentNews')
|
|
1725
|
+
intro_sentence = f"<p>I'm reaching out because leaders at {company_name} are raising the same questions we hear from other {industry} innovators: how to keep {approach_focus} moving without burning out the team.</p>"
|
|
1726
|
+
if news:
|
|
1727
|
+
intro_sentence = f"<p>I noticed the recent update about {news}. Many {industry} peers use FuseSell to capitalise on moments exactly like this.</p>"
|
|
1728
|
+
|
|
1729
|
+
html = (
|
|
1730
|
+
"<html><body>"
|
|
1731
|
+
f"<p>Hi {first_name},</p>"
|
|
1732
|
+
f"{intro_sentence}"
|
|
1733
|
+
f"<p>From our {approach_tone.lower()} conversations with {industry} operators, three ideas could help {company_name} right away:</p>"
|
|
1734
|
+
f"<ul>{bullet_html}</ul>"
|
|
1735
|
+
f"{product_sentence}"
|
|
1736
|
+
f"<p>{cta_text}</p>"
|
|
1737
|
+
f"<p>Best regards,<br>{staff_name}<br>{org_name}</p>"
|
|
1738
|
+
"</body></html>"
|
|
1739
|
+
)
|
|
1740
|
+
|
|
1741
|
+
return html
|
|
1678
1742
|
|
|
1679
1743
|
def _generate_fallback_draft(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any], context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
1680
1744
|
"""Generate fallback draft when LLM generation fails."""
|
|
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
|