fusesell 1.2.5__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusesell
3
- Version: 1.2.5
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
  fusesell.py,sha256=t5PjkhWEJGINp4k517u0EX0ge7lzuHOUHHro-BE1mGk,596
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
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
6
  fusesell_local/pipeline.py,sha256=zhAy8dtfcfjmRFqW9Dxr5fCGuxqvejFrIrJFw7s7hVU,39664
@@ -12,7 +12,7 @@ fusesell_local/stages/base_stage.py,sha256=ldo5xuHZto7ceEg3i_3rxAx0xPccK4n2jaxEJ
12
12
  fusesell_local/stages/data_acquisition.py,sha256=Td3mwakJRoEYbi3od4v2ZzKOHLgLSgccZVxH3ezs1_4,71081
13
13
  fusesell_local/stages/data_preparation.py,sha256=XWLg9b1w2NrMxLcrWDqB95mRmLQmVIMXpKNaBNr98TQ,52751
14
14
  fusesell_local/stages/follow_up.py,sha256=H9Xek6EoIbHrerQvGTRswXDNFH6zq71DcRxxj0zpo2g,77747
15
- fusesell_local/stages/initial_outreach.py,sha256=Y-WLtIGAg6nRlrCIs91l22giRrraOF6AGc1RlMDNqAM,110236
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=iG22AHat7rdz_tuhALagOCeHRVaBym6sl041ovVBlw0,185719
25
+ fusesell_local/utils/data_manager.py,sha256=60CLOVkVB76AQx1wQyja0PFmA1t-YJITiGNni14IPOs,188449
26
26
  fusesell_local/utils/event_scheduler.py,sha256=YwWIdkvRdWFdDLX-sepI5AXJOhEIullIclpk9njvZAA,38577
27
27
  fusesell_local/utils/llm_client.py,sha256=FVc25UlGt6hro7h5Iw7PHSXY3E3_67Xc-SUbHuMSRs0,10437
28
28
  fusesell_local/utils/logger.py,sha256=sWlV8Tjyz_Z8J4zXKOnNalh8_iD6ytfrwPZpD-wcEOs,6259
29
29
  fusesell_local/utils/timezone_detector.py,sha256=0cAE4c8ZXqCA8AvxRKm6PrFKmAmsbq3HOHR6w-mW3KQ,39997
30
30
  fusesell_local/utils/validators.py,sha256=Z1VzeoxFsnuzlIA_ZaMWoy-0Cgyqupd47kIdljlMDbs,15438
31
- fusesell-1.2.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,,
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,,
@@ -32,6 +32,6 @@ __all__ = [
32
32
  "validate_config",
33
33
  ]
34
34
 
35
- __version__ = "1.2.5"
35
+ __version__ = "1.2.6"
36
36
  __author__ = "FuseSell Team"
37
37
  __description__ = "Local implementation of FuseSell AI sales automation pipeline"
@@ -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
@@ -363,6 +372,110 @@ class InitialOutreachStage(BaseStage):
363
372
  'customextra': customextra
364
373
  }
365
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
+
366
479
  def _handle_close(self, context: Dict[str, Any]) -> Dict[str, Any]:
367
480
  """
368
481
  Handle close action - Close outreach when customer feels negative.
@@ -3,14 +3,15 @@ Local Data Manager for FuseSell Local Implementation
3
3
  Handles SQLite database operations and local file management
4
4
  """
5
5
 
6
- import sqlite3
6
+ import sqlite3
7
7
  import json
8
8
  import os
9
9
  import uuid
10
+ import shutil
10
11
  from typing import Dict, Any, List, Optional, Sequence, Union
11
- from datetime import datetime
12
- import logging
13
- from pathlib import Path
12
+ from datetime import datetime
13
+ import logging
14
+ from pathlib import Path
14
15
 
15
16
 
16
17
  class LocalDataManager:
@@ -69,10 +70,72 @@ class LocalDataManager:
69
70
  # Initialize database with optimization check
70
71
  self._init_database_optimized()
71
72
 
72
- def _create_directories(self) -> None:
73
- """Create necessary directories for data storage."""
74
- for directory in [self.data_dir, self.config_dir, self.drafts_dir, self.logs_dir]:
75
- directory.mkdir(parents=True, exist_ok=True)
73
+ def _create_directories(self) -> None:
74
+ """Create necessary directories for data storage."""
75
+ for directory in [self.data_dir, self.config_dir, self.drafts_dir, self.logs_dir]:
76
+ directory.mkdir(parents=True, exist_ok=True)
77
+ self._ensure_default_config_files()
78
+
79
+ def _ensure_default_config_files(self) -> None:
80
+ """
81
+ Copy bundled configuration defaults into the writable data directory when missing.
82
+
83
+ Ensures first-run executions always have the same baseline prompts, scoring criteria,
84
+ and email templates as the packaged FuseSell server flows.
85
+ """
86
+ try:
87
+ package_config_dir = Path(__file__).resolve().parents[2] / "fusesell_data" / "config"
88
+ except Exception as exc:
89
+ self.logger.debug(f"Unable to resolve packaged config directory: {exc}")
90
+ return
91
+
92
+ if not package_config_dir.exists():
93
+ self.logger.debug("Packaged config directory not found; skipping default config seeding")
94
+ return
95
+
96
+ default_files = [
97
+ "prompts.json",
98
+ "scoring_criteria.json",
99
+ "email_templates.json",
100
+ ]
101
+
102
+ for filename in default_files:
103
+ target = self.config_dir / filename
104
+ if target.exists():
105
+ continue
106
+
107
+ source = package_config_dir / filename
108
+ if not source.exists():
109
+ self.logger.debug(f"Packaged default {filename} not found; skipping seed")
110
+ continue
111
+
112
+ try:
113
+ shutil.copyfile(source, target)
114
+ self.logger.info(f"Seeded default configuration file: {filename}")
115
+ except Exception as exc:
116
+ self.logger.warning(f"Failed to seed default configuration {filename}: {exc}")
117
+
118
+ def _load_packaged_config_file(self, filename: str) -> Dict[str, Any]:
119
+ """
120
+ Load a configuration JSON file bundled with the package as a fallback.
121
+
122
+ Args:
123
+ filename: Name of the configuration file to load.
124
+
125
+ Returns:
126
+ Parsed configuration dictionary or empty dict on failure.
127
+ """
128
+ try:
129
+ package_config_dir = Path(__file__).resolve().parents[2] / "fusesell_data" / "config"
130
+ path = package_config_dir / filename
131
+ if not path.exists():
132
+ return {}
133
+
134
+ with path.open("r", encoding="utf-8") as handle:
135
+ return json.load(handle)
136
+ except Exception as exc:
137
+ self.logger.debug(f"Failed to load packaged config {filename}: {exc}")
138
+ return {}
76
139
 
77
140
  def _init_database_optimized(self) -> None:
78
141
  """
@@ -1050,56 +1113,71 @@ class LocalDataManager:
1050
1113
  self.logger.error(f"Failed to get stage results: {str(e)}")
1051
1114
  raise
1052
1115
 
1053
- def load_prompts(self) -> Dict[str, Any]:
1054
- """
1055
- Load prompt templates from configuration.
1056
-
1057
- Returns:
1058
- Dictionary of prompt templates
1059
- """
1060
- try:
1061
- prompts_file = self.config_dir / "prompts.json"
1062
- if prompts_file.exists():
1063
- with open(prompts_file, 'r', encoding='utf-8') as f:
1064
- return json.load(f)
1065
- return {}
1066
- except Exception as e:
1067
- self.logger.error(f"Failed to load prompts: {str(e)}")
1068
- return {}
1069
-
1070
- def load_scoring_criteria(self) -> Dict[str, Any]:
1116
+ def load_prompts(self) -> Dict[str, Any]:
1117
+ """
1118
+ Load prompt templates from configuration.
1119
+
1120
+ Returns:
1121
+ Dictionary of prompt templates
1122
+ """
1123
+ try:
1124
+ prompts_file = self.config_dir / "prompts.json"
1125
+ if prompts_file.exists():
1126
+ with open(prompts_file, 'r', encoding='utf-8') as f:
1127
+ return json.load(f)
1128
+
1129
+ packaged = self._load_packaged_config_file("prompts.json")
1130
+ if packaged:
1131
+ return packaged
1132
+
1133
+ return {}
1134
+ except Exception as e:
1135
+ self.logger.error(f"Failed to load prompts: {str(e)}")
1136
+ return {}
1137
+
1138
+ def load_scoring_criteria(self) -> Dict[str, Any]:
1071
1139
  """
1072
1140
  Load scoring criteria configuration.
1073
1141
 
1074
- Returns:
1075
- Dictionary of scoring criteria
1076
- """
1077
- try:
1078
- criteria_file = self.config_dir / "scoring_criteria.json"
1079
- if criteria_file.exists():
1080
- with open(criteria_file, 'r', encoding='utf-8') as f:
1081
- return json.load(f)
1082
- return {}
1083
- except Exception as e:
1084
- self.logger.error(f"Failed to load scoring criteria: {str(e)}")
1085
- return {}
1086
-
1087
- def load_email_templates(self) -> Dict[str, Any]:
1142
+ Returns:
1143
+ Dictionary of scoring criteria
1144
+ """
1145
+ try:
1146
+ criteria_file = self.config_dir / "scoring_criteria.json"
1147
+ if criteria_file.exists():
1148
+ with open(criteria_file, 'r', encoding='utf-8') as f:
1149
+ return json.load(f)
1150
+
1151
+ packaged = self._load_packaged_config_file("scoring_criteria.json")
1152
+ if packaged:
1153
+ return packaged
1154
+
1155
+ return {}
1156
+ except Exception as e:
1157
+ self.logger.error(f"Failed to load scoring criteria: {str(e)}")
1158
+ return {}
1159
+
1160
+ def load_email_templates(self) -> Dict[str, Any]:
1088
1161
  """
1089
1162
  Load email templates configuration.
1090
1163
 
1091
- Returns:
1092
- Dictionary of email templates
1093
- """
1094
- try:
1095
- templates_file = self.config_dir / "email_templates.json"
1096
- if templates_file.exists():
1097
- with open(templates_file, 'r', encoding='utf-8') as f:
1098
- return json.load(f)
1099
- return {}
1100
- except Exception as e:
1101
- self.logger.error(f"Failed to load email templates: {str(e)}")
1102
- return {}
1164
+ Returns:
1165
+ Dictionary of email templates
1166
+ """
1167
+ try:
1168
+ templates_file = self.config_dir / "email_templates.json"
1169
+ if templates_file.exists():
1170
+ with open(templates_file, 'r', encoding='utf-8') as f:
1171
+ return json.load(f)
1172
+
1173
+ packaged = self._load_packaged_config_file("email_templates.json")
1174
+ if packaged:
1175
+ return packaged
1176
+
1177
+ return {}
1178
+ except Exception as e:
1179
+ self.logger.error(f"Failed to load email templates: {str(e)}")
1180
+ return {}
1103
1181
 
1104
1182
  def _generate_customer_id(self) -> str:
1105
1183
  """Generate unique customer ID."""