fusesell 1.2.5__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.5 → fusesell-1.2.7}/CHANGELOG.md +19 -0
  2. {fusesell-1.2.5/fusesell.egg-info → fusesell-1.2.7}/PKG-INFO +1 -1
  3. {fusesell-1.2.5 → fusesell-1.2.7/fusesell.egg-info}/PKG-INFO +1 -1
  4. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/__init__.py +1 -1
  5. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/pipeline.py +23 -19
  6. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/stages/initial_outreach.py +234 -67
  7. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/utils/data_manager.py +130 -52
  8. {fusesell-1.2.5 → fusesell-1.2.7}/pyproject.toml +1 -1
  9. {fusesell-1.2.5 → fusesell-1.2.7}/LICENSE +0 -0
  10. {fusesell-1.2.5 → fusesell-1.2.7}/MANIFEST.in +0 -0
  11. {fusesell-1.2.5 → fusesell-1.2.7}/README.md +0 -0
  12. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell.egg-info/SOURCES.txt +0 -0
  13. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell.egg-info/dependency_links.txt +0 -0
  14. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell.egg-info/entry_points.txt +0 -0
  15. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell.egg-info/requires.txt +0 -0
  16. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell.egg-info/top_level.txt +0 -0
  17. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell.py +0 -0
  18. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/api.py +0 -0
  19. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/cli.py +0 -0
  20. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/config/__init__.py +0 -0
  21. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/config/prompts.py +0 -0
  22. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/config/settings.py +0 -0
  23. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/stages/__init__.py +0 -0
  24. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/stages/base_stage.py +0 -0
  25. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/stages/data_acquisition.py +0 -0
  26. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/stages/data_preparation.py +0 -0
  27. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/stages/follow_up.py +0 -0
  28. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/stages/lead_scoring.py +0 -0
  29. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/tests/conftest.py +0 -0
  30. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/tests/test_api.py +0 -0
  31. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/tests/test_cli.py +0 -0
  32. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/tests/test_data_manager_products.py +0 -0
  33. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/tests/test_data_manager_sales_process.py +0 -0
  34. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/tests/test_data_manager_teams.py +0 -0
  35. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/utils/__init__.py +0 -0
  36. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/utils/birthday_email_manager.py +0 -0
  37. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/utils/event_scheduler.py +0 -0
  38. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/utils/llm_client.py +0 -0
  39. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/utils/logger.py +0 -0
  40. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/utils/timezone_detector.py +0 -0
  41. {fusesell-1.2.5 → fusesell-1.2.7}/fusesell_local/utils/validators.py +0 -0
  42. {fusesell-1.2.5 → fusesell-1.2.7}/requirements.txt +0 -0
  43. {fusesell-1.2.5 → fusesell-1.2.7}/setup.cfg +0 -0
@@ -2,6 +2,25 @@
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
+
15
+ # [1.2.6] - 2025-10-24
16
+
17
+ ### Added
18
+ - Automatically seed packaged prompt, scoring, and template JSON files into the writable `fusesell_data/config` directory so fresh installs immediately pick up the default initial outreach draft prompt.
19
+ - Draft generation now records the scheduled reminder metadata in stage output while mirroring the server’s `schedule_auto_run` behaviour locally.
20
+
21
+ ### Fixed
22
+ - Bundled configuration files are used as a fallback when the data directory is missing overrides, preventing empty prompt loads that previously produced low-quality duplicate drafts.
23
+
5
24
  # [1.2.5] - 2025-10-24
6
25
 
7
26
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusesell
3
- Version: 1.2.5
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.5
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.5"
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)
@@ -102,23 +102,33 @@ class InitialOutreachStage(BaseStage):
102
102
  # Generate multiple email drafts
103
103
  email_drafts = self._generate_email_drafts(customer_data, recommended_product, scoring_data, context)
104
104
 
105
- # Save drafts to local files and database
106
- saved_drafts = self._save_email_drafts(context, email_drafts)
107
-
108
- # Prepare final output
109
- outreach_data = {
110
- 'action': 'draft_write',
111
- 'status': 'drafts_generated',
112
- 'email_drafts': saved_drafts,
113
- 'recommended_product': recommended_product,
114
- 'customer_summary': self._create_customer_summary(customer_data),
115
- 'total_drafts_generated': len(saved_drafts),
116
- 'generation_timestamp': datetime.now().isoformat(),
117
- 'customer_id': context.get('execution_id')
118
- }
119
-
120
- # Save to database
121
- self.save_stage_result(context, outreach_data)
105
+ # Save drafts to local files and database
106
+ saved_drafts = self._save_email_drafts(context, email_drafts)
107
+
108
+ schedule_summary = self._schedule_initial_reminder_for_drafts(
109
+ saved_drafts,
110
+ customer_data,
111
+ context
112
+ )
113
+
114
+ # Prepare final output
115
+ outreach_data = {
116
+ 'action': 'draft_write',
117
+ 'status': 'drafts_generated',
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),
123
+ 'generation_timestamp': datetime.now().isoformat(),
124
+ 'customer_id': context.get('execution_id')
125
+ }
126
+
127
+ if schedule_summary:
128
+ outreach_data['reminder_schedule'] = schedule_summary
129
+
130
+ # Save to database
131
+ self.save_stage_result(context, outreach_data)
122
132
 
123
133
  result = self.create_success_result(outreach_data, context)
124
134
  return result
@@ -318,8 +328,8 @@ class InitialOutreachStage(BaseStage):
318
328
  team_id = input_data.get('team_id')
319
329
  team_name = input_data.get('team_name')
320
330
  language = input_data.get('language')
321
- customer_name = input_data.get('customer_name')
322
- 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'
323
333
  reminder_room = self.config.get('reminder_room_id') or input_data.get('reminder_room_id')
324
334
  draft_id = draft.get('draft_id') or 'unknown_draft'
325
335
 
@@ -346,6 +356,8 @@ class InitialOutreachStage(BaseStage):
346
356
  customextra['approach'] = draft.get('approach')
347
357
  if draft.get('mail_tone'):
348
358
  customextra['mail_tone'] = draft.get('mail_tone')
359
+ if recipient_address and 'customer_email' not in customextra:
360
+ customextra['customer_email'] = recipient_address
349
361
 
350
362
  return {
351
363
  'status': 'published',
@@ -359,10 +371,124 @@ class InitialOutreachStage(BaseStage):
359
371
  'team_name': team_name,
360
372
  'language': language,
361
373
  'customer_name': customer_name,
374
+ 'customer_email': recipient_address,
362
375
  'staff_name': staff_name,
363
376
  'customextra': customextra
364
377
  }
365
378
 
379
+ def _schedule_initial_reminder_for_drafts(
380
+ self,
381
+ drafts: List[Dict[str, Any]],
382
+ customer_data: Dict[str, Any],
383
+ context: Dict[str, Any]
384
+ ) -> Optional[Dict[str, Any]]:
385
+ """
386
+ Schedule reminder_task row for the highest-ranked draft after draft generation.
387
+
388
+ Mirrors the server-side behaviour where schedule_auto_run seeds reminder_task
389
+ so RealTimeX automations can pick up pending outreach immediately.
390
+ """
391
+ if not drafts:
392
+ return None
393
+
394
+ input_data = context.get('input_data', {})
395
+
396
+ if input_data.get('send_immediately'):
397
+ self.logger.debug("Skipping reminder scheduling because send_immediately is True")
398
+ return None
399
+
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 {}
405
+
406
+ recipient_address = (
407
+ input_data.get('recipient_address')
408
+ or contact_info.get('email')
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')
413
+ )
414
+ if not recipient_address:
415
+ self.logger.info("Skipping reminder scheduling: recipient email not available")
416
+ return None
417
+
418
+ recipient_name = (
419
+ input_data.get('recipient_name')
420
+ or contact_info.get('name')
421
+ or contact_info.get('fullName')
422
+ or data_acquisition.get('contact_name')
423
+ or data_acquisition.get('customer_name')
424
+ or ''
425
+ )
426
+
427
+ def _draft_sort_key(draft: Dict[str, Any]) -> tuple[int, float]:
428
+ priority = draft.get('priority_order')
429
+ if not isinstance(priority, int):
430
+ priority = 999
431
+ personalization = draft.get('personalization_score', 0)
432
+ try:
433
+ personalization_value = float(personalization)
434
+ except (TypeError, ValueError):
435
+ personalization_value = 0.0
436
+ return (priority, -personalization_value)
437
+
438
+ ordered_drafts = sorted(drafts, key=_draft_sort_key)
439
+ if not ordered_drafts:
440
+ return None
441
+
442
+ top_draft = ordered_drafts[0]
443
+
444
+ try:
445
+ from ..utils.event_scheduler import EventScheduler
446
+ scheduler = EventScheduler(self.config.get('data_dir', './fusesell_data'))
447
+ except Exception as exc:
448
+ self.logger.warning(
449
+ "Failed to initialise EventScheduler for reminder scheduling: %s",
450
+ exc
451
+ )
452
+ return {'success': False, 'error': str(exc)}
453
+
454
+ reminder_context = self._build_initial_reminder_context(
455
+ top_draft,
456
+ recipient_address,
457
+ recipient_name,
458
+ context
459
+ )
460
+
461
+ try:
462
+ schedule_result = scheduler.schedule_email_event(
463
+ draft_id=top_draft.get('draft_id'),
464
+ recipient_address=recipient_address,
465
+ recipient_name=recipient_name,
466
+ org_id=input_data.get('org_id') or self.config.get('org_id', 'default'),
467
+ team_id=input_data.get('team_id') or self.config.get('team_id'),
468
+ customer_timezone=input_data.get('customer_timezone'),
469
+ email_type='initial',
470
+ send_immediately=False,
471
+ reminder_context=reminder_context
472
+ )
473
+ except Exception as exc:
474
+ self.logger.error(f"Initial reminder scheduling failed: {exc}")
475
+ return {'success': False, 'error': str(exc)}
476
+
477
+ if schedule_result.get('success'):
478
+ self.logger.info(
479
+ "Scheduled initial outreach reminder %s for draft %s",
480
+ schedule_result.get('reminder_task_id'),
481
+ top_draft.get('draft_id')
482
+ )
483
+ else:
484
+ self.logger.warning(
485
+ "Reminder scheduling returned failure for draft %s: %s",
486
+ top_draft.get('draft_id'),
487
+ schedule_result.get('error')
488
+ )
489
+
490
+ return schedule_result
491
+
366
492
  def _handle_close(self, context: Dict[str, Any]) -> Dict[str, Any]:
367
493
  """
368
494
  Handle close action - Close outreach when customer feels negative.
@@ -706,46 +832,80 @@ class InitialOutreachStage(BaseStage):
706
832
 
707
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]:
708
834
  input_data = context.get('input_data', {})
709
- company_info = customer_data.get('companyInfo', {}) or {}
710
- contact_info = customer_data.get('primaryContact', {}) or {}
711
- language = input_data.get('language') or company_info.get('language') or 'English'
712
- contact_name = contact_info.get('name') or input_data.get('customer_name') or input_data.get('recipient_name') or 'there'
713
- company_name = company_info.get('name') or input_data.get('company_name') or 'the company'
714
- staff_name = input_data.get('staff_name') or input_data.get('sender_name') or 'Sales Team'
715
- org_name = input_data.get('org_name') or 'Our Company'
716
- selected_product_name = recommended_product.get('product_name') if recommended_product else None
717
-
718
- action = input_data.get('action', 'draft_write')
719
- action_labels = {
720
- 'draft_write': 'email drafts',
721
- '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',
722
881
  'send': 'email sends',
723
882
  'close': 'email workflow'
724
883
  }
725
884
  action_type = action_labels.get(action, action.replace('_', ' '))
726
885
 
727
- company_summary = self._build_company_info_summary(
728
- company_info,
729
- contact_info,
730
- customer_data.get('painPoints', []),
731
- scoring_data
732
- )
733
- product_summary = self._build_product_info_summary(recommended_product)
734
- first_name_guide = self._build_first_name_guide(language, contact_name)
735
-
736
- replacements = {
737
- '##action_type##': action_type,
738
- '##language##': language.title() if isinstance(language, str) else 'English',
739
- '##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,
740
899
  '##company_name##': company_name,
741
- '##staff_name##': staff_name,
742
- '##org_name##': org_name,
743
- '##first_name_guide##': first_name_guide,
744
- '##selected_product##': selected_product_name or 'our solution',
745
- '##company_info##': company_summary,
746
- '##selected_product_info##': product_summary
747
- }
748
-
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
+
749
909
  return {key: (value if value is not None else '') for key, value in replacements.items()}
750
910
 
751
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:
@@ -819,20 +979,27 @@ class InitialOutreachStage(BaseStage):
819
979
  summary = "\n".join(lines).strip()
820
980
  return summary or 'Product details unavailable.'
821
981
 
822
- def _build_first_name_guide(self, language: str, contact_name: str) -> str:
823
- if not language:
824
- return ''
825
-
826
- language_lower = language.lower()
827
- if language_lower in ('vietnamese', 'vi'):
828
- if not contact_name or contact_name.lower() == 'a person':
829
- return "If the recipient's name is unknown, use `anh/chi` in the greeting."
830
- first_name = self._extract_first_name(contact_name)
831
- if first_name:
832
- return f"For Vietnamese recipients, use `anh/chi {first_name}` in the greeting to keep it respectful."
833
- return "For Vietnamese recipients, use `anh/chi` followed by the recipient's first name in the greeting."
834
-
835
- 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.'
836
1003
 
837
1004
  def _extract_first_name(self, full_name: str) -> str:
838
1005
  if not full_name:
@@ -3,14 +3,15 @@ Local Data Manager for FuseSell Local Implementation
3
3
  Handles SQLite database operations and local file management
4
4
  """
5
5
 
6
- import sqlite3
6
+ import sqlite3
7
7
  import json
8
8
  import os
9
9
  import uuid
10
+ import shutil
10
11
  from typing import Dict, Any, List, Optional, Sequence, Union
11
- from datetime import datetime
12
- import logging
13
- from pathlib import Path
12
+ from datetime import datetime
13
+ import logging
14
+ from pathlib import Path
14
15
 
15
16
 
16
17
  class LocalDataManager:
@@ -69,10 +70,72 @@ class LocalDataManager:
69
70
  # Initialize database with optimization check
70
71
  self._init_database_optimized()
71
72
 
72
- def _create_directories(self) -> None:
73
- """Create necessary directories for data storage."""
74
- for directory in [self.data_dir, self.config_dir, self.drafts_dir, self.logs_dir]:
75
- directory.mkdir(parents=True, exist_ok=True)
73
+ def _create_directories(self) -> None:
74
+ """Create necessary directories for data storage."""
75
+ for directory in [self.data_dir, self.config_dir, self.drafts_dir, self.logs_dir]:
76
+ directory.mkdir(parents=True, exist_ok=True)
77
+ self._ensure_default_config_files()
78
+
79
+ def _ensure_default_config_files(self) -> None:
80
+ """
81
+ Copy bundled configuration defaults into the writable data directory when missing.
82
+
83
+ Ensures first-run executions always have the same baseline prompts, scoring criteria,
84
+ and email templates as the packaged FuseSell server flows.
85
+ """
86
+ try:
87
+ package_config_dir = Path(__file__).resolve().parents[2] / "fusesell_data" / "config"
88
+ except Exception as exc:
89
+ self.logger.debug(f"Unable to resolve packaged config directory: {exc}")
90
+ return
91
+
92
+ if not package_config_dir.exists():
93
+ self.logger.debug("Packaged config directory not found; skipping default config seeding")
94
+ return
95
+
96
+ default_files = [
97
+ "prompts.json",
98
+ "scoring_criteria.json",
99
+ "email_templates.json",
100
+ ]
101
+
102
+ for filename in default_files:
103
+ target = self.config_dir / filename
104
+ if target.exists():
105
+ continue
106
+
107
+ source = package_config_dir / filename
108
+ if not source.exists():
109
+ self.logger.debug(f"Packaged default {filename} not found; skipping seed")
110
+ continue
111
+
112
+ try:
113
+ shutil.copyfile(source, target)
114
+ self.logger.info(f"Seeded default configuration file: {filename}")
115
+ except Exception as exc:
116
+ self.logger.warning(f"Failed to seed default configuration {filename}: {exc}")
117
+
118
+ def _load_packaged_config_file(self, filename: str) -> Dict[str, Any]:
119
+ """
120
+ Load a configuration JSON file bundled with the package as a fallback.
121
+
122
+ Args:
123
+ filename: Name of the configuration file to load.
124
+
125
+ Returns:
126
+ Parsed configuration dictionary or empty dict on failure.
127
+ """
128
+ try:
129
+ package_config_dir = Path(__file__).resolve().parents[2] / "fusesell_data" / "config"
130
+ path = package_config_dir / filename
131
+ if not path.exists():
132
+ return {}
133
+
134
+ with path.open("r", encoding="utf-8") as handle:
135
+ return json.load(handle)
136
+ except Exception as exc:
137
+ self.logger.debug(f"Failed to load packaged config {filename}: {exc}")
138
+ return {}
76
139
 
77
140
  def _init_database_optimized(self) -> None:
78
141
  """
@@ -1050,56 +1113,71 @@ class LocalDataManager:
1050
1113
  self.logger.error(f"Failed to get stage results: {str(e)}")
1051
1114
  raise
1052
1115
 
1053
- def load_prompts(self) -> Dict[str, Any]:
1054
- """
1055
- Load prompt templates from configuration.
1056
-
1057
- Returns:
1058
- Dictionary of prompt templates
1059
- """
1060
- try:
1061
- prompts_file = self.config_dir / "prompts.json"
1062
- if prompts_file.exists():
1063
- with open(prompts_file, 'r', encoding='utf-8') as f:
1064
- return json.load(f)
1065
- return {}
1066
- except Exception as e:
1067
- self.logger.error(f"Failed to load prompts: {str(e)}")
1068
- return {}
1069
-
1070
- def load_scoring_criteria(self) -> Dict[str, Any]:
1116
+ def load_prompts(self) -> Dict[str, Any]:
1117
+ """
1118
+ Load prompt templates from configuration.
1119
+
1120
+ Returns:
1121
+ Dictionary of prompt templates
1122
+ """
1123
+ try:
1124
+ prompts_file = self.config_dir / "prompts.json"
1125
+ if prompts_file.exists():
1126
+ with open(prompts_file, 'r', encoding='utf-8') as f:
1127
+ return json.load(f)
1128
+
1129
+ packaged = self._load_packaged_config_file("prompts.json")
1130
+ if packaged:
1131
+ return packaged
1132
+
1133
+ return {}
1134
+ except Exception as e:
1135
+ self.logger.error(f"Failed to load prompts: {str(e)}")
1136
+ return {}
1137
+
1138
+ def load_scoring_criteria(self) -> Dict[str, Any]:
1071
1139
  """
1072
1140
  Load scoring criteria configuration.
1073
1141
 
1074
- Returns:
1075
- Dictionary of scoring criteria
1076
- """
1077
- try:
1078
- criteria_file = self.config_dir / "scoring_criteria.json"
1079
- if criteria_file.exists():
1080
- with open(criteria_file, 'r', encoding='utf-8') as f:
1081
- return json.load(f)
1082
- return {}
1083
- except Exception as e:
1084
- self.logger.error(f"Failed to load scoring criteria: {str(e)}")
1085
- return {}
1086
-
1087
- def load_email_templates(self) -> Dict[str, Any]:
1142
+ Returns:
1143
+ Dictionary of scoring criteria
1144
+ """
1145
+ try:
1146
+ criteria_file = self.config_dir / "scoring_criteria.json"
1147
+ if criteria_file.exists():
1148
+ with open(criteria_file, 'r', encoding='utf-8') as f:
1149
+ return json.load(f)
1150
+
1151
+ packaged = self._load_packaged_config_file("scoring_criteria.json")
1152
+ if packaged:
1153
+ return packaged
1154
+
1155
+ return {}
1156
+ except Exception as e:
1157
+ self.logger.error(f"Failed to load scoring criteria: {str(e)}")
1158
+ return {}
1159
+
1160
+ def load_email_templates(self) -> Dict[str, Any]:
1088
1161
  """
1089
1162
  Load email templates configuration.
1090
1163
 
1091
- Returns:
1092
- Dictionary of email templates
1093
- """
1094
- try:
1095
- templates_file = self.config_dir / "email_templates.json"
1096
- if templates_file.exists():
1097
- with open(templates_file, 'r', encoding='utf-8') as f:
1098
- return json.load(f)
1099
- return {}
1100
- except Exception as e:
1101
- self.logger.error(f"Failed to load email templates: {str(e)}")
1102
- return {}
1164
+ Returns:
1165
+ Dictionary of email templates
1166
+ """
1167
+ try:
1168
+ templates_file = self.config_dir / "email_templates.json"
1169
+ if templates_file.exists():
1170
+ with open(templates_file, 'r', encoding='utf-8') as f:
1171
+ return json.load(f)
1172
+
1173
+ packaged = self._load_packaged_config_file("email_templates.json")
1174
+ if packaged:
1175
+ return packaged
1176
+
1177
+ return {}
1178
+ except Exception as e:
1179
+ self.logger.error(f"Failed to load email templates: {str(e)}")
1180
+ return {}
1103
1181
 
1104
1182
  def _generate_customer_id(self) -> str:
1105
1183
  """Generate unique customer ID."""
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fusesell"
7
- version = "1.2.5"
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