fusesell 1.2.3__py3-none-any.whl → 1.2.5__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.3.dist-info → fusesell-1.2.5.dist-info}/METADATA +4 -2
- {fusesell-1.2.3.dist-info → fusesell-1.2.5.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 +98 -29
- fusesell_local/utils/data_manager.py +214 -28
- fusesell_local/utils/event_scheduler.py +386 -76
- {fusesell-1.2.3.dist-info → fusesell-1.2.5.dist-info}/WHEEL +0 -0
- {fusesell-1.2.3.dist-info → fusesell-1.2.5.dist-info}/entry_points.txt +0 -0
- {fusesell-1.2.3.dist-info → fusesell-1.2.5.dist-info}/licenses/LICENSE +0 -0
- {fusesell-1.2.3.dist-info → fusesell-1.2.5.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fusesell
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
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
|
|
@@ -45,7 +45,9 @@ Dynamic: license-file
|
|
|
45
45
|
|
|
46
46
|
FuseSell Local is a production-ready implementation of the FuseSell AI sales automation system, converted from server-based YAML workflows to a comprehensive Python command-line tool with full data ownership and privacy control.
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
Latest release: `fusesell==1.2.1` is available on PyPI via `pip install fusesell`.
|
|
49
|
+
|
|
50
|
+
Contributors should review the [Repository Guidelines](AGENTS.md) before opening a pull request.
|
|
49
51
|
|
|
50
52
|
## 🚀 Complete Pipeline Overview
|
|
51
53
|
|
|
@@ -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.5.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
|
|
3
|
+
fusesell_local/__init__.py,sha256=_0lDJ4-RcAmyLE81anufpib0EESnvDjc_Q2-IK_xefg,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=Y-WLtIGAg6nRlrCIs91l22giRrraOF6AGc1RlMDNqAM,110236
|
|
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=iG22AHat7rdz_tuhALagOCeHRVaBym6sl041ovVBlw0,185719
|
|
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.5.dist-info/METADATA,sha256=3tPP66Wc1jK9LWT3Bi6_rpK7rg1nxZ-yiSZ7ad2l1V4,35074
|
|
32
|
+
fusesell-1.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
fusesell-1.2.5.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
|
|
34
|
+
fusesell-1.2.5.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
|
|
35
|
+
fusesell-1.2.5.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."""
|
|
@@ -250,23 +250,30 @@ class InitialOutreachStage(BaseStage):
|
|
|
250
250
|
|
|
251
251
|
input_data = context.get('input_data', {})
|
|
252
252
|
|
|
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
|
-
|
|
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
|
+
reminder_context = self._build_initial_reminder_context(
|
|
259
|
+
draft,
|
|
260
|
+
recipient_address,
|
|
261
|
+
recipient_name,
|
|
262
|
+
context
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Schedule the email event
|
|
266
|
+
schedule_result = scheduler.schedule_email_event(
|
|
267
|
+
draft_id=draft.get('draft_id'),
|
|
268
|
+
recipient_address=recipient_address,
|
|
269
|
+
recipient_name=recipient_name,
|
|
270
|
+
org_id=input_data.get('org_id', 'default'),
|
|
271
|
+
team_id=input_data.get('team_id'),
|
|
272
|
+
customer_timezone=input_data.get('customer_timezone'),
|
|
273
|
+
email_type='initial',
|
|
274
|
+
send_immediately=send_immediately,
|
|
275
|
+
reminder_context=reminder_context
|
|
276
|
+
)
|
|
270
277
|
|
|
271
278
|
if schedule_result['success']:
|
|
272
279
|
self.logger.info(f"Email event scheduled successfully: {schedule_result['event_id']} for {schedule_result['scheduled_time']}")
|
|
@@ -287,18 +294,80 @@ class InitialOutreachStage(BaseStage):
|
|
|
287
294
|
}
|
|
288
295
|
|
|
289
296
|
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
|
-
|
|
297
|
+
self.logger.error(f"Email scheduling failed: {str(e)}")
|
|
298
|
+
return {
|
|
299
|
+
'success': False,
|
|
300
|
+
'message': f'Email scheduling failed: {str(e)}',
|
|
301
|
+
'error': str(e)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
def _build_initial_reminder_context(
|
|
305
|
+
self,
|
|
306
|
+
draft: Dict[str, Any],
|
|
307
|
+
recipient_address: str,
|
|
308
|
+
recipient_name: str,
|
|
309
|
+
context: Dict[str, Any]
|
|
310
|
+
) -> Dict[str, Any]:
|
|
311
|
+
"""
|
|
312
|
+
Build reminder_task metadata for scheduled initial outreach emails.
|
|
313
|
+
"""
|
|
314
|
+
input_data = context.get('input_data', {})
|
|
315
|
+
org_id = input_data.get('org_id', 'default') or 'default'
|
|
316
|
+
customer_id = input_data.get('customer_id') or context.get('execution_id') or 'unknown'
|
|
317
|
+
task_id = context.get('execution_id') or input_data.get('task_id') or 'unknown_task'
|
|
318
|
+
team_id = input_data.get('team_id')
|
|
319
|
+
team_name = input_data.get('team_name')
|
|
320
|
+
language = input_data.get('language')
|
|
321
|
+
customer_name = input_data.get('customer_name')
|
|
322
|
+
staff_name = input_data.get('staff_name')
|
|
323
|
+
reminder_room = self.config.get('reminder_room_id') or input_data.get('reminder_room_id')
|
|
324
|
+
draft_id = draft.get('draft_id') or 'unknown_draft'
|
|
325
|
+
|
|
326
|
+
customextra = {
|
|
327
|
+
'reminder_content': 'draft_send',
|
|
328
|
+
'org_id': org_id,
|
|
329
|
+
'customer_id': customer_id,
|
|
330
|
+
'task_id': task_id,
|
|
331
|
+
'customer_name': customer_name,
|
|
332
|
+
'language': language,
|
|
333
|
+
'recipient_address': recipient_address,
|
|
334
|
+
'recipient_name': recipient_name,
|
|
335
|
+
'staff_name': staff_name,
|
|
336
|
+
'team_id': team_id,
|
|
337
|
+
'team_name': team_name,
|
|
338
|
+
'interaction_type': input_data.get('interaction_type'),
|
|
339
|
+
'draft_id': draft_id,
|
|
340
|
+
'import_uuid': f"{org_id}_{customer_id}_{task_id}_{draft_id}"
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if draft.get('product_name'):
|
|
344
|
+
customextra['product_name'] = draft.get('product_name')
|
|
345
|
+
if draft.get('approach'):
|
|
346
|
+
customextra['approach'] = draft.get('approach')
|
|
347
|
+
if draft.get('mail_tone'):
|
|
348
|
+
customextra['mail_tone'] = draft.get('mail_tone')
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
'status': 'published',
|
|
352
|
+
'task': f"FuseSell initial outreach {org_id}_{customer_id} - {task_id}",
|
|
353
|
+
'tags': ['fusesell', 'init-outreach'],
|
|
354
|
+
'room_id': reminder_room,
|
|
355
|
+
'org_id': org_id,
|
|
356
|
+
'customer_id': customer_id,
|
|
357
|
+
'task_id': task_id,
|
|
358
|
+
'team_id': team_id,
|
|
359
|
+
'team_name': team_name,
|
|
360
|
+
'language': language,
|
|
361
|
+
'customer_name': customer_name,
|
|
362
|
+
'staff_name': staff_name,
|
|
363
|
+
'customextra': customextra
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
def _handle_close(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
367
|
+
"""
|
|
368
|
+
Handle close action - Close outreach when customer feels negative.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
302
371
|
context: Execution context
|
|
303
372
|
|
|
304
373
|
Returns:
|
|
@@ -4,10 +4,10 @@ Handles SQLite database operations and local file management
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import sqlite3
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
|
-
import uuid
|
|
10
|
-
from typing import Dict, Any, List, Optional, Union
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import uuid
|
|
10
|
+
from typing import Dict, Any, List, Optional, Sequence, Union
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
import logging
|
|
13
13
|
from pathlib import Path
|
|
@@ -454,11 +454,11 @@ class LocalDataManager:
|
|
|
454
454
|
submission_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
455
455
|
retrieved_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
456
456
|
)
|
|
457
|
-
""")
|
|
458
|
-
|
|
459
|
-
# Create scheduler_rules table (equivalent to gs_scheduler)
|
|
460
|
-
cursor.execute("""
|
|
461
|
-
CREATE TABLE IF NOT EXISTS scheduler_rules (
|
|
457
|
+
""")
|
|
458
|
+
|
|
459
|
+
# Create scheduler_rules table (equivalent to gs_scheduler)
|
|
460
|
+
cursor.execute("""
|
|
461
|
+
CREATE TABLE IF NOT EXISTS scheduler_rules (
|
|
462
462
|
id TEXT PRIMARY KEY,
|
|
463
463
|
org_id TEXT NOT NULL,
|
|
464
464
|
org_name TEXT,
|
|
@@ -478,14 +478,36 @@ class LocalDataManager:
|
|
|
478
478
|
username TEXT,
|
|
479
479
|
fullname TEXT,
|
|
480
480
|
instance_id TEXT,
|
|
481
|
-
submission_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
482
|
-
)
|
|
483
|
-
""")
|
|
484
|
-
|
|
485
|
-
# Create
|
|
486
|
-
cursor.execute("""
|
|
487
|
-
CREATE TABLE IF NOT EXISTS
|
|
488
|
-
id TEXT PRIMARY KEY,
|
|
481
|
+
submission_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
482
|
+
)
|
|
483
|
+
""")
|
|
484
|
+
|
|
485
|
+
# Create reminder_task table (equivalent to Directus reminder_task)
|
|
486
|
+
cursor.execute("""
|
|
487
|
+
CREATE TABLE IF NOT EXISTS reminder_task (
|
|
488
|
+
id TEXT PRIMARY KEY,
|
|
489
|
+
status TEXT NOT NULL,
|
|
490
|
+
task TEXT NOT NULL,
|
|
491
|
+
cron TEXT NOT NULL,
|
|
492
|
+
room_id TEXT,
|
|
493
|
+
tags TEXT,
|
|
494
|
+
customextra TEXT,
|
|
495
|
+
org_id TEXT,
|
|
496
|
+
customer_id TEXT,
|
|
497
|
+
task_id TEXT,
|
|
498
|
+
import_uuid TEXT,
|
|
499
|
+
scheduled_time TIMESTAMP,
|
|
500
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
501
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
502
|
+
executed_at TIMESTAMP,
|
|
503
|
+
error_message TEXT
|
|
504
|
+
)
|
|
505
|
+
""")
|
|
506
|
+
|
|
507
|
+
# Create extracted_files table (equivalent to gs_plan_setting_extracted_file)
|
|
508
|
+
cursor.execute("""
|
|
509
|
+
CREATE TABLE IF NOT EXISTS extracted_files (
|
|
510
|
+
id TEXT PRIMARY KEY,
|
|
489
511
|
org_id TEXT NOT NULL,
|
|
490
512
|
plan_id TEXT,
|
|
491
513
|
team_id TEXT,
|
|
@@ -585,17 +607,25 @@ class LocalDataManager:
|
|
|
585
607
|
"CREATE INDEX IF NOT EXISTS idx_team_settings_team_id ON team_settings(team_id)")
|
|
586
608
|
cursor.execute(
|
|
587
609
|
"CREATE INDEX IF NOT EXISTS idx_products_org_id ON products(org_id)")
|
|
588
|
-
cursor.execute(
|
|
589
|
-
"CREATE INDEX IF NOT EXISTS idx_gs_customer_llmtask_task_id ON gs_customer_llmtask(task_id)")
|
|
590
|
-
cursor.execute(
|
|
591
|
-
"CREATE INDEX IF NOT EXISTS idx_prompts_org_id ON prompts(org_id)")
|
|
592
|
-
cursor.execute(
|
|
593
|
-
"CREATE INDEX IF NOT EXISTS idx_scheduler_rules_org_id ON scheduler_rules(org_id)")
|
|
594
|
-
cursor.execute(
|
|
595
|
-
"CREATE INDEX IF NOT EXISTS
|
|
596
|
-
cursor.execute(
|
|
597
|
-
"CREATE INDEX IF NOT EXISTS
|
|
598
|
-
cursor.execute(
|
|
610
|
+
cursor.execute(
|
|
611
|
+
"CREATE INDEX IF NOT EXISTS idx_gs_customer_llmtask_task_id ON gs_customer_llmtask(task_id)")
|
|
612
|
+
cursor.execute(
|
|
613
|
+
"CREATE INDEX IF NOT EXISTS idx_prompts_org_id ON prompts(org_id)")
|
|
614
|
+
cursor.execute(
|
|
615
|
+
"CREATE INDEX IF NOT EXISTS idx_scheduler_rules_org_id ON scheduler_rules(org_id)")
|
|
616
|
+
cursor.execute(
|
|
617
|
+
"CREATE INDEX IF NOT EXISTS idx_reminder_task_status ON reminder_task(status)")
|
|
618
|
+
cursor.execute(
|
|
619
|
+
"CREATE INDEX IF NOT EXISTS idx_reminder_task_org_id ON reminder_task(org_id)")
|
|
620
|
+
cursor.execute(
|
|
621
|
+
"CREATE INDEX IF NOT EXISTS idx_reminder_task_task_id ON reminder_task(task_id)")
|
|
622
|
+
cursor.execute(
|
|
623
|
+
"CREATE INDEX IF NOT EXISTS idx_reminder_task_cron ON reminder_task(cron)")
|
|
624
|
+
cursor.execute(
|
|
625
|
+
"CREATE INDEX IF NOT EXISTS idx_extracted_files_org_id ON extracted_files(org_id)")
|
|
626
|
+
cursor.execute(
|
|
627
|
+
"CREATE INDEX IF NOT EXISTS idx_llm_worker_plan_org_id ON llm_worker_plan(org_id)")
|
|
628
|
+
cursor.execute(
|
|
599
629
|
"CREATE INDEX IF NOT EXISTS idx_gs_company_criteria_org_id ON gs_company_criteria(org_id)")
|
|
600
630
|
|
|
601
631
|
# Create compatibility views for backward compatibility
|
|
@@ -1408,6 +1438,162 @@ class LocalDataManager:
|
|
|
1408
1438
|
self.logger.error(f"Failed to get team settings: {str(e)}")
|
|
1409
1439
|
raise
|
|
1410
1440
|
|
|
1441
|
+
def build_team_settings_snapshot(
|
|
1442
|
+
self,
|
|
1443
|
+
team_id: str,
|
|
1444
|
+
sections: Optional[Sequence[str]] = None
|
|
1445
|
+
) -> Dict[str, Any]:
|
|
1446
|
+
"""
|
|
1447
|
+
Build a response payload containing team settings in the expected RealTimeX format.
|
|
1448
|
+
|
|
1449
|
+
Args:
|
|
1450
|
+
team_id: Team identifier
|
|
1451
|
+
sections: Optional sequence of section names to include. Accepts either
|
|
1452
|
+
full keys (e.g. ``gs_team_product``) or shorthand without the prefix.
|
|
1453
|
+
|
|
1454
|
+
Returns:
|
|
1455
|
+
Dictionary shaped as ``{"data": [{...}]}``. When no settings exist,
|
|
1456
|
+
returns ``{"data": []}``.
|
|
1457
|
+
"""
|
|
1458
|
+
settings = self.get_team_settings(team_id)
|
|
1459
|
+
if not settings:
|
|
1460
|
+
return {"data": []}
|
|
1461
|
+
|
|
1462
|
+
available_fields = [
|
|
1463
|
+
'gs_team_organization',
|
|
1464
|
+
'gs_team_rep',
|
|
1465
|
+
'gs_team_product',
|
|
1466
|
+
'gs_team_schedule_time',
|
|
1467
|
+
'gs_team_initial_outreach',
|
|
1468
|
+
'gs_team_follow_up',
|
|
1469
|
+
'gs_team_auto_interaction',
|
|
1470
|
+
'gs_team_followup_schedule_time',
|
|
1471
|
+
'gs_team_birthday_email',
|
|
1472
|
+
]
|
|
1473
|
+
|
|
1474
|
+
if sections:
|
|
1475
|
+
normalized = set()
|
|
1476
|
+
for item in sections:
|
|
1477
|
+
if not item:
|
|
1478
|
+
continue
|
|
1479
|
+
item = item.strip()
|
|
1480
|
+
if not item:
|
|
1481
|
+
continue
|
|
1482
|
+
if item.startswith("gs_team_"):
|
|
1483
|
+
normalized.add(item)
|
|
1484
|
+
else:
|
|
1485
|
+
normalized.add(f"gs_team_{item}")
|
|
1486
|
+
fields_to_include = [field for field in available_fields if field in normalized]
|
|
1487
|
+
else:
|
|
1488
|
+
fields_to_include = available_fields
|
|
1489
|
+
|
|
1490
|
+
list_like_fields = {
|
|
1491
|
+
'gs_team_organization',
|
|
1492
|
+
'gs_team_rep',
|
|
1493
|
+
'gs_team_product',
|
|
1494
|
+
'gs_team_auto_interaction',
|
|
1495
|
+
}
|
|
1496
|
+
list_field_defaults = {
|
|
1497
|
+
'gs_team_organization': {
|
|
1498
|
+
'org_name': None,
|
|
1499
|
+
'address': None,
|
|
1500
|
+
'website': None,
|
|
1501
|
+
'industry': None,
|
|
1502
|
+
'description': None,
|
|
1503
|
+
'logo': None,
|
|
1504
|
+
'primary_email': None,
|
|
1505
|
+
'primary_phone': None,
|
|
1506
|
+
'primary_color': None,
|
|
1507
|
+
'is_active': False,
|
|
1508
|
+
'avg_rating': None,
|
|
1509
|
+
'total_sales': None,
|
|
1510
|
+
'total_products': None,
|
|
1511
|
+
'date_joined': None,
|
|
1512
|
+
'last_active': None,
|
|
1513
|
+
'social_media_links': [],
|
|
1514
|
+
},
|
|
1515
|
+
'gs_team_rep': {
|
|
1516
|
+
'name': None,
|
|
1517
|
+
'email': None,
|
|
1518
|
+
'phone': None,
|
|
1519
|
+
'position': None,
|
|
1520
|
+
'website': None,
|
|
1521
|
+
'logo': None,
|
|
1522
|
+
'username': None,
|
|
1523
|
+
'is_primary': False,
|
|
1524
|
+
'primary_color': None,
|
|
1525
|
+
'primary_phone': None,
|
|
1526
|
+
},
|
|
1527
|
+
'gs_team_product': {
|
|
1528
|
+
'product_id': None,
|
|
1529
|
+
'product_name': None,
|
|
1530
|
+
'image_url': None,
|
|
1531
|
+
'enabled': True,
|
|
1532
|
+
'priority': None,
|
|
1533
|
+
},
|
|
1534
|
+
'gs_team_auto_interaction': {
|
|
1535
|
+
'from_email': '',
|
|
1536
|
+
'from_name': '',
|
|
1537
|
+
'from_number': '',
|
|
1538
|
+
'tool_type': 'Email',
|
|
1539
|
+
'email_cc': '',
|
|
1540
|
+
'email_bcc': '',
|
|
1541
|
+
},
|
|
1542
|
+
}
|
|
1543
|
+
alias_fields = {
|
|
1544
|
+
'gs_team_organization': {
|
|
1545
|
+
'name': 'org_name',
|
|
1546
|
+
'brand_palette': 'primary_color',
|
|
1547
|
+
},
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
snapshot: Dict[str, Any] = {}
|
|
1551
|
+
for field in fields_to_include:
|
|
1552
|
+
value = settings.get(field)
|
|
1553
|
+
if value is None:
|
|
1554
|
+
continue
|
|
1555
|
+
|
|
1556
|
+
if field in list_like_fields:
|
|
1557
|
+
if isinstance(value, list):
|
|
1558
|
+
normalized_items = []
|
|
1559
|
+
defaults = list_field_defaults.get(field, {})
|
|
1560
|
+
aliases = alias_fields.get(field, {})
|
|
1561
|
+
for item in value:
|
|
1562
|
+
if not isinstance(item, dict):
|
|
1563
|
+
continue
|
|
1564
|
+
normalized = {}
|
|
1565
|
+
for key, default_val in defaults.items():
|
|
1566
|
+
if key == 'social_media_links':
|
|
1567
|
+
current = item.get(key)
|
|
1568
|
+
normalized[key] = current if isinstance(current, list) else []
|
|
1569
|
+
else:
|
|
1570
|
+
normalized[key] = item.get(key, default_val)
|
|
1571
|
+
for legacy_key, target_key in aliases.items():
|
|
1572
|
+
if normalized.get(target_key) in (None, '', []):
|
|
1573
|
+
if legacy_key in item:
|
|
1574
|
+
normalized[target_key] = item[legacy_key]
|
|
1575
|
+
# include any additional keys that might exist
|
|
1576
|
+
normalized_items.append(normalized)
|
|
1577
|
+
snapshot[field] = normalized_items
|
|
1578
|
+
elif value:
|
|
1579
|
+
defaults = list_field_defaults.get(field, {})
|
|
1580
|
+
aliases = alias_fields.get(field, {})
|
|
1581
|
+
normalized = {key: value.get(key, default_val) for key, default_val in defaults.items()}
|
|
1582
|
+
for legacy_key, target_key in aliases.items():
|
|
1583
|
+
if normalized.get(target_key) in (None, '', []):
|
|
1584
|
+
if legacy_key in value:
|
|
1585
|
+
normalized[target_key] = value[legacy_key]
|
|
1586
|
+
snapshot[field] = [normalized]
|
|
1587
|
+
else:
|
|
1588
|
+
snapshot[field] = []
|
|
1589
|
+
else:
|
|
1590
|
+
snapshot[field] = value
|
|
1591
|
+
|
|
1592
|
+
if not snapshot:
|
|
1593
|
+
return {"data": []}
|
|
1594
|
+
|
|
1595
|
+
return {"data": [snapshot]}
|
|
1596
|
+
|
|
1411
1597
|
def _deserialize_product_row(self, row: sqlite3.Row) -> Dict[str, Any]:
|
|
1412
1598
|
"""
|
|
1413
1599
|
Convert a product row into a dictionary with JSON fields parsed.
|
|
@@ -3,14 +3,14 @@ Event Scheduler - Database-based event scheduling system
|
|
|
3
3
|
Creates scheduled events in database for external app to handle
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import logging
|
|
7
|
-
from datetime import datetime, timedelta
|
|
8
|
-
from typing import Dict, Any, Optional, List
|
|
9
|
-
import pytz
|
|
10
|
-
import json
|
|
11
|
-
import sqlite3
|
|
12
|
-
import uuid
|
|
13
|
-
from pathlib import Path
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from typing import Dict, Any, Optional, List, Union
|
|
9
|
+
import pytz
|
|
10
|
+
import json
|
|
11
|
+
import sqlite3
|
|
12
|
+
import uuid
|
|
13
|
+
from pathlib import Path
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class EventScheduler:
|
|
@@ -73,15 +73,53 @@ class EventScheduler:
|
|
|
73
73
|
ON scheduled_events(scheduled_time, status)
|
|
74
74
|
""")
|
|
75
75
|
|
|
76
|
-
cursor.execute("""
|
|
77
|
-
CREATE INDEX IF NOT EXISTS idx_scheduled_events_org_team
|
|
78
|
-
ON scheduled_events(org_id, team_id)
|
|
79
|
-
""")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
cursor.execute("""
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_events_org_team
|
|
78
|
+
ON scheduled_events(org_id, team_id)
|
|
79
|
+
""")
|
|
80
|
+
|
|
81
|
+
cursor.execute("""
|
|
82
|
+
CREATE TABLE IF NOT EXISTS reminder_task (
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
status TEXT NOT NULL,
|
|
85
|
+
task TEXT NOT NULL,
|
|
86
|
+
cron TEXT NOT NULL,
|
|
87
|
+
room_id TEXT,
|
|
88
|
+
tags TEXT,
|
|
89
|
+
customextra TEXT,
|
|
90
|
+
org_id TEXT,
|
|
91
|
+
customer_id TEXT,
|
|
92
|
+
task_id TEXT,
|
|
93
|
+
import_uuid TEXT,
|
|
94
|
+
scheduled_time TIMESTAMP,
|
|
95
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
96
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
97
|
+
executed_at TIMESTAMP,
|
|
98
|
+
error_message TEXT
|
|
99
|
+
)
|
|
100
|
+
""")
|
|
101
|
+
|
|
102
|
+
cursor.execute("""
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_reminder_task_status
|
|
104
|
+
ON reminder_task(status)
|
|
105
|
+
""")
|
|
106
|
+
cursor.execute("""
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_reminder_task_org_id
|
|
108
|
+
ON reminder_task(org_id)
|
|
109
|
+
""")
|
|
110
|
+
cursor.execute("""
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_reminder_task_task_id
|
|
112
|
+
ON reminder_task(task_id)
|
|
113
|
+
""")
|
|
114
|
+
cursor.execute("""
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_reminder_task_cron
|
|
116
|
+
ON reminder_task(cron)
|
|
117
|
+
""")
|
|
118
|
+
|
|
119
|
+
conn.commit()
|
|
120
|
+
conn.close()
|
|
121
|
+
|
|
122
|
+
self.logger.info("Scheduled events database initialized")
|
|
85
123
|
|
|
86
124
|
except Exception as e:
|
|
87
125
|
self.logger.error(f"Failed to initialize scheduled events DB: {str(e)}")
|
|
@@ -122,15 +160,225 @@ class EventScheduler:
|
|
|
122
160
|
conn.commit()
|
|
123
161
|
conn.close()
|
|
124
162
|
|
|
125
|
-
self.logger.info("Scheduling rules database initialized")
|
|
126
|
-
|
|
127
|
-
except Exception as e:
|
|
128
|
-
self.logger.error(f"Failed to initialize scheduling rules DB: {str(e)}")
|
|
129
|
-
raise
|
|
163
|
+
self.logger.info("Scheduling rules database initialized")
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
self.logger.error(f"Failed to initialize scheduling rules DB: {str(e)}")
|
|
167
|
+
raise
|
|
168
|
+
|
|
169
|
+
def _format_datetime(self, value: Union[str, datetime, None]) -> str:
|
|
170
|
+
"""
|
|
171
|
+
Normalize datetime-like values to ISO 8601 strings.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
value: Datetime, ISO string, or None.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
ISO 8601 formatted string.
|
|
178
|
+
"""
|
|
179
|
+
if isinstance(value, datetime):
|
|
180
|
+
return value.isoformat()
|
|
181
|
+
if value is None:
|
|
182
|
+
return datetime.utcnow().isoformat()
|
|
183
|
+
|
|
184
|
+
value_str = str(value).strip()
|
|
185
|
+
if not value_str:
|
|
186
|
+
return datetime.utcnow().isoformat()
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
parsed = datetime.fromisoformat(value_str)
|
|
190
|
+
return parsed.isoformat()
|
|
191
|
+
except ValueError:
|
|
192
|
+
return value_str
|
|
193
|
+
|
|
194
|
+
def _build_reminder_payload(
|
|
195
|
+
self,
|
|
196
|
+
base_context: Dict[str, Any],
|
|
197
|
+
*,
|
|
198
|
+
event_id: str,
|
|
199
|
+
send_time: datetime,
|
|
200
|
+
email_type: str,
|
|
201
|
+
org_id: str,
|
|
202
|
+
recipient_address: str,
|
|
203
|
+
recipient_name: str,
|
|
204
|
+
draft_id: str,
|
|
205
|
+
customer_timezone: str,
|
|
206
|
+
follow_up_iteration: Optional[int] = None
|
|
207
|
+
) -> Optional[Dict[str, Any]]:
|
|
208
|
+
"""
|
|
209
|
+
Construct reminder_task payload mirroring server implementation.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
base_context: Context data supplied by caller.
|
|
213
|
+
event_id: Scheduled event identifier.
|
|
214
|
+
send_time: Planned send time (UTC).
|
|
215
|
+
email_type: 'initial' or 'follow_up'.
|
|
216
|
+
org_id: Organization identifier.
|
|
217
|
+
recipient_address: Recipient email.
|
|
218
|
+
recipient_name: Recipient name.
|
|
219
|
+
draft_id: Draft identifier.
|
|
220
|
+
customer_timezone: Customer timezone.
|
|
221
|
+
follow_up_iteration: Optional follow-up iteration counter.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Reminder payload dictionary or None if insufficient data.
|
|
225
|
+
"""
|
|
226
|
+
if not base_context:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
context = dict(base_context)
|
|
230
|
+
customextra_raw = context.pop('customextra', {}) or {}
|
|
231
|
+
if isinstance(customextra_raw, str):
|
|
232
|
+
try:
|
|
233
|
+
customextra = json.loads(customextra_raw)
|
|
234
|
+
except (json.JSONDecodeError, TypeError):
|
|
235
|
+
customextra = {}
|
|
236
|
+
elif isinstance(customextra_raw, dict):
|
|
237
|
+
customextra = dict(customextra_raw)
|
|
238
|
+
else:
|
|
239
|
+
customextra = {}
|
|
240
|
+
|
|
241
|
+
status = context.pop('status', 'published') or 'published'
|
|
242
|
+
cron_value = context.pop('cron', None)
|
|
243
|
+
scheduled_time_value = context.pop('scheduled_time', None)
|
|
244
|
+
room_id = context.pop('room_id', context.pop('room', None))
|
|
245
|
+
tags = context.pop('tags', None)
|
|
246
|
+
task_label = context.pop('task', None)
|
|
247
|
+
org_id_override = context.pop('org_id', None) or org_id
|
|
248
|
+
customer_id = context.pop('customer_id', None) or customextra.get('customer_id')
|
|
249
|
+
task_id = context.pop('task_id', None) or context.pop('execution_id', None) or customextra.get('task_id')
|
|
250
|
+
customer_name = context.pop('customer_name', None)
|
|
251
|
+
language = context.pop('language', None)
|
|
252
|
+
team_id = context.pop('team_id', None)
|
|
253
|
+
team_name = context.pop('team_name', None)
|
|
254
|
+
staff_name = context.pop('staff_name', None)
|
|
255
|
+
import_uuid = context.pop('import_uuid', None) or customextra.get('import_uuid')
|
|
256
|
+
|
|
257
|
+
customextra.setdefault('reminder_content', 'draft_send' if email_type == 'initial' else 'follow_up')
|
|
258
|
+
customextra.setdefault('org_id', org_id_override)
|
|
259
|
+
customextra.setdefault('customer_id', customer_id)
|
|
260
|
+
customextra.setdefault('task_id', task_id)
|
|
261
|
+
customextra.setdefault('event_id', event_id)
|
|
262
|
+
customextra.setdefault('email_type', email_type)
|
|
263
|
+
customextra.setdefault('recipient_address', recipient_address)
|
|
264
|
+
customextra.setdefault('recipient_name', recipient_name)
|
|
265
|
+
customextra.setdefault('draft_id', draft_id)
|
|
266
|
+
customextra.setdefault('customer_timezone', customer_timezone)
|
|
267
|
+
customextra.setdefault('scheduled_time_utc', send_time.isoformat())
|
|
268
|
+
|
|
269
|
+
if team_id and 'team_id' not in customextra:
|
|
270
|
+
customextra['team_id'] = team_id
|
|
271
|
+
if team_name and 'team_name' not in customextra:
|
|
272
|
+
customextra['team_name'] = team_name
|
|
273
|
+
if language and 'language' not in customextra:
|
|
274
|
+
customextra['language'] = language
|
|
275
|
+
if staff_name and 'staff_name' not in customextra:
|
|
276
|
+
customextra['staff_name'] = staff_name
|
|
277
|
+
if customer_name and 'customer_name' not in customextra:
|
|
278
|
+
customextra['customer_name'] = customer_name
|
|
279
|
+
|
|
280
|
+
iteration = follow_up_iteration or context.pop('current_follow_up_time', None)
|
|
281
|
+
if iteration is not None and 'current_follow_up_time' not in customextra:
|
|
282
|
+
customextra['current_follow_up_time'] = iteration
|
|
283
|
+
|
|
284
|
+
if not import_uuid:
|
|
285
|
+
import_uuid = f"{customextra.get('org_id', '')}_{customextra.get('customer_id', '')}_{customextra.get('task_id', '')}_{event_id}"
|
|
286
|
+
customextra.setdefault('import_uuid', import_uuid)
|
|
287
|
+
|
|
288
|
+
if not tags:
|
|
289
|
+
tags = ['fusesell', 'init-outreach' if email_type == 'initial' else 'follow-up']
|
|
290
|
+
|
|
291
|
+
if not task_label:
|
|
292
|
+
readable_type = "Initial Outreach" if email_type == 'initial' else "Follow-up"
|
|
293
|
+
identifier = customextra.get('customer_name') or customextra.get('customer_id') or customer_id or 'customer'
|
|
294
|
+
tracking_id = customextra.get('task_id') or task_id or draft_id
|
|
295
|
+
task_label = f"FuseSell {readable_type} {identifier} - {tracking_id}"
|
|
296
|
+
|
|
297
|
+
cron_value = self._format_datetime(cron_value or send_time)
|
|
298
|
+
scheduled_time_str = self._format_datetime(scheduled_time_value or send_time)
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
'status': status,
|
|
302
|
+
'task': task_label,
|
|
303
|
+
'cron': cron_value,
|
|
304
|
+
'room_id': room_id,
|
|
305
|
+
'tags': tags,
|
|
306
|
+
'customextra': customextra,
|
|
307
|
+
'org_id': customextra.get('org_id'),
|
|
308
|
+
'customer_id': customextra.get('customer_id'),
|
|
309
|
+
'task_id': customextra.get('task_id'),
|
|
310
|
+
'import_uuid': customextra.get('import_uuid'),
|
|
311
|
+
'scheduled_time': scheduled_time_str
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
def _insert_reminder_task(self, payload: Optional[Dict[str, Any]]) -> Optional[str]:
|
|
315
|
+
"""
|
|
316
|
+
Insert reminder_task record into local database.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
payload: Reminder payload produced by _build_reminder_payload.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Reminder task identifier or None on failure.
|
|
323
|
+
"""
|
|
324
|
+
if not payload:
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
reminder_id = payload.get('id') or f"uuid:{str(uuid.uuid4())}"
|
|
329
|
+
|
|
330
|
+
tags_value = payload.get('tags')
|
|
331
|
+
if isinstance(tags_value, (list, tuple)):
|
|
332
|
+
tags_str = json.dumps(list(tags_value))
|
|
333
|
+
elif isinstance(tags_value, str):
|
|
334
|
+
tags_str = tags_value
|
|
335
|
+
else:
|
|
336
|
+
tags_str = json.dumps([])
|
|
337
|
+
|
|
338
|
+
customextra_value = payload.get('customextra') or {}
|
|
339
|
+
if isinstance(customextra_value, dict):
|
|
340
|
+
customextra_str = json.dumps(customextra_value)
|
|
341
|
+
elif isinstance(customextra_value, str):
|
|
342
|
+
customextra_str = customextra_value
|
|
343
|
+
else:
|
|
344
|
+
customextra_str = json.dumps({})
|
|
345
|
+
|
|
346
|
+
conn = sqlite3.connect(self.main_db_path)
|
|
347
|
+
cursor = conn.cursor()
|
|
348
|
+
|
|
349
|
+
cursor.execute("""
|
|
350
|
+
INSERT INTO reminder_task
|
|
351
|
+
(id, status, task, cron, room_id, tags, customextra, org_id, customer_id, task_id, import_uuid, scheduled_time)
|
|
352
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
353
|
+
""", (
|
|
354
|
+
reminder_id,
|
|
355
|
+
payload.get('status', 'published'),
|
|
356
|
+
payload.get('task') or 'FuseSell Reminder',
|
|
357
|
+
self._format_datetime(payload.get('cron')),
|
|
358
|
+
payload.get('room_id'),
|
|
359
|
+
tags_str,
|
|
360
|
+
customextra_str,
|
|
361
|
+
payload.get('org_id'),
|
|
362
|
+
payload.get('customer_id'),
|
|
363
|
+
payload.get('task_id'),
|
|
364
|
+
payload.get('import_uuid'),
|
|
365
|
+
self._format_datetime(payload.get('scheduled_time'))
|
|
366
|
+
))
|
|
367
|
+
|
|
368
|
+
conn.commit()
|
|
369
|
+
conn.close()
|
|
370
|
+
|
|
371
|
+
self.logger.debug(f"Created reminder_task record {reminder_id}")
|
|
372
|
+
return reminder_id
|
|
373
|
+
|
|
374
|
+
except Exception as exc:
|
|
375
|
+
self.logger.error(f"Failed to create reminder_task record: {str(exc)}")
|
|
376
|
+
return None
|
|
130
377
|
|
|
131
|
-
def schedule_email_event(self, draft_id: str, recipient_address: str, recipient_name: str,
|
|
132
|
-
org_id: str, team_id: str = None, customer_timezone: str = None,
|
|
133
|
-
email_type: str = 'initial', send_immediately: bool = False
|
|
378
|
+
def schedule_email_event(self, draft_id: str, recipient_address: str, recipient_name: str,
|
|
379
|
+
org_id: str, team_id: str = None, customer_timezone: str = None,
|
|
380
|
+
email_type: str = 'initial', send_immediately: bool = False,
|
|
381
|
+
reminder_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
134
382
|
"""
|
|
135
383
|
Schedule an email event in the database for external app to handle.
|
|
136
384
|
|
|
@@ -142,7 +390,8 @@ class EventScheduler:
|
|
|
142
390
|
team_id: Team ID (optional)
|
|
143
391
|
customer_timezone: Customer's timezone (optional)
|
|
144
392
|
email_type: Type of email ('initial' or 'follow_up')
|
|
145
|
-
send_immediately: If True, schedule for immediate sending
|
|
393
|
+
send_immediately: If True, schedule for immediate sending
|
|
394
|
+
reminder_context: Optional metadata for reminder_task mirroring server behaviour
|
|
146
395
|
|
|
147
396
|
Returns:
|
|
148
397
|
Event creation result with event ID and scheduled time
|
|
@@ -180,40 +429,82 @@ class EventScheduler:
|
|
|
180
429
|
conn = sqlite3.connect(self.main_db_path)
|
|
181
430
|
cursor = conn.cursor()
|
|
182
431
|
|
|
183
|
-
cursor.execute("""
|
|
184
|
-
INSERT INTO scheduled_events
|
|
185
|
-
(id, event_id, event_type, scheduled_time, org_id, team_id, draft_id,
|
|
186
|
-
recipient_address, recipient_name, customer_timezone, event_data)
|
|
187
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
432
|
+
cursor.execute("""
|
|
433
|
+
INSERT INTO scheduled_events
|
|
434
|
+
(id, event_id, event_type, scheduled_time, org_id, team_id, draft_id,
|
|
435
|
+
recipient_address, recipient_name, customer_timezone, event_data)
|
|
436
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
188
437
|
""", (
|
|
189
438
|
f"uuid:{str(uuid.uuid4())}", event_id, 'email_send', send_time, org_id, team_id, draft_id,
|
|
190
439
|
recipient_address, recipient_name, customer_timezone, json.dumps(event_data)
|
|
191
440
|
))
|
|
192
441
|
|
|
193
|
-
conn.commit()
|
|
194
|
-
conn.close()
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
442
|
+
conn.commit()
|
|
443
|
+
conn.close()
|
|
444
|
+
|
|
445
|
+
reminder_task_id = None
|
|
446
|
+
if reminder_context:
|
|
447
|
+
reminder_payload = self._build_reminder_payload(
|
|
448
|
+
dict(reminder_context),
|
|
449
|
+
event_id=event_id,
|
|
450
|
+
send_time=send_time,
|
|
451
|
+
email_type=email_type,
|
|
452
|
+
org_id=org_id,
|
|
453
|
+
recipient_address=recipient_address,
|
|
454
|
+
recipient_name=recipient_name,
|
|
455
|
+
draft_id=draft_id,
|
|
456
|
+
customer_timezone=customer_timezone
|
|
457
|
+
)
|
|
458
|
+
reminder_task_id = self._insert_reminder_task(reminder_payload)
|
|
459
|
+
|
|
460
|
+
# Log the scheduling
|
|
461
|
+
self.logger.info(f"Scheduled email event {event_id} for {send_time} (draft: {draft_id})")
|
|
462
|
+
|
|
463
|
+
# Schedule follow-up if this is an initial email
|
|
464
|
+
follow_up_event_id = None
|
|
465
|
+
follow_up_reminder_id = None
|
|
466
|
+
if email_type == 'initial' and not send_immediately:
|
|
467
|
+
follow_up_context = None
|
|
468
|
+
if reminder_context:
|
|
469
|
+
follow_up_context = dict(reminder_context)
|
|
470
|
+
follow_up_extra = dict(follow_up_context.get('customextra', {}) or {})
|
|
471
|
+
follow_up_extra['reminder_content'] = 'follow_up'
|
|
472
|
+
follow_up_extra.setdefault('current_follow_up_time', 1)
|
|
473
|
+
follow_up_context['customextra'] = follow_up_extra
|
|
474
|
+
follow_up_context['tags'] = follow_up_context.get('tags') or ['fusesell', 'follow-up']
|
|
475
|
+
|
|
476
|
+
follow_up_result = self._schedule_follow_up_event(
|
|
477
|
+
draft_id,
|
|
478
|
+
recipient_address,
|
|
479
|
+
recipient_name,
|
|
480
|
+
org_id,
|
|
481
|
+
team_id,
|
|
482
|
+
customer_timezone,
|
|
483
|
+
reminder_context=follow_up_context
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
if follow_up_result.get('success'):
|
|
487
|
+
follow_up_event_id = follow_up_result.get('event_id')
|
|
488
|
+
follow_up_reminder_id = follow_up_result.get('reminder_task_id')
|
|
489
|
+
else:
|
|
490
|
+
self.logger.warning(
|
|
491
|
+
"Follow-up scheduling failed for event %s: %s",
|
|
492
|
+
event_id,
|
|
493
|
+
follow_up_result.get('error', 'unknown error')
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
'success': True,
|
|
498
|
+
'event_id': event_id,
|
|
499
|
+
'scheduled_time': send_time.isoformat(),
|
|
500
|
+
'recipient_address': recipient_address,
|
|
501
|
+
'recipient_name': recipient_name,
|
|
502
|
+
'draft_id': draft_id,
|
|
503
|
+
'email_type': email_type,
|
|
504
|
+
'reminder_task_id': reminder_task_id,
|
|
505
|
+
'follow_up_event_id': follow_up_event_id,
|
|
506
|
+
'follow_up_reminder_task_id': follow_up_reminder_id
|
|
507
|
+
}
|
|
217
508
|
|
|
218
509
|
except Exception as e:
|
|
219
510
|
self.logger.error(f"Failed to schedule email event: {str(e)}")
|
|
@@ -222,9 +513,10 @@ class EventScheduler:
|
|
|
222
513
|
'error': str(e)
|
|
223
514
|
}
|
|
224
515
|
|
|
225
|
-
def _schedule_follow_up_event(self, original_draft_id: str, recipient_address: str,
|
|
226
|
-
recipient_name: str, org_id: str, team_id: str = None,
|
|
227
|
-
customer_timezone: str = None
|
|
516
|
+
def _schedule_follow_up_event(self, original_draft_id: str, recipient_address: str,
|
|
517
|
+
recipient_name: str, org_id: str, team_id: str = None,
|
|
518
|
+
customer_timezone: str = None,
|
|
519
|
+
reminder_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
228
520
|
"""
|
|
229
521
|
Schedule follow-up email event after initial email.
|
|
230
522
|
|
|
@@ -234,7 +526,8 @@ class EventScheduler:
|
|
|
234
526
|
recipient_name: Name of recipient
|
|
235
527
|
org_id: Organization ID
|
|
236
528
|
team_id: Team ID (optional)
|
|
237
|
-
customer_timezone: Customer's timezone (optional)
|
|
529
|
+
customer_timezone: Customer's timezone (optional)
|
|
530
|
+
reminder_context: Optional metadata for reminder_task rows
|
|
238
531
|
|
|
239
532
|
Returns:
|
|
240
533
|
Follow-up event creation result
|
|
@@ -275,24 +568,41 @@ class EventScheduler:
|
|
|
275
568
|
original_draft_id, recipient_address, recipient_name,
|
|
276
569
|
customer_timezone, json.dumps(event_data)
|
|
277
570
|
))
|
|
278
|
-
|
|
279
|
-
conn.commit()
|
|
280
|
-
conn.close()
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
571
|
+
|
|
572
|
+
conn.commit()
|
|
573
|
+
conn.close()
|
|
574
|
+
|
|
575
|
+
reminder_task_id = None
|
|
576
|
+
if reminder_context:
|
|
577
|
+
reminder_payload = self._build_reminder_payload(
|
|
578
|
+
dict(reminder_context),
|
|
579
|
+
event_id=followup_event_id,
|
|
580
|
+
send_time=follow_up_time,
|
|
581
|
+
email_type='follow_up',
|
|
582
|
+
org_id=org_id,
|
|
583
|
+
recipient_address=recipient_address,
|
|
584
|
+
recipient_name=recipient_name,
|
|
585
|
+
draft_id=original_draft_id,
|
|
586
|
+
customer_timezone=event_data['customer_timezone']
|
|
587
|
+
)
|
|
588
|
+
reminder_task_id = self._insert_reminder_task(reminder_payload)
|
|
589
|
+
|
|
590
|
+
self.logger.info(f"Scheduled follow-up event {followup_event_id} for {follow_up_time}")
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
'success': True,
|
|
594
|
+
'event_id': followup_event_id,
|
|
595
|
+
'scheduled_time': follow_up_time.isoformat(),
|
|
596
|
+
'reminder_task_id': reminder_task_id
|
|
597
|
+
}
|
|
289
598
|
|
|
290
599
|
except Exception as e:
|
|
291
|
-
self.logger.error(f"Failed to schedule follow-up event: {str(e)}")
|
|
292
|
-
return {
|
|
293
|
-
'success': False,
|
|
294
|
-
'error': str(e)
|
|
295
|
-
|
|
600
|
+
self.logger.error(f"Failed to schedule follow-up event: {str(e)}")
|
|
601
|
+
return {
|
|
602
|
+
'success': False,
|
|
603
|
+
'error': str(e),
|
|
604
|
+
'reminder_task_id': None
|
|
605
|
+
}
|
|
296
606
|
|
|
297
607
|
def _get_scheduling_rule(self, org_id: str, team_id: str = None) -> Dict[str, Any]:
|
|
298
608
|
"""
|
|
@@ -615,4 +925,4 @@ class EventScheduler:
|
|
|
615
925
|
|
|
616
926
|
except Exception as e:
|
|
617
927
|
self.logger.error(f"Failed to create scheduling rule: {str(e)}")
|
|
618
|
-
return False
|
|
928
|
+
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|