fusesell 1.2.4__tar.gz → 1.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fusesell might be problematic. Click here for more details.

Files changed (43) hide show
  1. {fusesell-1.2.4 → fusesell-1.2.6}/CHANGELOG.md +18 -0
  2. {fusesell-1.2.4/fusesell.egg-info → fusesell-1.2.6}/PKG-INFO +1 -1
  3. {fusesell-1.2.4 → fusesell-1.2.6/fusesell.egg-info}/PKG-INFO +1 -1
  4. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/__init__.py +1 -1
  5. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/pipeline.py +11 -5
  6. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/stages/follow_up.py +98 -22
  7. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/stages/initial_outreach.py +224 -42
  8. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/utils/data_manager.py +265 -78
  9. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/utils/event_scheduler.py +386 -76
  10. {fusesell-1.2.4 → fusesell-1.2.6}/pyproject.toml +1 -1
  11. {fusesell-1.2.4 → fusesell-1.2.6}/LICENSE +0 -0
  12. {fusesell-1.2.4 → fusesell-1.2.6}/MANIFEST.in +0 -0
  13. {fusesell-1.2.4 → fusesell-1.2.6}/README.md +0 -0
  14. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell.egg-info/SOURCES.txt +0 -0
  15. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell.egg-info/dependency_links.txt +0 -0
  16. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell.egg-info/entry_points.txt +0 -0
  17. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell.egg-info/requires.txt +0 -0
  18. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell.egg-info/top_level.txt +0 -0
  19. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell.py +0 -0
  20. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/api.py +0 -0
  21. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/cli.py +0 -0
  22. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/config/__init__.py +0 -0
  23. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/config/prompts.py +0 -0
  24. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/config/settings.py +0 -0
  25. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/stages/__init__.py +0 -0
  26. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/stages/base_stage.py +0 -0
  27. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/stages/data_acquisition.py +0 -0
  28. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/stages/data_preparation.py +0 -0
  29. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/stages/lead_scoring.py +0 -0
  30. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/tests/conftest.py +0 -0
  31. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/tests/test_api.py +0 -0
  32. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/tests/test_cli.py +0 -0
  33. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/tests/test_data_manager_products.py +0 -0
  34. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/tests/test_data_manager_sales_process.py +0 -0
  35. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/tests/test_data_manager_teams.py +0 -0
  36. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/utils/__init__.py +0 -0
  37. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/utils/birthday_email_manager.py +0 -0
  38. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/utils/llm_client.py +0 -0
  39. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/utils/logger.py +0 -0
  40. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/utils/timezone_detector.py +0 -0
  41. {fusesell-1.2.4 → fusesell-1.2.6}/fusesell_local/utils/validators.py +0 -0
  42. {fusesell-1.2.4 → fusesell-1.2.6}/requirements.txt +0 -0
  43. {fusesell-1.2.4 → fusesell-1.2.6}/setup.cfg +0 -0
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes to FuseSell Local will be documented in this file.
4
4
 
5
+ # [1.2.6] - 2025-10-24
6
+
7
+ ### Added
8
+ - Automatically seed packaged prompt, scoring, and template JSON files into the writable `fusesell_data/config` directory so fresh installs immediately pick up the default initial outreach draft prompt.
9
+ - Draft generation now records the scheduled reminder metadata in stage output while mirroring the server’s `schedule_auto_run` behaviour locally.
10
+
11
+ ### Fixed
12
+ - Bundled configuration files are used as a fallback when the data directory is missing overrides, preventing empty prompt loads that previously produced low-quality duplicate drafts.
13
+
14
+ # [1.2.5] - 2025-10-24
15
+
16
+ ### Added
17
+ - Local `reminder_task` table and scheduler plumbing so scheduled outreach mirrors the server flow and can be consumed by RealTimeX orchestration.
18
+ - Initial outreach and follow-up stages now emit reminder metadata whenever emails are scheduled, including team/customer context.
19
+
20
+ ### Changed
21
+ - Event scheduler returns reminder IDs alongside scheduled events while preserving immutable default prompts when layering team overrides.
22
+
5
23
  # [1.2.3] - 2025-10-21
6
24
 
7
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusesell
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: Local implementation of FuseSell AI sales automation pipeline
5
5
  Author-email: FuseSell Team <team@fusesell.ai>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusesell
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: Local implementation of FuseSell AI sales automation pipeline
5
5
  Author-email: FuseSell Team <team@fusesell.ai>
6
6
  License-Expression: MIT
@@ -32,6 +32,6 @@ __all__ = [
32
32
  "validate_config",
33
33
  ]
34
34
 
35
- __version__ = "1.2.4"
35
+ __version__ = "1.2.6"
36
36
  __author__ = "FuseSell Team"
37
37
  __description__ = "Local implementation of FuseSell AI sales automation pipeline"
@@ -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(f"✅ Timing validation PASSED (discrepancy: {discrepancy_percentage:.1f}%)")
256
- else:
257
- self.logger.warning(f"⚠️ Timing validation WARNING (discrepancy: {discrepancy_percentage:.1f}%)")
258
- self.logger.warning(f" Expected ~{total_stage_time:.2f}s, got {total_duration:.2f}s")
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
- # Schedule the follow-up email event
886
- schedule_result = scheduler.schedule_email_event(
887
- draft_id=draft.get('draft_id'),
888
- recipient_address=recipient_address,
889
- recipient_name=recipient_name,
890
- org_id=input_data.get('org_id', 'default'),
891
- team_id=input_data.get('team_id'),
892
- customer_timezone=input_data.get('customer_timezone'),
893
- email_type='follow_up',
894
- send_immediately=send_immediately
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
- # Prepare final output
109
- outreach_data = {
110
- 'action': 'draft_write',
111
- 'status': 'drafts_generated',
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
- # Save to database
121
- self.save_stage_result(context, outreach_data)
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
- # Schedule the email event
260
- schedule_result = scheduler.schedule_email_event(
261
- draft_id=draft.get('draft_id'),
262
- recipient_address=recipient_address,
263
- recipient_name=recipient_name,
264
- org_id=input_data.get('org_id', 'default'),
265
- team_id=input_data.get('team_id'),
266
- customer_timezone=input_data.get('customer_timezone'),
267
- email_type='initial',
268
- send_immediately=send_immediately
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 _handle_close(self, context: Dict[str, Any]) -> Dict[str, Any]:
298
- """
299
- Handle close action - Close outreach when customer feels negative.
300
-
301
- Args:
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: