fusesell 1.2.5__py3-none-any.whl → 1.2.7__py3-none-any.whl
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.5.dist-info → fusesell-1.2.7.dist-info}/METADATA +1 -1
- {fusesell-1.2.5.dist-info → fusesell-1.2.7.dist-info}/RECORD +10 -10
- fusesell_local/__init__.py +1 -1
- fusesell_local/pipeline.py +23 -19
- fusesell_local/stages/initial_outreach.py +234 -67
- fusesell_local/utils/data_manager.py +130 -52
- {fusesell-1.2.5.dist-info → fusesell-1.2.7.dist-info}/WHEEL +0 -0
- {fusesell-1.2.5.dist-info → fusesell-1.2.7.dist-info}/entry_points.txt +0 -0
- {fusesell-1.2.5.dist-info → fusesell-1.2.7.dist-info}/licenses/LICENSE +0 -0
- {fusesell-1.2.5.dist-info → fusesell-1.2.7.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
fusesell.py,sha256=t5PjkhWEJGINp4k517u0EX0ge7lzuHOUHHro-BE1mGk,596
|
|
2
|
-
fusesell-1.2.
|
|
3
|
-
fusesell_local/__init__.py,sha256=
|
|
2
|
+
fusesell-1.2.7.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
|
|
3
|
+
fusesell_local/__init__.py,sha256=8U1jX2uHDz_7rJeLg7NGKJGJum7I8VK-RwZCtXxRVrs,966
|
|
4
4
|
fusesell_local/api.py,sha256=AcPune5YJdgi7nsMeusCUqc49z5UiycsQb6n3yiV_No,10839
|
|
5
5
|
fusesell_local/cli.py,sha256=MYnVxuEf5KTR4VcO3sc-VtP9NkWlSixJsYfOWST2Ds0,65859
|
|
6
|
-
fusesell_local/pipeline.py,sha256=
|
|
6
|
+
fusesell_local/pipeline.py,sha256=RMF_kgwNEc1ka8-CDJyzIOTSo8PGtR_zPKAgRevhlNo,39913
|
|
7
7
|
fusesell_local/config/__init__.py,sha256=0ErO7QiSDqKn-LHcjIRdLZzh5QaRTkRsIlwfgpkkDz8,209
|
|
8
8
|
fusesell_local/config/prompts.py,sha256=5O3Y2v3GCi9d9FEyR6Ekc1UXVq2TcZp3Rrspvx4bkac,10164
|
|
9
9
|
fusesell_local/config/settings.py,sha256=rbjGPLQTFFr7DiWrPnZDFaOSNsdEMMYFx6pn7b13xGs,10743
|
|
@@ -12,7 +12,7 @@ fusesell_local/stages/base_stage.py,sha256=ldo5xuHZto7ceEg3i_3rxAx0xPccK4n2jaxEJ
|
|
|
12
12
|
fusesell_local/stages/data_acquisition.py,sha256=Td3mwakJRoEYbi3od4v2ZzKOHLgLSgccZVxH3ezs1_4,71081
|
|
13
13
|
fusesell_local/stages/data_preparation.py,sha256=XWLg9b1w2NrMxLcrWDqB95mRmLQmVIMXpKNaBNr98TQ,52751
|
|
14
14
|
fusesell_local/stages/follow_up.py,sha256=H9Xek6EoIbHrerQvGTRswXDNFH6zq71DcRxxj0zpo2g,77747
|
|
15
|
-
fusesell_local/stages/initial_outreach.py,sha256=
|
|
15
|
+
fusesell_local/stages/initial_outreach.py,sha256=U6KHZIpr7d-W3x65z9XVm0gKL88SaVR47-_cQq3f-dc,117170
|
|
16
16
|
fusesell_local/stages/lead_scoring.py,sha256=ir3l849eMGrGLf0OYUcmA1F3FwyYhAplS4niU3R2GRY,60658
|
|
17
17
|
fusesell_local/tests/conftest.py,sha256=TWUtlP6cNPVOYkTPz-j9BzS_KnXdPWy8D-ObPLHvXYs,366
|
|
18
18
|
fusesell_local/tests/test_api.py,sha256=763rUVb5pAuAQOovug6Ka0T9eGK8-WVOC_J08M7TETo,1827
|
|
@@ -22,14 +22,14 @@ fusesell_local/tests/test_data_manager_sales_process.py,sha256=NbwxQ9oBKCZfrkRQY
|
|
|
22
22
|
fusesell_local/tests/test_data_manager_teams.py,sha256=kjk4V4r9ja4EVREIiQMxkuZd470SSwRHJAvpHln9KO4,4578
|
|
23
23
|
fusesell_local/utils/__init__.py,sha256=TVemlo0wpckhNUxP3a1Tky3ekswy8JdIHaXBlkKXKBQ,330
|
|
24
24
|
fusesell_local/utils/birthday_email_manager.py,sha256=NKLoUyzPedyhewZPma21SOoU8p9wPquehloer7TRA9U,20478
|
|
25
|
-
fusesell_local/utils/data_manager.py,sha256=
|
|
25
|
+
fusesell_local/utils/data_manager.py,sha256=60CLOVkVB76AQx1wQyja0PFmA1t-YJITiGNni14IPOs,188449
|
|
26
26
|
fusesell_local/utils/event_scheduler.py,sha256=YwWIdkvRdWFdDLX-sepI5AXJOhEIullIclpk9njvZAA,38577
|
|
27
27
|
fusesell_local/utils/llm_client.py,sha256=FVc25UlGt6hro7h5Iw7PHSXY3E3_67Xc-SUbHuMSRs0,10437
|
|
28
28
|
fusesell_local/utils/logger.py,sha256=sWlV8Tjyz_Z8J4zXKOnNalh8_iD6ytfrwPZpD-wcEOs,6259
|
|
29
29
|
fusesell_local/utils/timezone_detector.py,sha256=0cAE4c8ZXqCA8AvxRKm6PrFKmAmsbq3HOHR6w-mW3KQ,39997
|
|
30
30
|
fusesell_local/utils/validators.py,sha256=Z1VzeoxFsnuzlIA_ZaMWoy-0Cgyqupd47kIdljlMDbs,15438
|
|
31
|
-
fusesell-1.2.
|
|
32
|
-
fusesell-1.2.
|
|
33
|
-
fusesell-1.2.
|
|
34
|
-
fusesell-1.2.
|
|
35
|
-
fusesell-1.2.
|
|
31
|
+
fusesell-1.2.7.dist-info/METADATA,sha256=uHVe8upgxtCHgruhMvflhXpBfUpi5DD2eeDR4g9JPU8,35074
|
|
32
|
+
fusesell-1.2.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
fusesell-1.2.7.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
|
|
34
|
+
fusesell-1.2.7.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
|
|
35
|
+
fusesell-1.2.7.dist-info/RECORD,,
|
fusesell_local/__init__.py
CHANGED
fusesell_local/pipeline.py
CHANGED
|
@@ -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)
|
|
@@ -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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
'##
|
|
745
|
-
'##
|
|
746
|
-
'##
|
|
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
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|