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.

Files changed (43) hide show
  1. {fusesell-1.2.6 → fusesell-1.2.7}/CHANGELOG.md +10 -0
  2. {fusesell-1.2.6/fusesell.egg-info → fusesell-1.2.7}/PKG-INFO +1 -1
  3. {fusesell-1.2.6 → fusesell-1.2.7/fusesell.egg-info}/PKG-INFO +1 -1
  4. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/__init__.py +1 -1
  5. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/pipeline.py +23 -19
  6. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/initial_outreach.py +108 -54
  7. {fusesell-1.2.6 → fusesell-1.2.7}/pyproject.toml +1 -1
  8. {fusesell-1.2.6 → fusesell-1.2.7}/LICENSE +0 -0
  9. {fusesell-1.2.6 → fusesell-1.2.7}/MANIFEST.in +0 -0
  10. {fusesell-1.2.6 → fusesell-1.2.7}/README.md +0 -0
  11. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell.egg-info/SOURCES.txt +0 -0
  12. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell.egg-info/dependency_links.txt +0 -0
  13. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell.egg-info/entry_points.txt +0 -0
  14. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell.egg-info/requires.txt +0 -0
  15. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell.egg-info/top_level.txt +0 -0
  16. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell.py +0 -0
  17. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/api.py +0 -0
  18. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/cli.py +0 -0
  19. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/config/__init__.py +0 -0
  20. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/config/prompts.py +0 -0
  21. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/config/settings.py +0 -0
  22. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/__init__.py +0 -0
  23. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/base_stage.py +0 -0
  24. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/data_acquisition.py +0 -0
  25. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/data_preparation.py +0 -0
  26. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/follow_up.py +0 -0
  27. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/stages/lead_scoring.py +0 -0
  28. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/tests/conftest.py +0 -0
  29. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/tests/test_api.py +0 -0
  30. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/tests/test_cli.py +0 -0
  31. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/tests/test_data_manager_products.py +0 -0
  32. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/tests/test_data_manager_sales_process.py +0 -0
  33. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/tests/test_data_manager_teams.py +0 -0
  34. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/__init__.py +0 -0
  35. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/birthday_email_manager.py +0 -0
  36. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/data_manager.py +0 -0
  37. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/event_scheduler.py +0 -0
  38. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/llm_client.py +0 -0
  39. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/logger.py +0 -0
  40. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/timezone_detector.py +0 -0
  41. {fusesell-1.2.6 → fusesell-1.2.7}/fusesell_local/utils/validators.py +0 -0
  42. {fusesell-1.2.6 → fusesell-1.2.7}/requirements.txt +0 -0
  43. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusesell
3
- Version: 1.2.6
3
+ Version: 1.2.7
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.6
3
+ Version: 1.2.7
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.6"
35
+ __version__ = "1.2.7"
36
36
  __author__ = "FuseSell Team"
37
37
  __description__ = "Local implementation of FuseSell AI sales automation pipeline"
@@ -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
- 'language': self.config.get('language', 'english'),
385
- # Data sources (matching executor schema)
386
- 'input_website': self.config.get('input_website', ''),
387
- 'input_description': self.config.get('input_description', ''),
388
- 'input_business_card': self.config.get('input_business_card', ''),
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
- email_drafts.extend(data.get('drafts', []))
609
- elif stage_name == 'follow_up':
610
- email_drafts.extend(data.get('drafts', []))
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
- 'recommended_product': recommended_product,
120
- 'customer_summary': self._create_customer_summary(customer_data),
121
- 'total_drafts_generated': len(saved_drafts),
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
- language = input_data.get('language') or company_info.get('language') or 'English'
825
- contact_name = contact_info.get('name') or input_data.get('customer_name') or input_data.get('recipient_name') or 'there'
826
- company_name = company_info.get('name') or input_data.get('company_name') or 'the company'
827
- staff_name = input_data.get('staff_name') or input_data.get('sender_name') or 'Sales Team'
828
- org_name = input_data.get('org_name') or 'Our Company'
829
- selected_product_name = recommended_product.get('product_name') if recommended_product else None
830
-
831
- action = input_data.get('action', 'draft_write')
832
- action_labels = {
833
- 'draft_write': 'email drafts',
834
- 'draft_rewrite': 'email rewrites',
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
- '##selected_product##': selected_product_name or 'our solution',
858
- '##company_info##': company_summary,
859
- '##selected_product_info##': product_summary
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 language_lower in ('vietnamese', 'vi'):
941
- if not contact_name or contact_name.lower() == 'a person':
942
- return "If the recipient's name is unknown, use `anh/chi` in the greeting."
943
- first_name = self._extract_first_name(contact_name)
944
- if first_name:
945
- return f"For Vietnamese recipients, use `anh/chi {first_name}` in the greeting to keep it respectful."
946
- return "For Vietnamese recipients, use `anh/chi` followed by the recipient's first name in the greeting."
947
-
948
- return ''
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:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fusesell"
7
- version = "1.2.6"
7
+ version = "1.2.7"
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