fusesell 1.2.4__py3-none-any.whl → 1.2.6__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.4.dist-info → fusesell-1.2.6.dist-info}/METADATA +1 -1
- {fusesell-1.2.4.dist-info → fusesell-1.2.6.dist-info}/RECORD +12 -12
- fusesell_local/__init__.py +1 -1
- fusesell_local/pipeline.py +11 -5
- fusesell_local/stages/follow_up.py +98 -22
- fusesell_local/stages/initial_outreach.py +224 -42
- fusesell_local/utils/data_manager.py +265 -78
- fusesell_local/utils/event_scheduler.py +386 -76
- {fusesell-1.2.4.dist-info → fusesell-1.2.6.dist-info}/WHEEL +0 -0
- {fusesell-1.2.4.dist-info → fusesell-1.2.6.dist-info}/entry_points.txt +0 -0
- {fusesell-1.2.4.dist-info → fusesell-1.2.6.dist-info}/licenses/LICENSE +0 -0
- {fusesell-1.2.4.dist-info → fusesell-1.2.6.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.6.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
|
|
3
|
+
fusesell_local/__init__.py,sha256=06b8-0tg3rF1k8Bb_XTASXJ3rgA3lIM8RgdXEDeaiCY,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=zhAy8dtfcfjmRFqW9Dxr5fCGuxqvejFrIrJFw7s7hVU,39664
|
|
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
|
|
@@ -11,8 +11,8 @@ fusesell_local/stages/__init__.py,sha256=2mAmzcMlVKZdseOR5Jju4PaPdKGsBT1ePqvt5RS
|
|
|
11
11
|
fusesell_local/stages/base_stage.py,sha256=ldo5xuHZto7ceEg3i_3rxAx0xPccK4n2jaxEJA96RUE,22069
|
|
12
12
|
fusesell_local/stages/data_acquisition.py,sha256=Td3mwakJRoEYbi3od4v2ZzKOHLgLSgccZVxH3ezs1_4,71081
|
|
13
13
|
fusesell_local/stages/data_preparation.py,sha256=XWLg9b1w2NrMxLcrWDqB95mRmLQmVIMXpKNaBNr98TQ,52751
|
|
14
|
-
fusesell_local/stages/follow_up.py,sha256=
|
|
15
|
-
fusesell_local/stages/initial_outreach.py,sha256=
|
|
14
|
+
fusesell_local/stages/follow_up.py,sha256=H9Xek6EoIbHrerQvGTRswXDNFH6zq71DcRxxj0zpo2g,77747
|
|
15
|
+
fusesell_local/stages/initial_outreach.py,sha256=znokii7zEEOqgRWzOAeGwZxBLUyA7ks70zGN3uAiCDQ,114322
|
|
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=
|
|
26
|
-
fusesell_local/utils/event_scheduler.py,sha256=
|
|
25
|
+
fusesell_local/utils/data_manager.py,sha256=60CLOVkVB76AQx1wQyja0PFmA1t-YJITiGNni14IPOs,188449
|
|
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.6.dist-info/METADATA,sha256=1JvOPSQ0R602Oa0HoGfPVbQ4c1akfTDyc5rsevASgf8,35074
|
|
32
|
+
fusesell-1.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
fusesell-1.2.6.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
|
|
34
|
+
fusesell-1.2.6.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
|
|
35
|
+
fusesell-1.2.6.dist-info/RECORD,,
|
fusesell_local/__init__.py
CHANGED
fusesell_local/pipeline.py
CHANGED
|
@@ -251,11 +251,17 @@ class FuseSellPipeline:
|
|
|
251
251
|
|
|
252
252
|
self.logger.info("-" * 40)
|
|
253
253
|
self.logger.info("TIMING VALIDATION:")
|
|
254
|
-
if discrepancy_percentage < 5.0:
|
|
255
|
-
self.logger.info(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
254
|
+
if discrepancy_percentage < 5.0:
|
|
255
|
+
self.logger.info(
|
|
256
|
+
f"[OK] Timing validation PASSED (discrepancy: {discrepancy_percentage:.1f}%)"
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
self.logger.warning(
|
|
260
|
+
f"[WARN] Timing validation WARNING (discrepancy: {discrepancy_percentage:.1f}%)"
|
|
261
|
+
)
|
|
262
|
+
self.logger.warning(
|
|
263
|
+
f" Expected ~{total_stage_time:.2f}s, got {total_duration:.2f}s"
|
|
264
|
+
)
|
|
259
265
|
|
|
260
266
|
self.logger.info("=" * 60)
|
|
261
267
|
|
|
@@ -877,22 +877,29 @@ Generate only the email content, no additional commentary:"""
|
|
|
877
877
|
input_data = context.get('input_data', {})
|
|
878
878
|
|
|
879
879
|
# Initialize event scheduler
|
|
880
|
-
scheduler = EventScheduler(self.config.get('data_dir', './fusesell_data'))
|
|
881
|
-
|
|
882
|
-
# Check if immediate sending is requested
|
|
883
|
-
send_immediately = input_data.get('send_immediately', False)
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
880
|
+
scheduler = EventScheduler(self.config.get('data_dir', './fusesell_data'))
|
|
881
|
+
|
|
882
|
+
# Check if immediate sending is requested
|
|
883
|
+
send_immediately = input_data.get('send_immediately', False)
|
|
884
|
+
reminder_context = self._build_follow_up_reminder_context(
|
|
885
|
+
draft,
|
|
886
|
+
recipient_address,
|
|
887
|
+
recipient_name,
|
|
888
|
+
context
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
# Schedule the follow-up email event
|
|
892
|
+
schedule_result = scheduler.schedule_email_event(
|
|
893
|
+
draft_id=draft.get('draft_id'),
|
|
894
|
+
recipient_address=recipient_address,
|
|
895
|
+
recipient_name=recipient_name,
|
|
896
|
+
org_id=input_data.get('org_id', 'default'),
|
|
897
|
+
team_id=input_data.get('team_id'),
|
|
898
|
+
customer_timezone=input_data.get('customer_timezone'),
|
|
899
|
+
email_type='follow_up',
|
|
900
|
+
send_immediately=send_immediately,
|
|
901
|
+
reminder_context=reminder_context
|
|
902
|
+
)
|
|
896
903
|
|
|
897
904
|
if schedule_result['success']:
|
|
898
905
|
self.logger.info(f"Follow-up email event scheduled successfully: {schedule_result['event_id']} for {schedule_result['scheduled_time']}")
|
|
@@ -912,12 +919,81 @@ Generate only the email content, no additional commentary:"""
|
|
|
912
919
|
}
|
|
913
920
|
|
|
914
921
|
except Exception as e:
|
|
915
|
-
self.logger.error(f"Follow-up email scheduling failed: {str(e)}")
|
|
916
|
-
return {
|
|
917
|
-
'success': False,
|
|
918
|
-
'message': f'Follow-up email scheduling failed: {str(e)}',
|
|
919
|
-
'error': str(e)
|
|
920
|
-
}
|
|
922
|
+
self.logger.error(f"Follow-up email scheduling failed: {str(e)}")
|
|
923
|
+
return {
|
|
924
|
+
'success': False,
|
|
925
|
+
'message': f'Follow-up email scheduling failed: {str(e)}',
|
|
926
|
+
'error': str(e)
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
def _build_follow_up_reminder_context(
|
|
930
|
+
self,
|
|
931
|
+
draft: Dict[str, Any],
|
|
932
|
+
recipient_address: str,
|
|
933
|
+
recipient_name: str,
|
|
934
|
+
context: Dict[str, Any]
|
|
935
|
+
) -> Dict[str, Any]:
|
|
936
|
+
"""
|
|
937
|
+
Build reminder_task metadata for scheduled follow-up emails.
|
|
938
|
+
"""
|
|
939
|
+
input_data = context.get('input_data', {})
|
|
940
|
+
org_id = input_data.get('org_id', 'default') or 'default'
|
|
941
|
+
customer_id = input_data.get('customer_id') or context.get('execution_id') or 'unknown'
|
|
942
|
+
task_id = context.get('execution_id') or input_data.get('task_id') or 'unknown_task'
|
|
943
|
+
team_id = input_data.get('team_id')
|
|
944
|
+
team_name = input_data.get('team_name')
|
|
945
|
+
language = input_data.get('language')
|
|
946
|
+
customer_name = input_data.get('customer_name')
|
|
947
|
+
staff_name = input_data.get('staff_name')
|
|
948
|
+
interaction_type = input_data.get('interaction_type', 'follow_up')
|
|
949
|
+
follow_up_iteration = input_data.get('current_follow_up_time') or 1
|
|
950
|
+
reminder_room = self.config.get('reminder_room_id') or input_data.get('reminder_room_id')
|
|
951
|
+
draft_id = draft.get('draft_id') or 'unknown_draft'
|
|
952
|
+
product_name = draft.get('product_name') or input_data.get('product_name')
|
|
953
|
+
|
|
954
|
+
customextra = {
|
|
955
|
+
'reminder_content': 'follow_up',
|
|
956
|
+
'org_id': org_id,
|
|
957
|
+
'customer_id': customer_id,
|
|
958
|
+
'task_id': task_id,
|
|
959
|
+
'customer_name': customer_name,
|
|
960
|
+
'language': language,
|
|
961
|
+
'recipient_address': recipient_address,
|
|
962
|
+
'recipient_name': recipient_name,
|
|
963
|
+
'staff_name': staff_name,
|
|
964
|
+
'team_id': team_id,
|
|
965
|
+
'team_name': team_name,
|
|
966
|
+
'interaction_type': interaction_type,
|
|
967
|
+
'action_status': 'scheduled',
|
|
968
|
+
'current_follow_up_time': follow_up_iteration,
|
|
969
|
+
'draft_id': draft_id,
|
|
970
|
+
'import_uuid': f"{org_id}_{customer_id}_{task_id}_{draft_id}"
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if product_name:
|
|
974
|
+
customextra['product_name'] = product_name
|
|
975
|
+
if draft.get('approach'):
|
|
976
|
+
customextra['approach'] = draft.get('approach')
|
|
977
|
+
if draft.get('mail_tone'):
|
|
978
|
+
customextra['mail_tone'] = draft.get('mail_tone')
|
|
979
|
+
if draft.get('message_type'):
|
|
980
|
+
customextra['message_type'] = draft.get('message_type')
|
|
981
|
+
|
|
982
|
+
return {
|
|
983
|
+
'status': 'published',
|
|
984
|
+
'task': f"FuseSell follow-up {org_id}_{customer_id} - {task_id}",
|
|
985
|
+
'tags': ['fusesell', 'follow-up'],
|
|
986
|
+
'room_id': reminder_room,
|
|
987
|
+
'org_id': org_id,
|
|
988
|
+
'customer_id': customer_id,
|
|
989
|
+
'task_id': task_id,
|
|
990
|
+
'team_id': team_id,
|
|
991
|
+
'team_name': team_name,
|
|
992
|
+
'language': language,
|
|
993
|
+
'customer_name': customer_name,
|
|
994
|
+
'staff_name': staff_name,
|
|
995
|
+
'customextra': customextra
|
|
996
|
+
}
|
|
921
997
|
# Data access methods (similar to initial outreach)
|
|
922
998
|
def _get_customer_data(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
923
999
|
"""Get customer data from previous stages or input."""
|
|
@@ -102,23 +102,32 @@ 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
|
-
|
|
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',
|
|
112
118
|
'email_drafts': saved_drafts,
|
|
113
119
|
'recommended_product': recommended_product,
|
|
114
120
|
'customer_summary': self._create_customer_summary(customer_data),
|
|
115
121
|
'total_drafts_generated': len(saved_drafts),
|
|
116
|
-
'generation_timestamp': datetime.now().isoformat(),
|
|
117
|
-
'customer_id': context.get('execution_id')
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
'generation_timestamp': datetime.now().isoformat(),
|
|
123
|
+
'customer_id': context.get('execution_id')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if schedule_summary:
|
|
127
|
+
outreach_data['reminder_schedule'] = schedule_summary
|
|
128
|
+
|
|
129
|
+
# Save to database
|
|
130
|
+
self.save_stage_result(context, outreach_data)
|
|
122
131
|
|
|
123
132
|
result = self.create_success_result(outreach_data, context)
|
|
124
133
|
return result
|
|
@@ -250,23 +259,30 @@ class InitialOutreachStage(BaseStage):
|
|
|
250
259
|
|
|
251
260
|
input_data = context.get('input_data', {})
|
|
252
261
|
|
|
253
|
-
# Initialize event scheduler
|
|
254
|
-
scheduler = EventScheduler(self.config.get('data_dir', './fusesell_data'))
|
|
255
|
-
|
|
256
|
-
# Check if immediate sending is requested
|
|
257
|
-
send_immediately = input_data.get('send_immediately', False)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
262
|
+
# Initialize event scheduler
|
|
263
|
+
scheduler = EventScheduler(self.config.get('data_dir', './fusesell_data'))
|
|
264
|
+
|
|
265
|
+
# Check if immediate sending is requested
|
|
266
|
+
send_immediately = input_data.get('send_immediately', False)
|
|
267
|
+
reminder_context = self._build_initial_reminder_context(
|
|
268
|
+
draft,
|
|
269
|
+
recipient_address,
|
|
270
|
+
recipient_name,
|
|
271
|
+
context
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Schedule the email event
|
|
275
|
+
schedule_result = scheduler.schedule_email_event(
|
|
276
|
+
draft_id=draft.get('draft_id'),
|
|
277
|
+
recipient_address=recipient_address,
|
|
278
|
+
recipient_name=recipient_name,
|
|
279
|
+
org_id=input_data.get('org_id', 'default'),
|
|
280
|
+
team_id=input_data.get('team_id'),
|
|
281
|
+
customer_timezone=input_data.get('customer_timezone'),
|
|
282
|
+
email_type='initial',
|
|
283
|
+
send_immediately=send_immediately,
|
|
284
|
+
reminder_context=reminder_context
|
|
285
|
+
)
|
|
270
286
|
|
|
271
287
|
if schedule_result['success']:
|
|
272
288
|
self.logger.info(f"Email event scheduled successfully: {schedule_result['event_id']} for {schedule_result['scheduled_time']}")
|
|
@@ -287,18 +303,184 @@ class InitialOutreachStage(BaseStage):
|
|
|
287
303
|
}
|
|
288
304
|
|
|
289
305
|
except Exception as e:
|
|
290
|
-
self.logger.error(f"Email scheduling failed: {str(e)}")
|
|
291
|
-
return {
|
|
292
|
-
'success': False,
|
|
293
|
-
'message': f'Email scheduling failed: {str(e)}',
|
|
294
|
-
'error': str(e)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
def
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
306
|
+
self.logger.error(f"Email scheduling failed: {str(e)}")
|
|
307
|
+
return {
|
|
308
|
+
'success': False,
|
|
309
|
+
'message': f'Email scheduling failed: {str(e)}',
|
|
310
|
+
'error': str(e)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
def _build_initial_reminder_context(
|
|
314
|
+
self,
|
|
315
|
+
draft: Dict[str, Any],
|
|
316
|
+
recipient_address: str,
|
|
317
|
+
recipient_name: str,
|
|
318
|
+
context: Dict[str, Any]
|
|
319
|
+
) -> Dict[str, Any]:
|
|
320
|
+
"""
|
|
321
|
+
Build reminder_task metadata for scheduled initial outreach emails.
|
|
322
|
+
"""
|
|
323
|
+
input_data = context.get('input_data', {})
|
|
324
|
+
org_id = input_data.get('org_id', 'default') or 'default'
|
|
325
|
+
customer_id = input_data.get('customer_id') or context.get('execution_id') or 'unknown'
|
|
326
|
+
task_id = context.get('execution_id') or input_data.get('task_id') or 'unknown_task'
|
|
327
|
+
team_id = input_data.get('team_id')
|
|
328
|
+
team_name = input_data.get('team_name')
|
|
329
|
+
language = input_data.get('language')
|
|
330
|
+
customer_name = input_data.get('customer_name')
|
|
331
|
+
staff_name = input_data.get('staff_name')
|
|
332
|
+
reminder_room = self.config.get('reminder_room_id') or input_data.get('reminder_room_id')
|
|
333
|
+
draft_id = draft.get('draft_id') or 'unknown_draft'
|
|
334
|
+
|
|
335
|
+
customextra = {
|
|
336
|
+
'reminder_content': 'draft_send',
|
|
337
|
+
'org_id': org_id,
|
|
338
|
+
'customer_id': customer_id,
|
|
339
|
+
'task_id': task_id,
|
|
340
|
+
'customer_name': customer_name,
|
|
341
|
+
'language': language,
|
|
342
|
+
'recipient_address': recipient_address,
|
|
343
|
+
'recipient_name': recipient_name,
|
|
344
|
+
'staff_name': staff_name,
|
|
345
|
+
'team_id': team_id,
|
|
346
|
+
'team_name': team_name,
|
|
347
|
+
'interaction_type': input_data.get('interaction_type'),
|
|
348
|
+
'draft_id': draft_id,
|
|
349
|
+
'import_uuid': f"{org_id}_{customer_id}_{task_id}_{draft_id}"
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if draft.get('product_name'):
|
|
353
|
+
customextra['product_name'] = draft.get('product_name')
|
|
354
|
+
if draft.get('approach'):
|
|
355
|
+
customextra['approach'] = draft.get('approach')
|
|
356
|
+
if draft.get('mail_tone'):
|
|
357
|
+
customextra['mail_tone'] = draft.get('mail_tone')
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
'status': 'published',
|
|
361
|
+
'task': f"FuseSell initial outreach {org_id}_{customer_id} - {task_id}",
|
|
362
|
+
'tags': ['fusesell', 'init-outreach'],
|
|
363
|
+
'room_id': reminder_room,
|
|
364
|
+
'org_id': org_id,
|
|
365
|
+
'customer_id': customer_id,
|
|
366
|
+
'task_id': task_id,
|
|
367
|
+
'team_id': team_id,
|
|
368
|
+
'team_name': team_name,
|
|
369
|
+
'language': language,
|
|
370
|
+
'customer_name': customer_name,
|
|
371
|
+
'staff_name': staff_name,
|
|
372
|
+
'customextra': customextra
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
def _schedule_initial_reminder_for_drafts(
|
|
376
|
+
self,
|
|
377
|
+
drafts: List[Dict[str, Any]],
|
|
378
|
+
customer_data: Dict[str, Any],
|
|
379
|
+
context: Dict[str, Any]
|
|
380
|
+
) -> Optional[Dict[str, Any]]:
|
|
381
|
+
"""
|
|
382
|
+
Schedule reminder_task row for the highest-ranked draft after draft generation.
|
|
383
|
+
|
|
384
|
+
Mirrors the server-side behaviour where schedule_auto_run seeds reminder_task
|
|
385
|
+
so RealTimeX automations can pick up pending outreach immediately.
|
|
386
|
+
"""
|
|
387
|
+
if not drafts:
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
input_data = context.get('input_data', {})
|
|
391
|
+
|
|
392
|
+
if input_data.get('send_immediately'):
|
|
393
|
+
self.logger.debug("Skipping reminder scheduling because send_immediately is True")
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
contact_info = customer_data.get('primaryContact', {}) or {}
|
|
397
|
+
|
|
398
|
+
recipient_address = (
|
|
399
|
+
input_data.get('recipient_address')
|
|
400
|
+
or contact_info.get('email')
|
|
401
|
+
or contact_info.get('emailAddress')
|
|
402
|
+
)
|
|
403
|
+
if not recipient_address:
|
|
404
|
+
self.logger.info("Skipping reminder scheduling: recipient email not available")
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
recipient_name = (
|
|
408
|
+
input_data.get('recipient_name')
|
|
409
|
+
or contact_info.get('name')
|
|
410
|
+
or contact_info.get('fullName')
|
|
411
|
+
or ''
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def _draft_sort_key(draft: Dict[str, Any]) -> tuple[int, float]:
|
|
415
|
+
priority = draft.get('priority_order')
|
|
416
|
+
if not isinstance(priority, int):
|
|
417
|
+
priority = 999
|
|
418
|
+
personalization = draft.get('personalization_score', 0)
|
|
419
|
+
try:
|
|
420
|
+
personalization_value = float(personalization)
|
|
421
|
+
except (TypeError, ValueError):
|
|
422
|
+
personalization_value = 0.0
|
|
423
|
+
return (priority, -personalization_value)
|
|
424
|
+
|
|
425
|
+
ordered_drafts = sorted(drafts, key=_draft_sort_key)
|
|
426
|
+
if not ordered_drafts:
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
top_draft = ordered_drafts[0]
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
from ..utils.event_scheduler import EventScheduler
|
|
433
|
+
scheduler = EventScheduler(self.config.get('data_dir', './fusesell_data'))
|
|
434
|
+
except Exception as exc:
|
|
435
|
+
self.logger.warning(
|
|
436
|
+
"Failed to initialise EventScheduler for reminder scheduling: %s",
|
|
437
|
+
exc
|
|
438
|
+
)
|
|
439
|
+
return {'success': False, 'error': str(exc)}
|
|
440
|
+
|
|
441
|
+
reminder_context = self._build_initial_reminder_context(
|
|
442
|
+
top_draft,
|
|
443
|
+
recipient_address,
|
|
444
|
+
recipient_name,
|
|
445
|
+
context
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
schedule_result = scheduler.schedule_email_event(
|
|
450
|
+
draft_id=top_draft.get('draft_id'),
|
|
451
|
+
recipient_address=recipient_address,
|
|
452
|
+
recipient_name=recipient_name,
|
|
453
|
+
org_id=input_data.get('org_id') or self.config.get('org_id', 'default'),
|
|
454
|
+
team_id=input_data.get('team_id') or self.config.get('team_id'),
|
|
455
|
+
customer_timezone=input_data.get('customer_timezone'),
|
|
456
|
+
email_type='initial',
|
|
457
|
+
send_immediately=False,
|
|
458
|
+
reminder_context=reminder_context
|
|
459
|
+
)
|
|
460
|
+
except Exception as exc:
|
|
461
|
+
self.logger.error(f"Initial reminder scheduling failed: {exc}")
|
|
462
|
+
return {'success': False, 'error': str(exc)}
|
|
463
|
+
|
|
464
|
+
if schedule_result.get('success'):
|
|
465
|
+
self.logger.info(
|
|
466
|
+
"Scheduled initial outreach reminder %s for draft %s",
|
|
467
|
+
schedule_result.get('reminder_task_id'),
|
|
468
|
+
top_draft.get('draft_id')
|
|
469
|
+
)
|
|
470
|
+
else:
|
|
471
|
+
self.logger.warning(
|
|
472
|
+
"Reminder scheduling returned failure for draft %s: %s",
|
|
473
|
+
top_draft.get('draft_id'),
|
|
474
|
+
schedule_result.get('error')
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
return schedule_result
|
|
478
|
+
|
|
479
|
+
def _handle_close(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
480
|
+
"""
|
|
481
|
+
Handle close action - Close outreach when customer feels negative.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
302
484
|
context: Execution context
|
|
303
485
|
|
|
304
486
|
Returns:
|