fusesell 1.2.6__tar.gz → 1.2.7__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.2.6 → fusesell-1.2.7}/CHANGELOG.md +10 -0
- {fusesell-1.2.6/fusesell.egg-info → fusesell-1.2.7}/PKG-INFO +1 -1
- {fusesell-1.2.6 → fusesell-1.2.7/fusesell.egg-info}/PKG-INFO +1 -1
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/__init__.py +1 -1
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/pipeline.py +23 -19
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/initial_outreach.py +108 -54
- {fusesell-1.2.6 → fusesell-1.2.7}/pyproject.toml +1 -1
- {fusesell-1.2.6 → fusesell-1.2.7}/LICENSE +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/MANIFEST.in +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/README.md +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell.egg-info/SOURCES.txt +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell.egg-info/dependency_links.txt +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell.egg-info/entry_points.txt +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell.egg-info/requires.txt +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell.egg-info/top_level.txt +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/api.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/cli.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/config/__init__.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/config/prompts.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/config/settings.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/__init__.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/base_stage.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/data_acquisition.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/data_preparation.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/follow_up.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/lead_scoring.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/tests/conftest.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/tests/test_api.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/tests/test_cli.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/tests/test_data_manager_products.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/tests/test_data_manager_sales_process.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/tests/test_data_manager_teams.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/__init__.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/birthday_email_manager.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/data_manager.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/event_scheduler.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/llm_client.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/logger.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/timezone_detector.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/validators.py +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/requirements.txt +0 -0
- {fusesell-1.2.6 → fusesell-1.2.7}/setup.cfg +0 -0
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to FuseSell Local will be documented in this file.
|
|
4
4
|
|
|
5
|
+
# [1.2.7] - 2025-10-24
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- RealTimeX sales-process normalization now forwards `recipient_address`, `recipient_name`, and `customer_email` into the FuseSell pipeline so prompt generation and scheduling have complete context.
|
|
9
|
+
- Default outreach prompt replacements enrich customer metadata with toolkit-derived contact details and enforce first-name greetings to match server quality.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Initial outreach stage now records generated drafts in the pipeline summary, seeds reminder_task rows with toolkit credentials, and schedules follow-up events when `send_immediately` is false.
|
|
13
|
+
- Prompt-based draft generation no longer skips scheduling due to missing email fields and guarantees outputs without unresolved placeholders.
|
|
14
|
+
|
|
5
15
|
# [1.2.6] - 2025-10-24
|
|
6
16
|
|
|
7
17
|
### Added
|
|
@@ -377,15 +377,16 @@ class FuseSellPipeline:
|
|
|
377
377
|
return {
|
|
378
378
|
'org_id': self.config['org_id'],
|
|
379
379
|
'org_name': self.config['org_name'],
|
|
380
|
-
'team_id': self.config.get('team_id'),
|
|
381
|
-
'team_name': self.config.get('team_name'),
|
|
382
|
-
'project_code': self.config.get('project_code'),
|
|
383
|
-
'staff_name': self.config.get('staff_name', 'Sales Team'),
|
|
384
|
-
'
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
'
|
|
388
|
-
'
|
|
380
|
+
'team_id': self.config.get('team_id'),
|
|
381
|
+
'team_name': self.config.get('team_name'),
|
|
382
|
+
'project_code': self.config.get('project_code'),
|
|
383
|
+
'staff_name': self.config.get('staff_name', 'Sales Team'),
|
|
384
|
+
'customer_name': self.config.get('customer_name', ''),
|
|
385
|
+
'language': self.config.get('language', 'english'),
|
|
386
|
+
# Data sources (matching executor schema)
|
|
387
|
+
'input_website': self.config.get('input_website', ''),
|
|
388
|
+
'input_description': self.config.get('input_description', ''),
|
|
389
|
+
'input_business_card': self.config.get('input_business_card', ''),
|
|
389
390
|
'input_linkedin_url': self.config.get('input_linkedin_url', ''),
|
|
390
391
|
'input_facebook_url': self.config.get('input_facebook_url', ''),
|
|
391
392
|
'input_freetext': self.config.get('input_freetext', ''),
|
|
@@ -396,10 +397,11 @@ class FuseSellPipeline:
|
|
|
396
397
|
|
|
397
398
|
# Action and continuation fields (for server executor compatibility)
|
|
398
399
|
'action': self.config.get('action', 'draft_write'),
|
|
399
|
-
'selected_draft_id': self.config.get('selected_draft_id', ''),
|
|
400
|
-
'reason': self.config.get('reason', ''),
|
|
400
|
+
'selected_draft_id': self.config.get('selected_draft_id', ''),
|
|
401
|
+
'reason': self.config.get('reason', ''),
|
|
401
402
|
'recipient_address': self.config.get('recipient_address', ''),
|
|
402
403
|
'recipient_name': self.config.get('recipient_name', ''),
|
|
404
|
+
'customer_email': self.config.get('customer_email', ''),
|
|
403
405
|
'interaction_type': self.config.get('interaction_type', 'email'),
|
|
404
406
|
'human_action_id': self.config.get('human_action_id', ''),
|
|
405
407
|
|
|
@@ -600,14 +602,16 @@ class FuseSellPipeline:
|
|
|
600
602
|
if result.get('status') == 'success':
|
|
601
603
|
data = result.get('data', {})
|
|
602
604
|
|
|
603
|
-
if stage_name == 'data_preparation':
|
|
604
|
-
customer_data = data
|
|
605
|
-
elif stage_name == 'lead_scoring':
|
|
606
|
-
lead_scores = data.get('scores', [])
|
|
607
|
-
elif stage_name == 'initial_outreach':
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
605
|
+
if stage_name == 'data_preparation':
|
|
606
|
+
customer_data = data
|
|
607
|
+
elif stage_name == 'lead_scoring':
|
|
608
|
+
lead_scores = data.get('scores', [])
|
|
609
|
+
elif stage_name == 'initial_outreach':
|
|
610
|
+
drafts = data.get('drafts') or data.get('email_drafts') or []
|
|
611
|
+
email_drafts.extend(drafts)
|
|
612
|
+
elif stage_name == 'follow_up':
|
|
613
|
+
drafts = data.get('drafts') or data.get('email_drafts') or []
|
|
614
|
+
email_drafts.extend(drafts)
|
|
611
615
|
|
|
612
616
|
# Generate performance analytics
|
|
613
617
|
performance_analytics = self._generate_performance_analytics(duration)
|
|
@@ -115,10 +115,11 @@ class InitialOutreachStage(BaseStage):
|
|
|
115
115
|
outreach_data = {
|
|
116
116
|
'action': 'draft_write',
|
|
117
117
|
'status': 'drafts_generated',
|
|
118
|
-
'email_drafts': saved_drafts,
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
118
|
+
'email_drafts': saved_drafts,
|
|
119
|
+
'drafts': saved_drafts,
|
|
120
|
+
'recommended_product': recommended_product,
|
|
121
|
+
'customer_summary': self._create_customer_summary(customer_data),
|
|
122
|
+
'total_drafts_generated': len(saved_drafts),
|
|
122
123
|
'generation_timestamp': datetime.now().isoformat(),
|
|
123
124
|
'customer_id': context.get('execution_id')
|
|
124
125
|
}
|
|
@@ -327,8 +328,8 @@ class InitialOutreachStage(BaseStage):
|
|
|
327
328
|
team_id = input_data.get('team_id')
|
|
328
329
|
team_name = input_data.get('team_name')
|
|
329
330
|
language = input_data.get('language')
|
|
330
|
-
customer_name = input_data.get('customer_name')
|
|
331
|
-
staff_name = input_data.get('staff_name')
|
|
331
|
+
customer_name = input_data.get('customer_name') or input_data.get('recipient_name')
|
|
332
|
+
staff_name = input_data.get('staff_name') or self.config.get('staff_name') or 'Sales Team'
|
|
332
333
|
reminder_room = self.config.get('reminder_room_id') or input_data.get('reminder_room_id')
|
|
333
334
|
draft_id = draft.get('draft_id') or 'unknown_draft'
|
|
334
335
|
|
|
@@ -355,6 +356,8 @@ class InitialOutreachStage(BaseStage):
|
|
|
355
356
|
customextra['approach'] = draft.get('approach')
|
|
356
357
|
if draft.get('mail_tone'):
|
|
357
358
|
customextra['mail_tone'] = draft.get('mail_tone')
|
|
359
|
+
if recipient_address and 'customer_email' not in customextra:
|
|
360
|
+
customextra['customer_email'] = recipient_address
|
|
358
361
|
|
|
359
362
|
return {
|
|
360
363
|
'status': 'published',
|
|
@@ -368,6 +371,7 @@ class InitialOutreachStage(BaseStage):
|
|
|
368
371
|
'team_name': team_name,
|
|
369
372
|
'language': language,
|
|
370
373
|
'customer_name': customer_name,
|
|
374
|
+
'customer_email': recipient_address,
|
|
371
375
|
'staff_name': staff_name,
|
|
372
376
|
'customextra': customextra
|
|
373
377
|
}
|
|
@@ -394,11 +398,18 @@ class InitialOutreachStage(BaseStage):
|
|
|
394
398
|
return None
|
|
395
399
|
|
|
396
400
|
contact_info = customer_data.get('primaryContact', {}) or {}
|
|
401
|
+
stage_results = context.get('stage_results', {}) or {}
|
|
402
|
+
data_acquisition = {}
|
|
403
|
+
if isinstance(stage_results, dict):
|
|
404
|
+
data_acquisition = stage_results.get('data_acquisition', {}).get('data', {}) or {}
|
|
397
405
|
|
|
398
406
|
recipient_address = (
|
|
399
407
|
input_data.get('recipient_address')
|
|
400
408
|
or contact_info.get('email')
|
|
401
409
|
or contact_info.get('emailAddress')
|
|
410
|
+
or data_acquisition.get('customer_email')
|
|
411
|
+
or data_acquisition.get('contact_email')
|
|
412
|
+
or input_data.get('customer_email')
|
|
402
413
|
)
|
|
403
414
|
if not recipient_address:
|
|
404
415
|
self.logger.info("Skipping reminder scheduling: recipient email not available")
|
|
@@ -408,6 +419,8 @@ class InitialOutreachStage(BaseStage):
|
|
|
408
419
|
input_data.get('recipient_name')
|
|
409
420
|
or contact_info.get('name')
|
|
410
421
|
or contact_info.get('fullName')
|
|
422
|
+
or data_acquisition.get('contact_name')
|
|
423
|
+
or data_acquisition.get('customer_name')
|
|
411
424
|
or ''
|
|
412
425
|
)
|
|
413
426
|
|
|
@@ -819,46 +832,80 @@ class InitialOutreachStage(BaseStage):
|
|
|
819
832
|
|
|
820
833
|
def _build_prompt_replacements(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any], scoring_data: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, str]:
|
|
821
834
|
input_data = context.get('input_data', {})
|
|
822
|
-
company_info = customer_data.get('companyInfo', {}) or {}
|
|
823
|
-
contact_info = customer_data.get('primaryContact', {}) or {}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
+
company_info = customer_data.get('companyInfo', {}) or {}
|
|
836
|
+
contact_info = dict(customer_data.get('primaryContact', {}) or {})
|
|
837
|
+
stage_results = context.get('stage_results', {}) or {}
|
|
838
|
+
data_acquisition = {}
|
|
839
|
+
if isinstance(stage_results, dict):
|
|
840
|
+
data_acquisition = stage_results.get('data_acquisition', {}).get('data', {}) or {}
|
|
841
|
+
if not contact_info.get('name'):
|
|
842
|
+
fallback_contact_name = (
|
|
843
|
+
data_acquisition.get('contact_name')
|
|
844
|
+
or data_acquisition.get('customer_contact')
|
|
845
|
+
or input_data.get('recipient_name')
|
|
846
|
+
or input_data.get('customer_name')
|
|
847
|
+
)
|
|
848
|
+
if fallback_contact_name:
|
|
849
|
+
contact_info['name'] = fallback_contact_name
|
|
850
|
+
if not contact_info.get('email'):
|
|
851
|
+
fallback_email = (
|
|
852
|
+
data_acquisition.get('customer_email')
|
|
853
|
+
or data_acquisition.get('contact_email')
|
|
854
|
+
or input_data.get('recipient_address')
|
|
855
|
+
or input_data.get('customer_email')
|
|
856
|
+
)
|
|
857
|
+
if fallback_email:
|
|
858
|
+
contact_info['email'] = fallback_email
|
|
859
|
+
customer_data = dict(customer_data)
|
|
860
|
+
customer_data['primaryContact'] = contact_info
|
|
861
|
+
language = input_data.get('language') or company_info.get('language') or 'English'
|
|
862
|
+
contact_name = contact_info.get('name') or input_data.get('customer_name') or input_data.get('recipient_name') or 'there'
|
|
863
|
+
company_name = company_info.get('name') or input_data.get('company_name') or 'the company'
|
|
864
|
+
staff_name = input_data.get('staff_name') or input_data.get('sender_name') or 'Sales Team'
|
|
865
|
+
org_name = input_data.get('org_name') or 'Our Company'
|
|
866
|
+
selected_product_name = recommended_product.get('product_name') if recommended_product else None
|
|
867
|
+
language_lower = language.lower() if isinstance(language, str) else ''
|
|
868
|
+
name_parts = contact_name.split() if isinstance(contact_name, str) else []
|
|
869
|
+
if name_parts:
|
|
870
|
+
if language_lower in ('vietnamese', 'vi'):
|
|
871
|
+
first_name = name_parts[-1]
|
|
872
|
+
else:
|
|
873
|
+
first_name = name_parts[0]
|
|
874
|
+
else:
|
|
875
|
+
first_name = contact_name or ''
|
|
876
|
+
|
|
877
|
+
action = input_data.get('action', 'draft_write')
|
|
878
|
+
action_labels = {
|
|
879
|
+
'draft_write': 'email drafts',
|
|
880
|
+
'draft_rewrite': 'email rewrites',
|
|
835
881
|
'send': 'email sends',
|
|
836
882
|
'close': 'email workflow'
|
|
837
883
|
}
|
|
838
884
|
action_type = action_labels.get(action, action.replace('_', ' '))
|
|
839
885
|
|
|
840
|
-
company_summary = self._build_company_info_summary(
|
|
841
|
-
company_info,
|
|
842
|
-
contact_info,
|
|
843
|
-
customer_data.get('painPoints', []),
|
|
844
|
-
scoring_data
|
|
845
|
-
)
|
|
846
|
-
product_summary = self._build_product_info_summary(recommended_product)
|
|
847
|
-
first_name_guide = self._build_first_name_guide(language, contact_name)
|
|
848
|
-
|
|
849
|
-
replacements = {
|
|
850
|
-
'##action_type##': action_type,
|
|
851
|
-
'##language##': language.title() if isinstance(language, str) else 'English',
|
|
852
|
-
'##customer_name##': contact_name,
|
|
886
|
+
company_summary = self._build_company_info_summary(
|
|
887
|
+
company_info,
|
|
888
|
+
contact_info,
|
|
889
|
+
customer_data.get('painPoints', []),
|
|
890
|
+
scoring_data
|
|
891
|
+
)
|
|
892
|
+
product_summary = self._build_product_info_summary(recommended_product)
|
|
893
|
+
first_name_guide = self._build_first_name_guide(language, contact_name)
|
|
894
|
+
|
|
895
|
+
replacements = {
|
|
896
|
+
'##action_type##': action_type,
|
|
897
|
+
'##language##': language.title() if isinstance(language, str) else 'English',
|
|
898
|
+
'##customer_name##': contact_name,
|
|
853
899
|
'##company_name##': company_name,
|
|
854
|
-
'##staff_name##': staff_name,
|
|
855
|
-
'##org_name##': org_name,
|
|
856
|
-
'##first_name_guide##': first_name_guide,
|
|
857
|
-
'##
|
|
858
|
-
'##
|
|
859
|
-
'##
|
|
860
|
-
|
|
861
|
-
|
|
900
|
+
'##staff_name##': staff_name,
|
|
901
|
+
'##org_name##': org_name,
|
|
902
|
+
'##first_name_guide##': first_name_guide,
|
|
903
|
+
'##customer_first_name##': first_name or contact_name,
|
|
904
|
+
'##selected_product##': selected_product_name or 'our solution',
|
|
905
|
+
'##company_info##': company_summary,
|
|
906
|
+
'##selected_product_info##': product_summary
|
|
907
|
+
}
|
|
908
|
+
|
|
862
909
|
return {key: (value if value is not None else '') for key, value in replacements.items()}
|
|
863
910
|
|
|
864
911
|
def _build_company_info_summary(self, company_info: Dict[str, Any], contact_info: Dict[str, Any], pain_points: List[Dict[str, Any]], scoring_data: Dict[str, Any]) -> str:
|
|
@@ -932,20 +979,27 @@ class InitialOutreachStage(BaseStage):
|
|
|
932
979
|
summary = "\n".join(lines).strip()
|
|
933
980
|
return summary or 'Product details unavailable.'
|
|
934
981
|
|
|
935
|
-
def _build_first_name_guide(self, language: str, contact_name: str) -> str:
|
|
936
|
-
if not language:
|
|
937
|
-
return ''
|
|
938
|
-
|
|
939
|
-
language_lower = language.lower()
|
|
940
|
-
if
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
if
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
982
|
+
def _build_first_name_guide(self, language: str, contact_name: str) -> str:
|
|
983
|
+
if not language:
|
|
984
|
+
return ''
|
|
985
|
+
|
|
986
|
+
language_lower = language.lower()
|
|
987
|
+
name_parts = contact_name.split() if contact_name else []
|
|
988
|
+
if language_lower in ('vietnamese', 'vi'):
|
|
989
|
+
if not contact_name or contact_name.lower() == 'a person':
|
|
990
|
+
return "If the recipient's name is unknown, use `anh/chi` in the greeting."
|
|
991
|
+
vn_name = name_parts[-1] if name_parts else contact_name
|
|
992
|
+
if vn_name:
|
|
993
|
+
return f"For Vietnamese recipients, use `anh/chi {vn_name}` in the greeting to keep it respectful. Do not use placeholders or omit the honorific."
|
|
994
|
+
return "For Vietnamese recipients, use `anh/chi` followed by the recipient's first name in the greeting."
|
|
995
|
+
|
|
996
|
+
if name_parts:
|
|
997
|
+
en_name = name_parts[0]
|
|
998
|
+
return (
|
|
999
|
+
f'Use only the recipient\'s first name "{en_name}" in the greeting. '
|
|
1000
|
+
f'Start with "Hi {en_name}," or "Hello {en_name}," and do not use the surname or placeholders.'
|
|
1001
|
+
)
|
|
1002
|
+
return 'If the recipient name is unknown, use a neutral greeting like "Hi there," without placeholders.'
|
|
949
1003
|
|
|
950
1004
|
def _extract_first_name(self, full_name: str) -> str:
|
|
951
1005
|
if not full_name:
|
|
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
|